From ff1982788d4429ff03f4d2a5fc0376cee4bec59e Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Wed, 31 Jan 2024 11:54:03 -0500 Subject: [PATCH] refactor(elements): Split out contexts and hooks (#2695) * refactor(elements): Split out Sign In * refactor(elements): Standardize SignIn context * refactor(elements): Split out Sign Up * chore(elements): Remove temp router machine * chore(elements): Add changeset --- .changeset/wise-drinks-divide.md | 2 + .../nextjs/components/sign-in-debug.tsx | 6 +- .../nextjs/components/sign-up-debug.tsx | 6 +- .../machines/sign-in/sign-in.context.ts | 193 ------------------ .../machines/sign-in/sign-in.machine.ts | 6 + .../machines/sign-up/sign-up.context.ts | 132 ------------ .../machines/sign-up/sign-up.machine.ts | 7 +- .../src/react/hooks/use-active-states.hook.ts | 49 +++++ .../src/react/hooks/use-active-tags.hook.ts | 55 +++++ .../react/sign-in/contexts/sign-in.context.ts | 8 + .../sign-in/contexts/strategies.context.ts | 14 ++ .../elements/src/react/sign-in/continue.tsx | 16 +- .../sign-in/hooks/use-strategies.hook.ts | 21 ++ .../react/sign-in/hooks/use-strategy.hook.ts | 23 +++ .../hooks/use-third-party-provider.hook.ts | 45 ++++ packages/elements/src/react/sign-in/index.ts | 9 +- packages/elements/src/react/sign-in/root.tsx | 6 +- .../src/react/sign-in/social-providers.tsx | 4 +- packages/elements/src/react/sign-in/start.tsx | 9 +- .../src/react/sign-in/verifications.tsx | 15 +- .../react/sign-up/contexts/sign-up.context.ts | 8 + .../sign-up/contexts/strategies.context.ts | 13 ++ .../elements/src/react/sign-up/continue.tsx | 9 +- .../hooks/use-third-party-provider.hook.ts | 48 +++++ packages/elements/src/react/sign-up/index.ts | 9 +- packages/elements/src/react/sign-up/root.tsx | 6 +- .../src/react/sign-up/social-providers.tsx | 4 +- packages/elements/src/react/sign-up/start.tsx | 9 +- .../src/react/sign-up/verifications.tsx | 18 +- packages/elements/src/types/clerk.d.ts | 2 + 30 files changed, 376 insertions(+), 376 deletions(-) create mode 100644 .changeset/wise-drinks-divide.md delete mode 100644 packages/elements/src/internals/machines/sign-in/sign-in.context.ts delete mode 100644 packages/elements/src/internals/machines/sign-up/sign-up.context.ts create mode 100644 packages/elements/src/react/hooks/use-active-states.hook.ts create mode 100644 packages/elements/src/react/hooks/use-active-tags.hook.ts create mode 100644 packages/elements/src/react/sign-in/contexts/sign-in.context.ts create mode 100644 packages/elements/src/react/sign-in/contexts/strategies.context.ts create mode 100644 packages/elements/src/react/sign-in/hooks/use-strategies.hook.ts create mode 100644 packages/elements/src/react/sign-in/hooks/use-strategy.hook.ts create mode 100644 packages/elements/src/react/sign-in/hooks/use-third-party-provider.hook.ts create mode 100644 packages/elements/src/react/sign-up/contexts/sign-up.context.ts create mode 100644 packages/elements/src/react/sign-up/contexts/strategies.context.ts create mode 100644 packages/elements/src/react/sign-up/hooks/use-third-party-provider.hook.ts diff --git a/.changeset/wise-drinks-divide.md b/.changeset/wise-drinks-divide.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/wise-drinks-divide.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/elements/examples/nextjs/components/sign-in-debug.tsx b/packages/elements/examples/nextjs/components/sign-in-debug.tsx index 51e872fcdd..8801fcf9a7 100644 --- a/packages/elements/examples/nextjs/components/sign-in-debug.tsx +++ b/packages/elements/examples/nextjs/components/sign-in-debug.tsx @@ -1,13 +1,13 @@ 'use client'; import { SignedIn } from '@clerk/clerk-react'; -import { useSignInFlow, useSignInFlowSelector } from '@clerk/elements/sign-in'; +import { useSignInActorRef_internal, useSignInSelector_internal } from '@clerk/elements/sign-in'; import { SignOutButton } from '@clerk/nextjs'; import { Button } from './design'; function SignInActiveState() { - const activeState = useSignInFlowSelector(state => state.value); + const activeState = useSignInSelector_internal(state => state.value); const state = activeState ? (typeof activeState === 'string' ? activeState : JSON.stringify({ ...activeState })) : ''; return ( @@ -18,7 +18,7 @@ function SignInActiveState() { } export function SignInLogButtons() { - const ref = useSignInFlow(); + const ref = useSignInActorRef_internal(); return ( <> diff --git a/packages/elements/examples/nextjs/components/sign-up-debug.tsx b/packages/elements/examples/nextjs/components/sign-up-debug.tsx index 6bb47f1d43..75810f31c8 100644 --- a/packages/elements/examples/nextjs/components/sign-up-debug.tsx +++ b/packages/elements/examples/nextjs/components/sign-up-debug.tsx @@ -1,13 +1,13 @@ 'use client'; import { SignedIn } from '@clerk/clerk-react'; -import { useSignUpFlow, useSignUpFlowSelector } from '@clerk/elements/sign-up'; +import { useSignUpActorRef_internal, useSignUpSelector_internal } from '@clerk/elements/sign-up'; import { SignOutButton } from '@clerk/nextjs'; import { Button } from './design'; function SignUpActiveState() { - const activeState = useSignUpFlowSelector(state => state.value); + const activeState = useSignUpSelector_internal(state => state.value); const state = activeState ? (typeof activeState === 'string' ? activeState : JSON.stringify({ ...activeState })) : ''; return ( @@ -18,7 +18,7 @@ function SignUpActiveState() { } export function SignUpLogButtons() { - const ref = useSignUpFlow(); + const ref = useSignUpActorRef_internal(); return ( <> diff --git a/packages/elements/src/internals/machines/sign-in/sign-in.context.ts b/packages/elements/src/internals/machines/sign-in/sign-in.context.ts deleted file mode 100644 index 3e7cd3b478..0000000000 --- a/packages/elements/src/internals/machines/sign-in/sign-in.context.ts +++ /dev/null @@ -1,193 +0,0 @@ -import type { OAuthProvider, OAuthStrategy, SignInStrategy, Web3Provider, Web3Strategy } from '@clerk/types'; -import { createActorContext } from '@xstate/react'; -import type React from 'react'; -import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; -import type { SnapshotFrom } from 'xstate'; - -import { ClerkElementsRuntimeError } from '~/internals/errors/error'; -import { matchStrategy } from '~/internals/machines/utils/strategies'; -import type { UseThirdPartyProviderReturn } from '~/react/common/third-party-providers/social-provider'; -import { - getEnabledThirdPartyProviders, - isAuthenticatableOauthStrategy, - isWeb3Strategy, -} from '~/utils/third-party-strategies'; - -import { SignInMachine } from './sign-in.machine'; -import type { SignInStrategyName } from './sign-in.types'; - -export type SnapshotState = SnapshotFrom; - -// ================= MACHINE CONTEXT/HOOKS ================= // - -export const { - Provider: SignInFlowProvider, - useActorRef: useSignInFlow, - useSelector: useSignInFlowSelector, -} = createActorContext(SignInMachine); - -// ================= CONTEXTS ================= // - -export type StrategiesContextValue = { - current: SignInStrategy | undefined; - isActive: (name: string) => boolean; - preferred: SignInStrategy | undefined; -}; - -export const StrategiesContext = createContext({ - current: undefined, - isActive: _name => false, - preferred: undefined, -}); - -// ================= SELECTORS ================= // - -/** - * Selects the clerk environment - */ -const clerkEnvironmentSelector = (state: SnapshotState) => state.context.clerk.__unstable__environment; - -/** - * Selects the clerk environment - */ -const clerkCurrentStrategy = (state: SnapshotState) => state.context.currentFactor?.strategy; - -/** - * Selects the clerk third-party provider - */ -const clerkThirdPartyProviderSelector = (provider: OAuthProvider | Web3Provider) => (state: SnapshotState) => - state.context.thirdPartyProviders.providerToDisplayData[provider]; - -// ================= HOOKS ================= // - -export function useStrategy(name: SignInStrategyName) { - const ctx = useContext(StrategiesContext); - - if (!ctx) { - throw new ClerkElementsRuntimeError('useSignInStrategy must be used within a component.'); - } - - const { current, preferred, isActive } = ctx; - - return { - current, - preferred, - get shouldRender() { - return isActive(name); - }, - }; -} - -export const useSignInStateMatcher = () => { - return useSignInFlowSelector( - state => state, - (prev, next) => prev.value === next.value, - ); -}; - -export function useSignInStrategies(_preferred?: SignInStrategy) { - const state = useSignInStateMatcher(); - const current = useSignInFlowSelector(clerkCurrentStrategy); - - const shouldRender = state.matches('FirstFactor') || state.matches('SecondFactor'); - - const isActive = useCallback((name: string) => (current ? matchStrategy(current, name) : false), [current]); - - return { - current, - isActive, - shouldRender, - }; -} - -/** - * Provides the onClick handler for oauth - * - * @experimental - */ -export const useSignInThirdPartyProviders = () => { - const ref = useSignInFlow(); - const env = useSignInFlowSelector(clerkEnvironmentSelector); - const providers = useMemo(() => env && getEnabledThirdPartyProviders(env), [env]); - - // Register the onSubmit handler for button click - const createOnClick = useCallback( - (strategy: Web3Strategy | OAuthStrategy) => { - return (event: React.MouseEvent) => { - if (!providers) return; - - event.preventDefault(); - - if (isWeb3Strategy(strategy, providers.web3Strategies)) { - return ref.send({ type: 'AUTHENTICATE.WEB3', strategy }); - } - - if (isAuthenticatableOauthStrategy(strategy, providers.authenticatableOauthStrategies)) { - return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy }); - } - - throw new Error(`${strategy} is not yet supported.`); - }; - }, - [ref, providers], - ); - - if (!providers) { - return null; - } - - return { - ...providers, - createOnClick, - }; -}; - -export const useSignInThirdPartyProvider = (provider: OAuthProvider | Web3Provider): UseThirdPartyProviderReturn => { - const ref = useSignInFlow(); - const details = useSignInFlowSelector(clerkThirdPartyProviderSelector(provider)); - - const authenticate = useCallback( - (event: React.MouseEvent) => { - if (!details) return; - - event.preventDefault(); - - if (provider === 'metamask') { - return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' }); - } - - return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` }); - }, - [provider, details, ref], - ); - - if (!details) { - console.warn(`Please ensure that ${provider} is enabled.`); - return null; - } - - return { - events: { - authenticate, - }, - ...details, - }; -}; - -/** - * Ensures that the callback handler is sent to the machine once the environment is loaded - */ -export const useSSOCallbackHandler = () => { - const ref = useSignInFlow(); - const hasEnv = useSignInFlowSelector(clerkEnvironmentSelector); - - // TODO: Wholesale move this to the machine ? - // Wait for the environment to be loaded before sending the callback event - useEffect(() => { - if (!hasEnv) { - return; - } - - ref.send({ type: 'OAUTH.CALLBACK' }); - }, [hasEnv]); // eslint-disable-line react-hooks/exhaustive-deps -}; diff --git a/packages/elements/src/internals/machines/sign-in/sign-in.machine.ts b/packages/elements/src/internals/machines/sign-in/sign-in.machine.ts index 26fd316c70..2dd659d5ce 100644 --- a/packages/elements/src/internals/machines/sign-in/sign-in.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/sign-in.machine.ts @@ -49,6 +49,8 @@ export interface SignInMachineInput { router: ClerkRouter; } +export type SignInMachineTags = 'state:start' | 'state:first-factor' | 'state:second-factor' | 'external'; + export type SignInMachineEvents = | ErrorActorEvent | { type: 'AUTHENTICATE.OAUTH'; strategy: OAuthStrategy } @@ -62,6 +64,7 @@ export interface SignInMachineTypes { context: SignInMachineContext; input: SignInMachineInput; events: SignInMachineEvents; + tags: SignInMachineTags; } export const SignInMachine = setup({ @@ -237,6 +240,7 @@ export const SignInMachine = setup({ }, Start: { id: 'Start', + tags: 'state:start', description: 'The intial state of the sign-in flow.', initial: 'AwaitingInput', on: { @@ -291,6 +295,7 @@ export const SignInMachine = setup({ }, }, FirstFactor: { + tags: 'state:first-factor', initial: 'DeterminingState', entry: 'assignStartingFirstFactor', onDone: [ @@ -378,6 +383,7 @@ export const SignInMachine = setup({ }, }, SecondFactor: { + tags: 'state:second-factor', initial: 'DeterminingState', entry: 'assignStartingSecondFactor', onDone: [ diff --git a/packages/elements/src/internals/machines/sign-up/sign-up.context.ts b/packages/elements/src/internals/machines/sign-up/sign-up.context.ts deleted file mode 100644 index 6ba7fdc6d0..0000000000 --- a/packages/elements/src/internals/machines/sign-up/sign-up.context.ts +++ /dev/null @@ -1,132 +0,0 @@ -import type { OAuthProvider, OAuthStrategy, Web3Provider, Web3Strategy } from '@clerk/types'; -import { createActorContext } from '@xstate/react'; -import type React from 'react'; -import { createContext, useCallback, useMemo } from 'react'; -import type { SnapshotFrom } from 'xstate'; - -import type { UseThirdPartyProviderReturn } from '~/react/common/third-party-providers/social-provider'; -import { - getEnabledThirdPartyProviders, - isAuthenticatableOauthStrategy, - isWeb3Strategy, -} from '~/utils/third-party-strategies'; - -import { SignUpMachine } from './sign-up.machine'; - -export type SnapshotState = SnapshotFrom; - -// ================= MACHINE CONTEXT/HOOKS ================= // - -export const { - Provider: SignUpFlowProvider, - useActorRef: useSignUpFlow, - useSelector: useSignUpFlowSelector, -} = createActorContext(SignUpMachine); - -// ================= CONTEXTS ================= // - -export type StrategiesContextValue = { - current: string | undefined; // TODO: Update type - isActive: (name: string) => boolean; - preferred: string | undefined; // TODO: Update type -}; - -export const StrategiesContext = createContext({ - current: undefined, - isActive: _name => false, - preferred: undefined, -}); - -// ================= SELECTORS ================= // - -/** - * Selects the clerk environment - */ -const clerkEnvironmentSelector = (state: SnapshotState) => state.context.clerk.__unstable__environment; - -/** - * Selects the clerk third-party provider - */ -const clerkThirdPartyProviderSelector = (provider: OAuthProvider | Web3Provider) => (state: SnapshotState) => - state.context.thirdPartyProviders.providerToDisplayData[provider]; - -// ================= HOOKS ================= // - -export const useSignUpStateMatcher = () => { - return useSignUpFlowSelector( - state => state, - (prev, next) => prev.value === next.value, - ); -}; - -/** - * Provides the onClick handler for oauth - */ -export const useSignUpThirdPartyProviders = () => { - const ref = useSignUpFlow(); - const env = useSignUpFlowSelector(clerkEnvironmentSelector); - const providers = useMemo(() => env && getEnabledThirdPartyProviders(env), [env]); - - // Register the onSubmit handler for button click - const createOnClick = useCallback( - (strategy: Web3Strategy | OAuthStrategy) => { - return (event: React.MouseEvent) => { - if (!providers) return; - - event.preventDefault(); - - if (isWeb3Strategy(strategy, providers.web3Strategies)) { - return ref.send({ type: 'AUTHENTICATE.WEB3', strategy }); - } - - if (isAuthenticatableOauthStrategy(strategy, providers.authenticatableOauthStrategies)) { - return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy }); - } - - throw new Error(`${strategy} is not yet supported.`); - }; - }, - [ref, providers], - ); - - if (!providers) { - return null; - } - - return { - ...providers, - createOnClick, - }; -}; - -export const useSignUpThirdPartyProvider = (provider: OAuthProvider | Web3Provider): UseThirdPartyProviderReturn => { - const ref = useSignUpFlow(); - const details = useSignUpFlowSelector(clerkThirdPartyProviderSelector(provider)); - - const authenticate = useCallback( - (event: React.MouseEvent) => { - if (!details) return; - - event.preventDefault(); - - if (provider === 'metamask') { - return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' }); - } - - return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` }); - }, - [provider, details, ref], - ); - - if (!details) { - console.warn(`Please ensure that ${provider} is enabled.`); - return null; - } - - return { - events: { - authenticate, - }, - ...details, - }; -}; diff --git a/packages/elements/src/internals/machines/sign-up/sign-up.machine.ts b/packages/elements/src/internals/machines/sign-up/sign-up.machine.ts index 74a5048d88..3ff2360000 100644 --- a/packages/elements/src/internals/machines/sign-up/sign-up.machine.ts +++ b/packages/elements/src/internals/machines/sign-up/sign-up.machine.ts @@ -68,7 +68,9 @@ export type SignUpMachineEvents = export type SignUpVerificationTags = 'code' | VerificationStrategy; -export type SignUpTags = SignUpVerificationTags | 'external'; +export type SignUpStateTags = 'state:start' | 'state:continue' | 'state:verification'; + +export type SignUpTags = SignUpStateTags | SignUpVerificationTags | 'external'; export type SignUpDelays = 'TIMEOUT.POLLING'; export interface SignUpMachineTypes { @@ -333,6 +335,7 @@ export const SignUpMachine = setup({ }, Start: { id: 'Start', + tags: 'state:start', description: 'The intial state of the sign-in flow.', entry: 'assignThirdPartyProviders', initial: 'AwaitingInput', @@ -390,9 +393,11 @@ export const SignUpMachine = setup({ }, }, Continue: { + tags: 'state:continue', entry: log('#SignUp.Continue: Not implemented.'), }, Verification: { + tags: 'state:verification', description: 'Verification state of the sign-up flow', initial: 'Init', states: { diff --git a/packages/elements/src/react/hooks/use-active-states.hook.ts b/packages/elements/src/react/hooks/use-active-states.hook.ts new file mode 100644 index 0000000000..3497fe6802 --- /dev/null +++ b/packages/elements/src/react/hooks/use-active-states.hook.ts @@ -0,0 +1,49 @@ +import { useSelector } from '@xstate/react'; +import type { ActorRef, AnyActorRef, AnyMachineSnapshot, MachineSnapshot } from 'xstate'; + +type StatefulActor = TActor extends ActorRef< + MachineSnapshot, + any +> + ? TStateValue + : never; + +/** + * Generic hook to check if a state is active. + * + * @example + * const ref = SignUpCtx.useActorRef(); + + * useActiveStates(ref, { Start: 'Attempting' }); + * useActiveStates(ref, [{ Start: 'AwaitingInput' }, { Start: 'Attempting' }]); + * + * @param actor {ActorRef} Machine actor reference + * @param state {StateValue | StateValue[]} The state(s) to check + * @param exact {boolean} Whether to match all tags or any tag + * + * @returns {boolean} + */ +export function useActiveStates>( + actor: TActor, + state: TState, +): boolean; +export function useActiveStates>( + actor: TActor, + states: TState[], +): boolean; +export function useActiveStates>( + actor: TActor, + states: TState | TState[], +): boolean { + const currentState = useSelector( + actor, + s => s, + (prev, next) => prev.value === next.value, + ); + + if (Array.isArray(states)) { + return states.some(s => currentState.hasTag(s)); + } + + return currentState.hasTag(states); +} diff --git a/packages/elements/src/react/hooks/use-active-tags.hook.ts b/packages/elements/src/react/hooks/use-active-tags.hook.ts new file mode 100644 index 0000000000..8aa3182b35 --- /dev/null +++ b/packages/elements/src/react/hooks/use-active-tags.hook.ts @@ -0,0 +1,55 @@ +import { useSelector } from '@xstate/react'; +import type { ActorRef, AnyActorRef, AnyMachineSnapshot, MachineSnapshot } from 'xstate'; + +type TaggedActor = TActor extends ActorRef< + MachineSnapshot, + any +> + ? TTags + : never; + +/** + * Generic hook to check if a tag is active. + * + * @example + * const ref = SignUpCtx.useActorRef(); + * + * useActiveTags(ref, 'external'); + * useActiveTags(ref, ['external', 'email_code']); + * + * @param actor {ActorRef} Machine actor reference + * @param tag {string | string[]} The tag(s) to check + * @param exact {boolean} Whether to match all tags or any tag + * + * @returns {boolean} + */ +export function useActiveTags>( + actor: TActor, + tag: TTag, +): boolean; +export function useActiveTags>( + actor: TActor, + tags: TTag[], + exact?: boolean, +): boolean; +export function useActiveTags>( + actor: TActor, + tags: TTag | TTag[], + exact?: boolean, +): boolean { + const currentState = useSelector( + actor, + s => s, + (prev, next) => prev.tags === next.tags, + ); + + if (typeof tags === 'string') { + return currentState.hasTag(tags); + } + + if (Array.isArray(tags)) { + return exact ? tags.every(tag => currentState.hasTag(tag)) : tags.some(tag => currentState.hasTag(tag)); + } + + return false; +} diff --git a/packages/elements/src/react/sign-in/contexts/sign-in.context.ts b/packages/elements/src/react/sign-in/contexts/sign-in.context.ts new file mode 100644 index 0000000000..f726a2feb3 --- /dev/null +++ b/packages/elements/src/react/sign-in/contexts/sign-in.context.ts @@ -0,0 +1,8 @@ +import { createActorContext } from '@xstate/react'; +import type { SnapshotFrom } from 'xstate'; + +import { SignInMachine } from '~/internals/machines/sign-in/sign-in.machine'; + +export type SnapshotState = SnapshotFrom; + +export const SignInCtx = createActorContext(SignInMachine); diff --git a/packages/elements/src/react/sign-in/contexts/strategies.context.ts b/packages/elements/src/react/sign-in/contexts/strategies.context.ts new file mode 100644 index 0000000000..06bd07f05d --- /dev/null +++ b/packages/elements/src/react/sign-in/contexts/strategies.context.ts @@ -0,0 +1,14 @@ +import type { SignInStrategy } from '@clerk/types'; +import { createContext } from 'react'; + +export type StrategiesContextValue = { + current: SignInStrategy | undefined; + isActive: (name: string) => boolean; + preferred: SignInStrategy | undefined; +}; + +export const StrategiesContext = createContext({ + current: undefined, + isActive: _name => false, + preferred: undefined, +}); diff --git a/packages/elements/src/react/sign-in/continue.tsx b/packages/elements/src/react/sign-in/continue.tsx index c9af9dc399..9bf01afc91 100644 --- a/packages/elements/src/react/sign-in/continue.tsx +++ b/packages/elements/src/react/sign-in/continue.tsx @@ -1,20 +1,24 @@ 'use client'; import type { SignInStrategy as ClerkSignInStrategy } from '@clerk/types'; -import type { PropsWithChildren } from 'react'; +import { type PropsWithChildren } from 'react'; -import { StrategiesContext, useSignInFlow, useSignInStrategies } from '~/internals/machines/sign-in/sign-in.context'; import { Form } from '~/react/common/form'; +import { useActiveTags } from '~/react/hooks/use-active-tags.hook'; +import { SignInCtx } from '~/react/sign-in/contexts/sign-in.context'; +import { StrategiesContext } from '~/react/sign-in/contexts/strategies.context'; +import { useStrategies } from '~/react/sign-in/hooks/use-strategies.hook'; export type SignInContinueProps = PropsWithChildren<{ preferred?: ClerkSignInStrategy }>; export function SignInContinue({ children, preferred }: SignInContinueProps) { - const { current, isActive, shouldRender } = useSignInStrategies(preferred); - const actorRef = useSignInFlow(); + const ref = SignInCtx.useActorRef(); + const activeState = useActiveTags(ref, ['state:first-factor', 'state:second-factor']); + const { current, isActive } = useStrategies(preferred); - return shouldRender ? ( + return activeState ? ( -
{children}
+
{children}
) : null; } diff --git a/packages/elements/src/react/sign-in/hooks/use-strategies.hook.ts b/packages/elements/src/react/sign-in/hooks/use-strategies.hook.ts new file mode 100644 index 0000000000..b781441236 --- /dev/null +++ b/packages/elements/src/react/sign-in/hooks/use-strategies.hook.ts @@ -0,0 +1,21 @@ +import type { SignInStrategy } from '@clerk/types'; +import { useCallback } from 'react'; + +import { matchStrategy } from '~/internals/machines/utils/strategies'; +import type { SnapshotState } from '~/react/sign-in/contexts/sign-in.context'; +import { SignInCtx } from '~/react/sign-in/contexts/sign-in.context'; + +/** + * Selects the Clerk current strategy + */ +const selector = (state: SnapshotState) => state.context.currentFactor?.strategy; + +export function useStrategies(_preferred?: SignInStrategy) { + const current = SignInCtx.useSelector(selector); + const isActive = useCallback((name: string) => (current ? matchStrategy(current, name) : false), [current]); + + return { + current, + isActive, + }; +} diff --git a/packages/elements/src/react/sign-in/hooks/use-strategy.hook.ts b/packages/elements/src/react/sign-in/hooks/use-strategy.hook.ts new file mode 100644 index 0000000000..38fe31e1b0 --- /dev/null +++ b/packages/elements/src/react/sign-in/hooks/use-strategy.hook.ts @@ -0,0 +1,23 @@ +import { useContext } from 'react'; + +import { ClerkElementsRuntimeError } from '~/internals/errors/error'; +import type { SignInStrategyName } from '~/internals/machines/sign-in/sign-in.types'; +import { StrategiesContext } from '~/react/sign-in/contexts/strategies.context'; + +export function useStrategy(name: SignInStrategyName) { + const ctx = useContext(StrategiesContext); + + if (!ctx) { + throw new ClerkElementsRuntimeError('useStrategy must be used within a component.'); + } + + const { current, preferred, isActive } = ctx; + + return { + current, + preferred, + get active() { + return isActive(name); + }, + }; +} diff --git a/packages/elements/src/react/sign-in/hooks/use-third-party-provider.hook.ts b/packages/elements/src/react/sign-in/hooks/use-third-party-provider.hook.ts new file mode 100644 index 0000000000..5fff339dee --- /dev/null +++ b/packages/elements/src/react/sign-in/hooks/use-third-party-provider.hook.ts @@ -0,0 +1,45 @@ +import type { OAuthProvider, Web3Provider } from '@clerk/types'; +import type React from 'react'; +import { useCallback } from 'react'; + +import type { UseThirdPartyProviderReturn } from '~/react/common/third-party-providers/social-provider'; +import type { SnapshotState } from '~/react/sign-in/contexts/sign-in.context'; +import { SignInCtx } from '~/react/sign-in/contexts/sign-in.context'; + +/** + * Selects the clerk third-party provider + */ +const selector = (provider: OAuthProvider | Web3Provider) => (state: SnapshotState) => + state.context.thirdPartyProviders.providerToDisplayData[provider]; + +export const useThirdPartyProvider = (provider: OAuthProvider | Web3Provider): UseThirdPartyProviderReturn => { + const ref = SignInCtx.useActorRef(); + const details = SignInCtx.useSelector(selector(provider)); + + const authenticate = useCallback( + (event: React.MouseEvent) => { + if (!details) return; + + event.preventDefault(); + + if (provider === 'metamask') { + return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' }); + } + + return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` }); + }, + [provider, details, ref], + ); + + if (!details) { + console.warn(`Please ensure that ${provider} is enabled.`); + return null; + } + + return { + events: { + authenticate, + }, + ...details, + }; +}; diff --git a/packages/elements/src/react/sign-in/index.ts b/packages/elements/src/react/sign-in/index.ts index a8ad8ad6e5..e59ad1c92d 100644 --- a/packages/elements/src/react/sign-in/index.ts +++ b/packages/elements/src/react/sign-in/index.ts @@ -1,5 +1,7 @@ 'use client'; +import { SignInCtx } from '~/react/sign-in/contexts/sign-in.context'; + export { SignInContinue as Continue } from './continue'; export { SignInRoot as SignIn, SignInRoot as Root } from './root'; export { @@ -9,5 +11,8 @@ export { export { SignInStart as Start } from './start'; export { SignInFactor as Factor, SignInVerification as Verification } from './verifications'; -// TODO: Move contexts from /internals to /react -export { useSignInFlow, useSignInFlowSelector } from '~/internals/machines/sign-in/sign-in.context'; +/** @internal Internal use only */ +export const useSignInActorRef_internal = SignInCtx.useActorRef; + +/** @internal Internal use only */ +export const useSignInSelector_internal = SignInCtx.useSelector; diff --git a/packages/elements/src/react/sign-in/root.tsx b/packages/elements/src/react/sign-in/root.tsx index f4aee729e1..c01ba8b00c 100644 --- a/packages/elements/src/react/sign-in/root.tsx +++ b/packages/elements/src/react/sign-in/root.tsx @@ -4,8 +4,8 @@ import { ClerkLoaded, useClerk } from '@clerk/clerk-react'; import type { PropsWithChildren } from 'react'; import { FormStoreProvider, useFormStore } from '~/internals/machines/form/form.context'; -import { SignInFlowProvider as SignInFlowContextProvider } from '~/internals/machines/sign-in/sign-in.context'; import { Router, useClerkRouter, useNextRouter } from '~/react/router'; +import { SignInCtx } from '~/react/sign-in/contexts/sign-in.context'; import { createBrowserInspectorReactHook } from '~/react/utils/xstate'; const { useBrowserInspector } = createBrowserInspectorReactHook(); @@ -25,7 +25,7 @@ function SignInFlowProvider({ children }: PropsWithChildren) { } return ( - {children} - + ); } diff --git a/packages/elements/src/react/sign-in/social-providers.tsx b/packages/elements/src/react/sign-in/social-providers.tsx index 9c8f1dddb5..c4504a7dbe 100644 --- a/packages/elements/src/react/sign-in/social-providers.tsx +++ b/packages/elements/src/react/sign-in/social-providers.tsx @@ -2,16 +2,16 @@ import type { OAuthProvider, Web3Provider } from '@clerk/types'; -import { useSignInThirdPartyProvider } from '~/internals/machines/sign-in/sign-in.context'; import type { SocialProviderProps } from '~/react/common/third-party-providers/social-provider'; import { SocialProvider, SocialProviderIcon } from '~/react/common/third-party-providers/social-provider'; +import { useThirdPartyProvider } from '~/react/sign-in/hooks/use-third-party-provider.hook'; export interface SignInSocialProviderProps extends Omit { name: OAuthProvider | Web3Provider; } export function SignInSocialProvider({ name, ...rest }: SignInSocialProviderProps) { - const thirdPartyProvider = useSignInThirdPartyProvider(name); + const thirdPartyProvider = useThirdPartyProvider(name); return ( {children} : null; + return activeState ?
{children}
: null; } diff --git a/packages/elements/src/react/sign-in/verifications.tsx b/packages/elements/src/react/sign-in/verifications.tsx index 289a46eac5..6cfb301773 100644 --- a/packages/elements/src/react/sign-in/verifications.tsx +++ b/packages/elements/src/react/sign-in/verifications.tsx @@ -1,21 +1,26 @@ 'use client'; -import { useSignInStateMatcher, useStrategy } from '~/internals/machines/sign-in/sign-in.context'; import type { SignInStrategyName } from '~/internals/machines/sign-in/sign-in.types'; +import { useActiveTags } from '~/react/hooks/use-active-tags.hook'; +import { SignInCtx } from '~/react/sign-in/contexts/sign-in.context'; +import { useStrategy } from '~/react/sign-in/hooks/use-strategy.hook'; export type SignInFactorProps = React.PropsWithChildren< { first: true; second?: never } | { first?: never; second: true } >; export function SignInFactor({ children, first, second }: SignInFactorProps) { - const state = useSignInStateMatcher(); - const render = (first && state.matches('FirstFactor')) || (second && state.matches('SecondFactor')); + const ref = SignInCtx.useActorRef(); + const activeFirstState = useActiveTags(ref, 'state:first-factor'); + const activeSecondState = useActiveTags(ref, 'state:second-factor'); + + const render = (first && activeFirstState) || (second && activeSecondState); return render ? children : null; } export type SignInVerificationProps = React.PropsWithChildren<{ name: SignInStrategyName }>; export function SignInVerification({ children, name }: SignInVerificationProps) { - const { shouldRender } = useStrategy(name); - return shouldRender ? children : null; + const { active } = useStrategy(name); + return active ? children : null; } diff --git a/packages/elements/src/react/sign-up/contexts/sign-up.context.ts b/packages/elements/src/react/sign-up/contexts/sign-up.context.ts new file mode 100644 index 0000000000..b6fee53abb --- /dev/null +++ b/packages/elements/src/react/sign-up/contexts/sign-up.context.ts @@ -0,0 +1,8 @@ +import { createActorContext } from '@xstate/react'; +import type { SnapshotFrom } from 'xstate'; + +import { SignUpMachine } from '~/internals/machines/sign-up/sign-up.machine'; + +export type SnapshotState = SnapshotFrom; + +export const SignUpCtx = createActorContext(SignUpMachine); diff --git a/packages/elements/src/react/sign-up/contexts/strategies.context.ts b/packages/elements/src/react/sign-up/contexts/strategies.context.ts new file mode 100644 index 0000000000..cbd55fa181 --- /dev/null +++ b/packages/elements/src/react/sign-up/contexts/strategies.context.ts @@ -0,0 +1,13 @@ +import { createContext } from 'react'; + +export type StrategiesContextValue = { + current: string | undefined; // TODO: Update type + isActive: (name: string) => boolean; + preferred: string | undefined; // TODO: Update type +}; + +export const StrategiesContext = createContext({ + current: undefined, + isActive: _name => false, + preferred: undefined, +}); diff --git a/packages/elements/src/react/sign-up/continue.tsx b/packages/elements/src/react/sign-up/continue.tsx index dae513e420..f04060d35e 100644 --- a/packages/elements/src/react/sign-up/continue.tsx +++ b/packages/elements/src/react/sign-up/continue.tsx @@ -2,14 +2,15 @@ import type { PropsWithChildren } from 'react'; -import { useSignUpFlow, useSignUpStateMatcher } from '~/internals/machines/sign-up/sign-up.context'; import { Form } from '~/react/common/form'; +import { useActiveTags } from '~/react/hooks/use-active-tags.hook'; +import { SignUpCtx } from '~/react/sign-up/contexts/sign-up.context'; export type SignUpContinueProps = PropsWithChildren; export function SignUpContinue({ children }: SignUpContinueProps) { - const state = useSignUpStateMatcher(); - const actorRef = useSignUpFlow(); + const ref = SignUpCtx.useActorRef(); + const active = useActiveTags(ref, 'state:continue'); - return state.matches('Continue') ?
{children}
: null; + return active ?
{children}
: null; } diff --git a/packages/elements/src/react/sign-up/hooks/use-third-party-provider.hook.ts b/packages/elements/src/react/sign-up/hooks/use-third-party-provider.hook.ts new file mode 100644 index 0000000000..49c59a2420 --- /dev/null +++ b/packages/elements/src/react/sign-up/hooks/use-third-party-provider.hook.ts @@ -0,0 +1,48 @@ +import type { OAuthProvider, Web3Provider } from '@clerk/types'; +import type React from 'react'; +import { useCallback } from 'react'; +import type { SnapshotFrom } from 'xstate'; + +import type { SignUpMachine } from '~/internals/machines/sign-up/sign-up.machine'; +import type { UseThirdPartyProviderReturn } from '~/react/common/third-party-providers/social-provider'; +import { SignUpCtx } from '~/react/sign-up/contexts/sign-up.context'; + +export type SnapshotState = SnapshotFrom; + +/** + * Selects the clerk third-party provider + */ +const clerkThirdPartyProviderSelector = (provider: OAuthProvider | Web3Provider) => (state: SnapshotState) => + state.context.thirdPartyProviders.providerToDisplayData[provider]; + +export const useThirdPartyProvider = (provider: OAuthProvider | Web3Provider): UseThirdPartyProviderReturn => { + const ref = SignUpCtx.useActorRef(); + const details = SignUpCtx.useSelector(clerkThirdPartyProviderSelector(provider)); + + const authenticate = useCallback( + (event: React.MouseEvent) => { + if (!details) return; + + event.preventDefault(); + + if (provider === 'metamask') { + return ref.send({ type: 'AUTHENTICATE.WEB3', strategy: 'web3_metamask_signature' }); + } + + return ref.send({ type: 'AUTHENTICATE.OAUTH', strategy: `oauth_${provider}` }); + }, + [provider, details, ref], + ); + + if (!details) { + console.warn(`Please ensure that ${provider} is enabled.`); + return null; + } + + return { + events: { + authenticate, + }, + ...details, + }; +}; diff --git a/packages/elements/src/react/sign-up/index.ts b/packages/elements/src/react/sign-up/index.ts index 93e6384593..8fc3bd183f 100644 --- a/packages/elements/src/react/sign-up/index.ts +++ b/packages/elements/src/react/sign-up/index.ts @@ -1,5 +1,7 @@ 'use client'; +import { SignUpCtx } from '~/react/sign-up/contexts/sign-up.context'; + export { SignUpContinue as Continue } from './continue'; export { SignUpRoot as SignUp, SignUpRoot as Root } from './root'; export { @@ -9,5 +11,8 @@ export { export { SignUpStart as Start } from './start'; export { SignUpVerification as Verification, SignUpVerify as Verify } from './verifications'; -// TODO: Move contexts from /internals to /react -export { useSignUpFlow, useSignUpFlowSelector } from '~/internals/machines/sign-up/sign-up.context'; +/** @internal Internal use only */ +export const useSignUpActorRef_internal = SignUpCtx.useActorRef; + +/** @internal Internal use only */ +export const useSignUpSelector_internal = SignUpCtx.useSelector; diff --git a/packages/elements/src/react/sign-up/root.tsx b/packages/elements/src/react/sign-up/root.tsx index 5c42e8ceb4..f03cadcdfb 100644 --- a/packages/elements/src/react/sign-up/root.tsx +++ b/packages/elements/src/react/sign-up/root.tsx @@ -4,8 +4,8 @@ import { ClerkLoaded, useClerk } from '@clerk/clerk-react'; import type { PropsWithChildren } from 'react'; import { FormStoreProvider, useFormStore } from '~/internals/machines/form/form.context'; -import { SignUpFlowProvider as SignUpFlowContextProvider } from '~/internals/machines/sign-up/sign-up.context'; import { Router, useClerkRouter, useNextRouter } from '~/react/router'; +import { SignUpCtx } from '~/react/sign-up/contexts/sign-up.context'; import { createBrowserInspectorReactHook } from '~/react/utils/xstate'; const { useBrowserInspector } = createBrowserInspectorReactHook(); @@ -25,7 +25,7 @@ function SignUpFlowProvider({ children }: PropsWithChildren) { } return ( - {children} - + ); } diff --git a/packages/elements/src/react/sign-up/social-providers.tsx b/packages/elements/src/react/sign-up/social-providers.tsx index 4aa1771707..ea3784a363 100644 --- a/packages/elements/src/react/sign-up/social-providers.tsx +++ b/packages/elements/src/react/sign-up/social-providers.tsx @@ -2,7 +2,7 @@ import type { OAuthProvider, Web3Provider } from '@clerk/types'; -import { useSignUpThirdPartyProvider } from '~/internals/machines/sign-up/sign-up.context'; +import { useThirdPartyProvider } from '~/react/sign-up/hooks/use-third-party-provider.hook'; import type { SocialProviderProps } from '../common/third-party-providers/social-provider'; import { SocialProvider, SocialProviderIcon } from '../common/third-party-providers/social-provider'; @@ -12,7 +12,7 @@ export interface SignUpSocialProviderProps extends Omit{children} : null; + return active ?
{children}
: null; } diff --git a/packages/elements/src/react/sign-up/verifications.tsx b/packages/elements/src/react/sign-up/verifications.tsx index e819481a2c..6dcf6cddad 100644 --- a/packages/elements/src/react/sign-up/verifications.tsx +++ b/packages/elements/src/react/sign-up/verifications.tsx @@ -2,22 +2,26 @@ import type { PropsWithChildren } from 'react'; -import { useSignUpFlow, useSignUpStateMatcher } from '~/internals/machines/sign-up/sign-up.context'; import type { SignUpVerificationTags } from '~/internals/machines/sign-up/sign-up.machine'; import { Form } from '~/react/common/form'; +import { SignUpCtx } from '~/react/sign-up/contexts/sign-up.context'; + +import { useActiveTags } from '../hooks/use-active-tags.hook'; export type SignUpVerifyProps = PropsWithChildren; export function SignUpVerify({ children }: SignUpVerifyProps) { - const actorRef = useSignUpFlow(); - const state = useSignUpStateMatcher(); + const ref = SignUpCtx.useActorRef(); + const active = useActiveTags(ref, 'state:verification'); - return state.matches('Verification') ?
{children}
: null; + return active ?
{children}
: null; } export type SignUpVerificationProps = PropsWithChildren<{ name: SignUpVerificationTags }>; -export function SignUpVerification({ children, name }: SignUpVerificationProps) { - const state = useSignUpStateMatcher(); - return state.hasTag(name) ? children : null; +export function SignUpVerification({ children, name: tag }: SignUpVerificationProps) { + const ref = SignUpCtx.useActorRef(); + const active = useActiveTags(ref, tag); + + return active ? children : null; } diff --git a/packages/elements/src/types/clerk.d.ts b/packages/elements/src/types/clerk.d.ts index 9e4d4c7802..52118b0568 100644 --- a/packages/elements/src/types/clerk.d.ts +++ b/packages/elements/src/types/clerk.d.ts @@ -7,3 +7,5 @@ declare module '@clerk/types' { __unstable__environment: EnvironmentResource | null | undefined; } } + +type TODO = any;