From 55f955fc75da782055ee1fe658f90311fbfb292d Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Fri, 5 Jan 2024 19:52:02 -0500 Subject: [PATCH] chore(elements): Introduce Strategies (#2497) * feat(elements): First Factor interim commit * feat(elements): Introduce Strategies * chore(elements): Introduce Strategies * chore(elements): Cleanup commented code * chore(elements): Remove actor wrapper around determineStartingSignInFactor --------- Co-authored-by: Bryce Kalow --- .changeset/soft-apples-chew.md | 2 + .../app/sign-in/[[...sign-in]]/page.tsx | 163 ++++++++++++++---- .../examples/nextjs/components/debug.tsx | 8 +- .../examples/nextjs/components/design.tsx | 36 ++++ packages/elements/package.json | 2 +- packages/elements/src/common/strategy.tsx | 25 +++ packages/elements/src/index.tsx | 10 +- .../src/internals/machines/sign-in.actors.ts | 25 +-- .../src/internals/machines/sign-in.context.ts | 59 ++++++- .../src/internals/machines/sign-in.machine.ts | 90 +++++----- .../src/internals/machines/sign-in.types.ts | 3 + .../src/internals/machines/sign-in.utils.ts | 2 +- .../internals/machines/utils/assert.test.ts | 42 +++++ .../src/internals/machines/utils/assert.ts | 2 +- .../machines/utils/strategies.test.ts | 24 +++ .../internals/machines/utils/strategies.ts | 11 ++ packages/elements/src/sign-in/index.tsx | 60 ++++++- packages/elements/turbo.json | 21 +++ 18 files changed, 464 insertions(+), 121 deletions(-) create mode 100644 .changeset/soft-apples-chew.md create mode 100644 packages/elements/examples/nextjs/components/design.tsx create mode 100644 packages/elements/src/common/strategy.tsx create mode 100644 packages/elements/src/internals/machines/utils/assert.test.ts create mode 100644 packages/elements/src/internals/machines/utils/strategies.test.ts create mode 100644 packages/elements/src/internals/machines/utils/strategies.ts diff --git a/.changeset/soft-apples-chew.md b/.changeset/soft-apples-chew.md new file mode 100644 index 0000000000..a845151cc8 --- /dev/null +++ b/.changeset/soft-apples-chew.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx index e7846fca53..3c7497fbe9 100644 --- a/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx +++ b/packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx @@ -10,6 +10,8 @@ import { SignInFactorTwo, SignInSSOCallback, SignInStart, + SignInStrategies, + SignInStrategy, SocialProviders, Submit, } from '@clerk/elements'; @@ -19,6 +21,7 @@ import type { CSSProperties } from 'react'; import { forwardRef } from 'react'; import { Debug } from '@/components/debug'; +import { H1, H2, H3, HR, P } from '@/components/design'; const BUTTON_BGS: Record = { github: 'rgba(23 23 23)', @@ -50,7 +53,7 @@ export default function SignInPage() {
-

START

+

START

{ @@ -82,7 +85,7 @@ export default function SignInPage() { />
-
+
- Sign In + Sign In with Email/Password + + +
+ + +
+ + + + +
+ + ( + + )} + /> +
+ + + Sign In with Phone Number
- -
-

FIRST FACTOR

- - -
- - +
+

STRATEGIES (FIRST/SECOND FACTOR)

+ +

+ First Factor + Second Factor +

+ + + +
+ + +
+ + ( + + )} + /> +
+ + + Sign In + +
+ + + +
+ + +
+ + ( + + )} /> -
+ + + + Sign In + + + + +

Verify your email

+ +

Please check your email for a verification code...

- ( - +
+ + - )} - /> - +
- - Sign In - -
- + ( + + )} + /> +
- -

Factor two child

