Skip to content

Commit

Permalink
feat(elements): Handle sign in start (#2443)
Browse files Browse the repository at this point in the history
* feat(elements): Implement initial createSignIn handling

* feat(elements): Handle complete and needs_first_factor from sign in create

* chore(elements): Rename back to Start

* chore(repo): Add changeset, fix dependencies

* feat(elements): Move param construction into actor
  • Loading branch information
BRKalow authored Dec 22, 2023
1 parent 4ffa9a4 commit 7ecceb5
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .changeset/cool-cars-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions packages/elements/examples/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<ClerkProvider
clerkJSVariant='headless'
publishableKey={process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || ''}
>
<ClerkProvider clerkJSVariant='headless'>
<body className={`${inter.variable} font-sans`}>{children}</body>
</ClerkProvider>
</html>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export default function SignInPage() {
type='identifier'
className='bg-tertiary rounded-sm px-2 py-1 border border-foreground'
/>

{/* <input type="text" name="identifier" placeholder="identifier" /> */}
{/* <button type="submit">Continue</button> */}
</Field>

<Field
Expand Down
1 change: 0 additions & 1 deletion packages/elements/examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"dependencies": {
"@clerk/elements": "*",
"@clerk/nextjs": "file:../../nextjs",
"@clerk/shared": "file:../../shared",
"@radix-ui/react-form": "^0.0.3",
"clsx": "^2.0.0",
"next": "14.0.4",
Expand Down
4 changes: 3 additions & 1 deletion packages/elements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
"test:cache:clear": "jest --clearCache --useStderr"
},
"dependencies": {
"@clerk/nextjs": "^5.0.0-alpha-v5.12",
"@clerk/clerk-react": "5.0.0-alpha-v5.12",
"@clerk/nextjs": "5.0.0-alpha-v5.13",
"@clerk/shared": "2.0.0-alpha-v5.7",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@xstate/react": "^4.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/elements/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';
import { useNextRouter } from './internals/router';

export * from './sign-in';
export * from './common/form';

export { useSignInFlow, useSignInFlowSelector } from './internals/machines/sign-in.context';
export { SignIn, SignInStart, SignInFactorOne, SignInFactorTwo, SignInSSOCallback } from './sign-in';

export { useNextRouter };
7 changes: 7 additions & 0 deletions packages/elements/src/internals/machines/shared.actors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Clerk } from '@clerk/types';
import { fromPromise } from 'xstate';

export const waitForClerk = fromPromise(
// @ts-expect-error -- not specified on the type
({ input: clerk }: { input: Clerk }) => new Promise(resolve => clerk.addOnLoaded(resolve)),
);
33 changes: 24 additions & 9 deletions packages/elements/src/internals/machines/sign-in.actors.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
import type {
AttemptFirstFactorParams,
AttemptSecondFactorParams,
LoadedClerk,
PrepareFirstFactorParams,
PrepareSecondFactorParams,
SignInCreateParams,
SignInResource,
} from '@clerk/types';
import { fromPromise } from 'xstate';

import type { SignInMachineContext } from './sign-in.machine';
import type { SignInResourceParams } from './sign-in.types';

export const createSignIn = fromPromise<SignInResource, SignInResourceParams<SignInCreateParams>>(
async ({ input: { client, params } }) => {
if (!client.signIn) {
throw new Error('signIn not available'); // TODO: better error
}
export const createSignIn = fromPromise<
SignInResource,
{ client: LoadedClerk['client']; fields: SignInMachineContext['fields'] }
>(({ input: { client, fields } }) => {
const password = fields.get('password');
const identifier = fields.get('identifier');

return client.signIn.create(params);
},
);
if (!identifier) {
throw new Error('Identifier field not present'); // TODO: better error
}

const passwordParams = password
? {
password: password.value,
strategy: 'password',
}
: {};

return client.signIn.create({
identifier: identifier.value as string,
...passwordParams,
});
});

export const prepareFirstFactor = fromPromise<SignInResource, SignInResourceParams<PrepareFirstFactorParams>>(
async ({ input: { client, params } }) => {
Expand Down
76 changes: 47 additions & 29 deletions packages/elements/src/internals/machines/sign-in.machine.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import type { ClerkAPIResponseError } from '@clerk/shared/error';
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { SignInResource } from '@clerk/types';
import { type ClerkAPIResponseError, isClerkAPIResponseError } from '@clerk/shared/error';
import type { LoadedClerk, SignInResource } from '@clerk/types';
import { assign, setup } from 'xstate';

import type { ClerkHostRouter } from '../router';
import { waitForClerk } from './shared.actors';
import {
attemptFirstFactor,
attemptSecondFactor,
createSignIn,
prepareFirstFactor,
prepareSecondFactor,
} from './sign-in.actors';
import type { FieldDetails, SignInClient } from './sign-in.types';
import type { FieldDetails } from './sign-in.types';

export interface SignInMachineContext {
client: SignInClient;
clerk: LoadedClerk;
router: ClerkHostRouter;
error?: Error | ClerkAPIResponseError;
resource?: SignInResource;
fields: Map<string, FieldDetails>;
}

export interface SignInMachineInput {
client: SignInClient;
clerk: LoadedClerk;
router: ClerkHostRouter;
}

Expand All @@ -44,19 +44,23 @@ export type SignInMachineEvents =

export const STATES = {
Init: 'Init',

Start: 'Start',
StartAttempting: 'StartAttempting',
StartFailure: 'StartFailure',

FirstFactor: 'FirstFactor',
FirstFactorPreparing: 'FirstFactorPreparing',
FirstFactorIdle: 'FirstFactorIdle',
FirstFactorAttempting: 'FirstFactorAttempting',
FirstFactorFailure: 'FirstFactorFailure',

SecondFactor: 'SecondFactor',
SecondFactorPreparing: 'SecondFactorPreparing',
SecondFactorIdle: 'SecondFactorIdle',
SecondFactorAttempting: 'SecondFactorAttempting',
SecondFactorFailure: 'SecondFactorFailure',

Complete: 'Complete',
} as const;

Expand All @@ -67,6 +71,7 @@ function eventHasError<T = any>(value: T): value is T & { error: Error } {

export const SignInMachine = setup({
actors: {
waitForClerk,
createSignIn,
prepareFirstFactor,
attemptFirstFactor,
Expand Down Expand Up @@ -106,7 +111,7 @@ export const SignInMachine = setup({
},
}).createMachine({
context: ({ input }) => ({
client: input.client,
clerk: input.clerk,
router: input.router,
currentFactor: null,
fields: new Map(),
Expand Down Expand Up @@ -164,33 +169,50 @@ export const SignInMachine = setup({
},
states: {
[STATES.Init]: {
always: 'Start',
invoke: {
src: 'waitForClerk',
input: ({ context }) => context.clerk,
onDone: {
target: STATES.Start,
actions: assign({
// @ts-expect-error -- this is really IsomorphicClerk up to this point
clerk: ({ context }) => context.clerk.clerkjs,
}),
},
},
},
[STATES.Start]: {
entry: ({ context }) => console.log('Start entry: ', context),
on: {
SUBMIT: STATES.StartAttempting,
SUBMIT: {
target: STATES.StartAttempting,
},
},
},
[STATES.StartAttempting]: {
entry: ({ context }) => console.log('StartAttempting entry: ', context),
invoke: {
id: 'createSignIn',
src: 'createSignIn',
input: ({ context }) => ({
client: context.client,
params: {
identifier: context.fields.get('identifier')?.value as string,
password: context.fields.get('identifier')?.value as string,
strategy: 'password',
},
client: context.clerk.client,
fields: context.fields,
}),
onDone: [{ actions: 'assignResourceToContext' }],
onDone: { actions: 'assignResourceToContext' },
onError: {
target: STATES.StartFailure,
actions: 'assignErrorMessageToContext',
},
},
always: [
{
guard: ({ context }) => context?.resource?.status === 'complete',
target: STATES.Complete,
},
{
guard: ({ context }) => context?.resource?.status === 'needs_first_factor',
target: STATES.FirstFactor,
},
],
},
[STATES.StartFailure]: {
entry: ({ context }) => console.log('StartFailure entry: ', context),
Expand All @@ -210,14 +232,6 @@ export const SignInMachine = setup({
guard: 'hasClerkAPIError',
target: STATES.Start,
},
{
actions: {
type: 'navigateTo',
params: {
path: '/sign-in/factor-one',
},
},
},
],
},
[STATES.FirstFactor]: {
Expand All @@ -229,7 +243,7 @@ export const SignInMachine = setup({
src: 'prepareFirstFactor',
// @ts-expect-error - TODO: Implement
input: ({ context }) => ({
client: context.client,
client: context.clerk.client,
params: {},
}),
onDone: {
Expand Down Expand Up @@ -264,7 +278,7 @@ export const SignInMachine = setup({
src: 'prepareFirstFactor',
// @ts-expect-error - TODO: Implement
input: ({ context }) => ({
client: context.client,
client: context.clerk.client,
params: {},
}),
onDone: {
Expand Down Expand Up @@ -310,7 +324,7 @@ export const SignInMachine = setup({
src: 'prepareSecondFactor',
// @ts-expect-error - TODO: Implement
input: ({ context }) => ({
client: context.client,
client: context.clerk.client,
params: {},
}),
onDone: {
Expand Down Expand Up @@ -338,7 +352,7 @@ export const SignInMachine = setup({
src: 'prepareFirstFactor',
// @ts-expect-error - TODO: Implement
input: ({ context }) => ({
client: context.client,
client: context.clerk.client,
params: {},
}),
onDone: {
Expand Down Expand Up @@ -370,6 +384,10 @@ export const SignInMachine = setup({
},
[STATES.Complete]: {
type: 'final',
entry: ({ context }) => {
const beforeEmit = () => context.router.push(context.clerk.buildAfterSignInUrl());
void context.clerk.setActive({ session: context.resource?.createdSessionId, beforeEmit });
},
},
},
});
6 changes: 2 additions & 4 deletions packages/elements/src/internals/machines/sign-in.types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import type { ClientResource } from '@clerk/types';
import type { LoadedClerk } from '@clerk/types';

export type FieldDetails = {
type?: string;
value?: string | readonly string[] | number;
error?: Error;
};

export type SignInClient = ClientResource;

export type SignInResourceParams<T> = {
client: SignInClient;
client: LoadedClerk['client'];
params: T;
};
28 changes: 18 additions & 10 deletions packages/elements/src/sign-in/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import { useClerk } from '@clerk/nextjs';
import { useClerk } from '@clerk/clerk-react';
import { useEffect } from 'react';

import { SignInFlowProvider, useSignInFlow } from '../internals/machines/sign-in.context';
Expand All @@ -12,20 +12,12 @@ export function SignIn({ children }: { children: React.ReactNode }): JSX.Element
const router = useNextRouter();
const clerk = useClerk();

// @ts-expect-error - clerk.clerkjs isn't typed
if (!router || !clerk?.clerkjs) {
return null;
}

// @ts-expect-error - clerk.clerkjs isn't typed
const client = clerk.clerkjs.client;

return (
<Router
router={router}
basePath='/sign-in'
>
<SignInFlowProvider options={{ input: { client, router } }}>{children}</SignInFlowProvider>
<SignInFlowProvider options={{ input: { clerk, router } }}>{children}</SignInFlowProvider>
</Router>
);
}
Expand All @@ -36,6 +28,22 @@ export function SignInStartInner({ children }: { children: React.ReactNode }) {
useEffect(() => ref.send({ type: 'START' }), [ref]);

return children;
// return (
// <form
// onSubmit={(event: any) => {
// event.preventDefault();

// const fields = {
// identifier: { value: event.target.elements.identifier?.value },
// password: { value: event.target.elements.password?.value },
// };

// ref.send({ type: 'SUBMIT', fields });
// }}
// >
// {children}
// </form>
// );
}

export function SignInStart({ children }: { children: React.ReactNode }) {
Expand Down

0 comments on commit 7ecceb5

Please sign in to comment.