-
+ + Verify + + +
+
diff --git a/packages/elements/examples/nextjs/components/debug.tsx b/packages/elements/examples/nextjs/components/debug.tsx index 035a628f37..8271ac65b8 100644 --- a/packages/elements/examples/nextjs/components/debug.tsx +++ b/packages/elements/examples/nextjs/components/debug.tsx @@ -33,16 +33,16 @@ function LogButtons() { Log Fields - + + + ); } export function Debug() { return ( -
+
diff --git a/packages/elements/examples/nextjs/components/design.tsx b/packages/elements/examples/nextjs/components/design.tsx new file mode 100644 index 0000000000..0d87c6df5c --- /dev/null +++ b/packages/elements/examples/nextjs/components/design.tsx @@ -0,0 +1,36 @@ +import type { ComponentPropsWithoutRef } from 'react'; + +export const H1 = (props: ComponentPropsWithoutRef<'h1'>) => ( +

+); + +export const H2 = (props: ComponentPropsWithoutRef<'h2'>) => ( +

+); + +export const H3 = (props: ComponentPropsWithoutRef<'h3'>) => ( +

+); + +export const P = (props: ComponentPropsWithoutRef<'p'>) => ( +

+); + +export const HR = (props: ComponentPropsWithoutRef<'hr'>) => ( +


+); diff --git a/packages/elements/package.json b/packages/elements/package.json index afe1a3c093..ae9600640f 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -54,7 +54,7 @@ "lint": "eslint src/", "lint:attw": "attw --pack .", "lint:publint": "publint", - "test": "jest --passWithNoTests", + "test": "jest", "test:cache:clear": "jest --clearCache --useStderr" }, "dependencies": { diff --git a/packages/elements/src/common/strategy.tsx b/packages/elements/src/common/strategy.tsx new file mode 100644 index 0000000000..bd94c8caff --- /dev/null +++ b/packages/elements/src/common/strategy.tsx @@ -0,0 +1,25 @@ +import type { FormProps } from '@radix-ui/react-form'; +import type { PropsWithChildren } from 'react'; + +import { useSignInFlowSelector } from '../internals/machines/sign-in.context'; +import { Form } from './form'; + +// ================= STRATEGIES ================= // + +type StrategiesProps = FormProps; +const Strategies = Form; + +// ================= STRATEGY ================= // + +type StrategyProps = PropsWithChildren<{ name: string }>; + +function Strategy({ children, name }: StrategyProps) { + // TODO: Make generic + const active = useSignInFlowSelector(state => state.context.currentFactor?.strategy === name); + return active ? children : null; +} + +// ================= EXPORTS ================= // + +export { Strategies, Strategy }; +export type { StrategiesProps, StrategyProps }; diff --git a/packages/elements/src/index.tsx b/packages/elements/src/index.tsx index 2f74d935eb..7ca8640075 100644 --- a/packages/elements/src/index.tsx +++ b/packages/elements/src/index.tsx @@ -6,7 +6,15 @@ export { Errors, Field, Form, Input, Label, Submit } from './common/form'; export { SocialProviders } from './common/social-providers'; /** Sign In Components */ -export { SignIn, SignInStart, SignInFactorOne, SignInFactorTwo, SignInSSOCallback } from './sign-in'; +export { + SignIn, + SignInStart, + SignInFactorOne, + SignInFactorTwo, + SignInSSOCallback, + SignInStrategies, + SignInStrategy, +} from './sign-in'; /** Hooks */ export { useSignInFlow, useSignInFlowSelector } from './internals/machines/sign-in.context'; diff --git a/packages/elements/src/internals/machines/sign-in.actors.ts b/packages/elements/src/internals/machines/sign-in.actors.ts index d0f65867fd..b35957209b 100644 --- a/packages/elements/src/internals/machines/sign-in.actors.ts +++ b/packages/elements/src/internals/machines/sign-in.actors.ts @@ -5,10 +5,8 @@ import type { EnvironmentResource, HandleOAuthCallbackParams, HandleSamlCallbackParams, - PreferredSignInStrategy, PrepareFirstFactorParams, PrepareSecondFactorParams, - SignInFactor, SignInFirstFactor, SignInResource, } from '@clerk/types'; @@ -18,7 +16,6 @@ import { ClerkElementsRuntimeError } from '../errors/error'; import type { ClerkRouter } from '../router'; import type { SignInMachineContext } from './sign-in.machine'; import type { WithClerk, WithClient, WithParams } from './sign-in.types'; -import { determineStartingSignInFactor } from './sign-in.utils'; import { assertIsDefined } from './utils/assert'; // ================= createSignIn ================= // @@ -62,24 +59,6 @@ export const authenticateWithRedirect = fromPromise( - async ({ input: { supportedFactors, identifier, preferredStrategy = 'password' } }) => { - try { - return Promise.resolve(determineStartingSignInFactor(supportedFactors, identifier, preferredStrategy)); - } catch (e) { - return Promise.reject(e); - } - }, -); - // ================= prepareFirstFactor ================= // export type PrepareFirstFactorInput = WithClient>; @@ -153,8 +132,8 @@ export const handleSSOCallback = fromPromise(as return input.clerk.handleRedirectCallback( { afterSignInUrl: input.clerk.buildAfterSignInUrl(), - firstFactorUrl: '../factor-one', - secondFactorUrl: '../factor-two', + firstFactorUrl: '../', + secondFactorUrl: '../', ...input.params, }, // @ts-expect-error - Align on return typing. `void` vs `Promise` diff --git a/packages/elements/src/internals/machines/sign-in.context.ts b/packages/elements/src/internals/machines/sign-in.context.ts index 5c93692606..c9db1b4d18 100644 --- a/packages/elements/src/internals/machines/sign-in.context.ts +++ b/packages/elements/src/internals/machines/sign-in.context.ts @@ -1,7 +1,7 @@ -import type { OAuthStrategy, Web3Strategy } from '@clerk/types'; +import type { OAuthStrategy, SignInStrategy, Web3Strategy } from '@clerk/types'; import { createActorContext } from '@xstate/react'; import type React from 'react'; -import { useCallback, useEffect, useMemo } from 'react'; +import { createContext, useCallback, useContext, useEffect, useMemo } from 'react'; import type { SnapshotFrom } from 'xstate'; import { @@ -9,7 +9,10 @@ import { isAuthenticatableOauthStrategy, isWeb3Strategy, } from '../../utils/third-party-strategies'; +import { ClerkElementsRuntimeError } from '../errors/error'; import { SignInMachine } from './sign-in.machine'; +import type { SignInStrategyName } from './sign-in.types'; +import { matchStrategy } from './utils/strategies'; export type SnapshotState = SnapshotFrom; @@ -21,6 +24,20 @@ export const { 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 ================= // /** @@ -28,8 +45,31 @@ export const { */ const clerkEnvironmentSelector = (state: SnapshotState) => state.context.environment; +/** + * Selects the clerk environment + */ +const clerkCurrentStrategy = (state: SnapshotState) => state.context.currentFactor?.strategy; + // ================= 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 useSignInState = () => { return useSignInFlowSelector( state => state, @@ -37,6 +77,21 @@ export const useSignInState = () => { ); }; +export function useSignInStrategies(_preferred?: SignInStrategy) { + const state = useSignInState(); + 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 */ diff --git a/packages/elements/src/internals/machines/sign-in.machine.ts b/packages/elements/src/internals/machines/sign-in.machine.ts index 04e587cf4b..9e3026ef1c 100644 --- a/packages/elements/src/internals/machines/sign-in.machine.ts +++ b/packages/elements/src/internals/machines/sign-in.machine.ts @@ -10,7 +10,7 @@ import type { Web3Strategy, } from '@clerk/types'; import type { ActorRefFrom, ErrorActorEvent, MachineContext } from 'xstate'; -import { and, assertEvent, assign, log, not, sendTo, setup } from 'xstate'; +import { and, assertEvent, assign, log, not, or, sendTo, setup } from 'xstate'; import type { ClerkRouter } from '../router'; import type { FormMachine } from './form.machine'; @@ -19,11 +19,11 @@ import { attemptFirstFactor, authenticateWithRedirect, createSignIn, - determineStartingFirstFactor, handleSSOCallback, prepareFirstFactor, } from './sign-in.actors'; import type { LoadedClerkWithEnv } from './sign-in.types'; +import { determineStartingSignInFactor } from './sign-in.utils'; import { assertActorEventError } from './utils/assert'; export interface SignInMachineContext extends MachineContext { @@ -51,7 +51,8 @@ export type SignInMachineEvents = | { type: 'NEXT' } | { type: 'OAUTH.CALLBACK' } | { type: 'RETRY' } - | { type: 'SUBMIT' }; + | { type: 'SUBMIT' } + | { type: 'NAVIGATE'; path: string }; export type SignInTags = 'start' | 'first-factor' | 'second-factor' | 'complete'; export interface SignInMachineTypes { @@ -71,7 +72,6 @@ export const SignInMachine = setup({ createSignIn, // First Factor - determineStartingFirstFactor, attemptFirstFactor, prepareFirstFactor, @@ -154,12 +154,12 @@ export const SignInMachine = setup({ Init: { always: [ { - description: 'If the SignIn resource is empty, invoke the sign-in start flow.', + description: 'Determine if running on the server', guard: 'isServer', target: 'Server', }, { - description: 'Wait for the Clerk instance to be ready.', + description: 'Determine if running on the browser', guard: 'isBrowser', target: 'Browser', }, @@ -167,33 +167,30 @@ export const SignInMachine = setup({ }, Server: { type: 'final', - description: 'Determines the state of the sign-in flow on the server. This is a no-op for now.', // TODO: Implement - entry: ['debug', log('Server no-op')], + description: 'Determines the state of the sign-in flow on the server [NOOP]', + entry: 'debug', }, Browser: { type: 'final', - description: 'Determines the state of the sign-in flow on the browser.', + description: 'Determines the state of the sign-in flow on the browser', always: [ { - description: 'Wait for the Clerk instance to be ready.', + description: 'Wait for the Clerk instance to be ready', guard: not('isClerkLoaded'), target: '#SignIn.WaitingForClerk', }, { - description: 'If loggedin and single-session, invoke the sign-in start flow with error.', - guard: and(['isLoggedIn', 'isSingleSessionMode']), + description: 'If sign-in complete or loggedin and single-session, skip to complete', + guard: or(['isSignInComplete', and(['isLoggedIn', 'isSingleSessionMode'])]), target: '#SignIn.Complete', }, { - description: 'If the SignIn resource is empty, invoke the sign-in start flow.', + description: 'If the SignIn resource is empty, invoke the sign-in start flow', guard: not('hasSignInResource'), target: '#SignIn.Start', }, { - guard: 'isSignInComplete', - target: '#SignIn.Complete', - }, - { + description: 'Default to the initial sign-in flow state', target: '#SignIn.Start', }, ], @@ -217,7 +214,6 @@ export const SignInMachine = setup({ }, }, }, - Start: { description: 'The intial state of the sign-in flow.', initial: 'AwaitingInput', @@ -269,7 +265,7 @@ export const SignInMachine = setup({ }, { guard: 'needsSecondFactor', - target: '#SignIn.FirstFactor', + target: '#SignIn.SecondFactor', }, { target: '#SignIn.DeterminingState', @@ -280,51 +276,41 @@ export const SignInMachine = setup({ Failure: { type: 'final', target: '#SignIn.DeterminingState', - reenter: true, }, }, }, FirstFactor: { initial: 'DeterminingState', + entry: assign({ + currentFactor: ({ context }) => + determineStartingSignInFactor( + context.clerk.client.signIn.supportedFirstFactors, + context.clerk.client.signIn.identifier, + context.environment?.displayConfig.preferredSignInStrategy, + ), + }), states: { DeterminingState: { always: [ { + description: 'If the current factor is not set, determine the starting factor details', guard: not('hasCurrentFactor'), target: 'DetermineStartingFactor', reenter: true, }, { + description: 'If the current factor is not password, prepare the factor', guard: not('isCurrentFactorPassword'), target: 'Preparing', reenter: true, }, { + description: 'Else, skip to awaiting input', target: 'AwaitingInput', reenter: true, }, ], }, - DetermineStartingFactor: { - invoke: { - id: 'determineStartingFirstFactor', - src: 'determineStartingFirstFactor', - input: ({ context }) => ({ - supportedFactors: context.clerk.client.signIn.supportedFirstFactors, - identifier: context.clerk.client.signIn.identifier, - preferredStrategy: context.environment?.displayConfig.preferredSignInStrategy, - }), - onDone: { - actions: assign({ currentFactor: ({ event }) => event.output }), - target: 'DeterminingState', - reenter: true, - }, - onError: { - actions: 'setFormErrors', - target: 'Failure', - }, - }, - }, Preparing: { invoke: { id: 'prepareFirstFactor', @@ -375,12 +361,13 @@ export const SignInMachine = setup({ }, }), onDone: { - actions: assign({ - resource: ({ event }) => event.output, - }), + actions: [ + assign({ + resource: ({ event }) => event.output, + }), + ], target: 'Success', }, - onError: { actions: 'setFormErrors', target: 'AwaitingInput', @@ -388,14 +375,19 @@ export const SignInMachine = setup({ }, }, Success: { + type: 'final', always: [ { guard: 'isSignInComplete', - actions: 'setAsActive', + target: '#SignIn.Complete', + }, + { + guard: 'needsFirstFactor', + target: '#SignIn.FirstFactor', }, { guard: 'needsSecondFactor', - target: '#SignIn.SecondFactor', + target: '#SignIn.FirstFactor', }, { target: '#SignIn.DeterminingState', @@ -460,6 +452,12 @@ export const SignInMachine = setup({ }, }, }, + HavingTrouble: { + type: 'final', + on: { + // RESTART: '#SignIn.Start', + }, + }, Complete: { type: 'final', entry: 'setAsActive', diff --git a/packages/elements/src/internals/machines/sign-in.types.ts b/packages/elements/src/internals/machines/sign-in.types.ts index 3135c5c07e..013d13d710 100644 --- a/packages/elements/src/internals/machines/sign-in.types.ts +++ b/packages/elements/src/internals/machines/sign-in.types.ts @@ -9,6 +9,7 @@ import type { ResetPasswordEmailCodeStrategy, ResetPasswordPhoneCodeStrategy, SamlStrategy, + SignInStrategy, TicketStrategy, Web3Strategy, } from '@clerk/types'; @@ -17,6 +18,8 @@ export type WithClerk = { clerk: LoadedClerkWithEnv } & T; export type WithClient = { client: LoadedClerkWithEnv['client'] } & T; export type WithParams = { params: T }; +export type SignInStrategyName = SignInStrategy | 'oauth' | 'web3'; + // ====================== CLERKJS MODIFICATIONS ====================== /** diff --git a/packages/elements/src/internals/machines/sign-in.utils.ts b/packages/elements/src/internals/machines/sign-in.utils.ts index c4b6c9a8df..52806c2076 100644 --- a/packages/elements/src/internals/machines/sign-in.utils.ts +++ b/packages/elements/src/internals/machines/sign-in.utils.ts @@ -15,7 +15,7 @@ const findFactorForIdentifier = (i: string | null) => (f: SignInFactor) => { export function determineStartingSignInFactor( firstFactors: SignInFactor[], identifier: string | null, - preferredSignInStrategy: PreferredSignInStrategy, + preferredSignInStrategy?: PreferredSignInStrategy, ): SignInFactor | null { if (!firstFactors || firstFactors.length === 0) { return null; diff --git a/packages/elements/src/internals/machines/utils/assert.test.ts b/packages/elements/src/internals/machines/utils/assert.test.ts new file mode 100644 index 0000000000..fd85f00ee9 --- /dev/null +++ b/packages/elements/src/internals/machines/utils/assert.test.ts @@ -0,0 +1,42 @@ +import { assertActorEventDone, assertActorEventError, assertIsDefined } from './assert'; + +describe('assertIsDefined', () => { + it('should throw an error if the value is undefined', () => { + const value = undefined; + expect(() => assertIsDefined(value)).toThrowError('undefined is not defined'); + }); + + it('should throw an error if the value is null', () => { + const value = null; + expect(() => assertIsDefined(value)).toThrowError('null is not defined'); + }); + + it('should not throw an error if the value is defined', () => { + const value = 'Hello'; + expect(() => assertIsDefined(value)).not.toThrowError(); + }); +}); + +describe('assertActorEventError', () => { + it('should throw an error if the event is not an error event', () => { + const event = { type: 'success' }; + expect(() => assertActorEventError(event)).toThrowError('Expected an error event, got "success"'); + }); + + it('should not throw an error if the event is an error event', () => { + const event = { type: 'error', error: new Error('Something went wrong') }; + expect(() => assertActorEventError(event)).not.toThrowError(); + }); +}); + +describe('assertActorEventDone', () => { + it('should throw an error if the event is not a done event', () => { + const event = { type: 'success' }; + expect(() => assertActorEventDone(event)).toThrowError('Expected a done event, got "success"'); + }); + + it('should not throw an error if the event is a done event', () => { + const event = { type: 'done', output: 'Result' }; + expect(() => assertActorEventDone(event)).not.toThrowError(); + }); +}); diff --git a/packages/elements/src/internals/machines/utils/assert.ts b/packages/elements/src/internals/machines/utils/assert.ts index c8de9dbaa0..0ae136a269 100644 --- a/packages/elements/src/internals/machines/utils/assert.ts +++ b/packages/elements/src/internals/machines/utils/assert.ts @@ -14,6 +14,6 @@ export function assertActorEventDone(event: EventObject): asserts event is Do export function assertActorEventError(event: EventObject): asserts event is ErrorActorEvent { if ('error' in event === false) { - throw new Error(`Expected a error event, got "${event.type}"`); + throw new Error(`Expected an error event, got "${event.type}"`); } } diff --git a/packages/elements/src/internals/machines/utils/strategies.test.ts b/packages/elements/src/internals/machines/utils/strategies.test.ts new file mode 100644 index 0000000000..23581586c1 --- /dev/null +++ b/packages/elements/src/internals/machines/utils/strategies.test.ts @@ -0,0 +1,24 @@ +import { matchStrategy } from './strategies'; + +describe('matchStrategy', () => { + it('should return false if either current or desired is undefined', () => { + expect(matchStrategy(undefined, 'oauth')).toBe(false); + expect(matchStrategy('password', undefined)).toBe(false); + expect(matchStrategy(undefined, undefined)).toBe(false); + }); + + it('should return true if current is equal to desired', () => { + expect(matchStrategy('password', 'password')).toBe(true); + }); + + it('should return true if current partially matches desired', () => { + expect(matchStrategy('oauth_google', 'oauth')).toBe(true); + expect(matchStrategy('web3_metamask_signature', 'web3')).toBe(true); + expect(matchStrategy('web3_metamask_signature', 'web3_metamask')).toBe(true); + }); + + it('should return false on invalid partial matches', () => { + expect(matchStrategy('oauth_google', 'web3')).toBe(false); + expect(matchStrategy('oauth_google', 'oauth_goog')).toBe(false); + }); +}); diff --git a/packages/elements/src/internals/machines/utils/strategies.ts b/packages/elements/src/internals/machines/utils/strategies.ts new file mode 100644 index 0000000000..5ea580e25c --- /dev/null +++ b/packages/elements/src/internals/machines/utils/strategies.ts @@ -0,0 +1,11 @@ +export const matchStrategy = (current: string | undefined, desired: string | undefined): boolean => { + if (!current || !desired) { + return false; + } + + if (current === desired) { + return true; + } + + return current.startsWith(`${desired}_`); +}; diff --git a/packages/elements/src/sign-in/index.tsx b/packages/elements/src/sign-in/index.tsx index dac6d3f3bf..5b162d31f7 100644 --- a/packages/elements/src/sign-in/index.tsx +++ b/packages/elements/src/sign-in/index.tsx @@ -1,21 +1,26 @@ 'use client'; import { useClerk } from '@clerk/clerk-react'; +import type { SignInStrategy as ClerkSignInStrategy } from '@clerk/types'; import type { createBrowserInspector } from '@statelyai/inspect'; +import type { PropsWithChildren } from 'react'; import { Form } from '../common/form'; import { FormStoreProvider, useFormStore } from '../internals/machines/form.context'; import { SignInFlowProvider as SignInFlowContextProvider, + StrategiesContext, useSignInFlow, useSignInState, + useSignInStrategies, useSSOCallbackHandler, + useStrategy, } from '../internals/machines/sign-in.context'; -import type { LoadedClerkWithEnv } from '../internals/machines/sign-in.types'; +import type { LoadedClerkWithEnv, SignInStrategyName } from '../internals/machines/sign-in.types'; import { useNextRouter } from '../internals/router'; import { Route, Router, useClerkRouter } from '../internals/router-react'; -type WithChildren = T & { children?: React.ReactNode }; +// ================= XState Inspector ================= // const DEBUG = process.env.NEXT_PUBLIC_CLERK_ELEMENTS_DEBUG === 'true'; let inspector: ReturnType; @@ -31,7 +36,10 @@ if (DEBUG && typeof window !== 'undefined') { .catch(console.error); } -function SignInFlowProvider({ children }: WithChildren) { +// ================= SignInFlowProvider ================= // + +function SignInFlowProvider({ children }: PropsWithChildren) { + // TODO: Do something about `__unstable__environment` typing const clerk = useClerk() as unknown as LoadedClerkWithEnv; const router = useClerkRouter(); const form = useFormStore(); @@ -60,9 +68,10 @@ function SignInFlowProvider({ children }: WithChildren) { ); } -export function SignIn({ children }: WithChildren): JSX.Element | null { +// ================= SignIn ================= // + +export function SignIn({ children }: PropsWithChildren): JSX.Element | null { // TODO: eventually we'll rely on the framework SDK to specify its host router, but for now we'll default to Next.js - // TODO: Do something about `__unstable__environment` typing const router = useNextRouter(); return ( @@ -77,33 +86,66 @@ export function SignIn({ children }: WithChildren): JSX.Element | null { ); } -export function SignInStart({ children }: WithChildren) { +// ================= SignInStart ================= // + +export function SignInStart({ children }: PropsWithChildren) { const state = useSignInState(); const actorRef = useSignInFlow(); return state.matches('Start') ?
{children}
: null; } -export function SignInFactorOne({ children }: WithChildren) { +// ================= SignInFactorOne ================= // + +export function SignInFactorOne({ children }: PropsWithChildren) { const state = useSignInState(); const actorRef = useSignInFlow(); return state.matches('FirstFactor') ?
{children}
: null; } -export function SignInFactorTwo({ children }: WithChildren) { +// ================= SignInFactorTwo ================= // + +export function SignInFactorTwo({ children }: PropsWithChildren) { const state = useSignInState(); const actorRef = useSignInFlow(); return state.matches('SecondFactor') ?
{children}
: null; } +// ================= SignInStrategies ================= // + +export type SignInStrategiesProps = PropsWithChildren<{ preferred?: ClerkSignInStrategy }>; + +export function SignInStrategies({ children, preferred }: SignInStrategiesProps) { + const { current, isActive, shouldRender } = useSignInStrategies(preferred); + const actorRef = useSignInFlow(); + + return shouldRender ? ( + +
{children}
+
+ ) : null; +} + +// ================= SignInStrategy ================= // + +export type SignInStrategyProps = PropsWithChildren<{ name: SignInStrategyName }>; + +export function SignInStrategy({ children, name }: SignInStrategyProps) { + const { shouldRender } = useStrategy(name); + return shouldRender ? children : null; +} + +// ================= SignInSSOCallback ================= // + +// TODO: Remove and rely on SignInMachine export function SignInSSOCallbackInner() { useSSOCallbackHandler(); return null; } -export function SignInSSOCallback({ children }: WithChildren) { +export function SignInSSOCallback({ children }: PropsWithChildren) { return ( diff --git a/packages/elements/turbo.json b/packages/elements/turbo.json index 693f65d538..4970fe756b 100644 --- a/packages/elements/turbo.json +++ b/packages/elements/turbo.json @@ -6,6 +6,27 @@ "inputs": ["src/**", "!examples/**"], "cache": false, "persistent": true + }, + "test": { + "dependsOn": ["^build"], + "inputs": [ + "*.d.ts", + "bundlewatch.config.json", + "jest.*", + "src/**", + "tests/**", + "tsconfig.json", + "tsconfig.*.json", + "tsup.config.ts", + "!**/__snapshots__/**", + "!CHANGELOG.md", + "!coverage/**", + "!dist/**", + "!examples/**", + "!node_modules/**" + ], + "outputMode": "new-only", + "outputs": [] } } }