Skip to content

Commit

Permalink
feat(elements): Handle ticket-based invitation sign-up workflows (#3910)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmilewski authored Aug 9, 2024
1 parent 7e0ced3 commit 755b3d0
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 36 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-mails-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/elements": minor
---

Handle ticket-based invitation sign-up workflows
128 changes: 128 additions & 0 deletions package-lock.json

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

55 changes: 48 additions & 7 deletions packages/elements/src/internals/machines/form/form.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ export interface FormMachineContext extends MachineContext {
}

export type FormMachineEvents =
| { type: 'FIELD.ADD'; field: Pick<FieldDetails, 'name' | 'type' | 'value' | 'checked'> }
| { type: 'FIELD.ADD'; field: Pick<FieldDetails, 'name' | 'type' | 'value' | 'checked' | 'disabled'> }
| { type: 'FIELD.REMOVE'; field: Pick<FieldDetails, 'name'> }
| { type: 'FIELD.ENABLE'; field: Pick<FieldDetails, 'name'> }
| { type: 'FIELD.DISABLE'; field: Pick<FieldDetails, 'name'> }
| {
type: 'MARK_AS_PROGRESSIVE';
defaultValues: FormDefaultValues;
Expand All @@ -35,7 +37,7 @@ export type FormMachineEvents =
| { type: 'UNMARK_AS_PROGRESSIVE' }
| {
type: 'FIELD.UPDATE';
field: Pick<FieldDetails, 'name' | 'value' | 'checked'>;
field: Pick<FieldDetails, 'name' | 'value' | 'checked' | 'disabled'>;
}
| { type: 'ERRORS.SET'; error: any }
| { type: 'ERRORS.CLEAR' }
Expand Down Expand Up @@ -157,11 +159,50 @@ export const FormMachine = setup({
throw new Error('Field name is required');
}

if (context.fields.has(event.field.name)) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
context.fields.get(event.field.name)!.value = event.field.value;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
context.fields.get(event.field.name)!.checked = event.field.checked;
const field = context.fields.get(event.field.name);

if (field) {
field.checked = event.field.checked;
field.disabled = event.field.disabled || false;
field.value = event.field.value;

context.fields.set(event.field.name, field);
}

return context.fields;
},
}),
},
'FIELD.DISABLE': {
actions: assign({
fields: ({ context, event }) => {
if (!event.field.name) {
throw new Error('Field name is required');
}

const field = context.fields.get(event.field.name);

if (field) {
field.disabled = true;
context.fields.set(event.field.name, field);
}

return context.fields;
},
}),
},
'FIELD.ENABLE': {
actions: assign({
fields: ({ context, event }) => {
if (!event.field.name) {
throw new Error('Field name is required');
}

const field = context.fields.get(event.field.name);

if (field) {
field.disabled = false;
context.fields.set(event.field.name, field);
}

return context.fields;
Expand Down
9 changes: 5 additions & 4 deletions packages/elements/src/internals/machines/form/form.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ interface FeedbackBase {
}

export interface FeedbackErrorType extends FeedbackBase {
type: Extract<FieldStates, 'error'>;
message: ClerkElementsFieldError;
type: Extract<FieldStates, 'error'>;
}

export interface FeedbackOtherType extends FeedbackBase {
type: Exclude<FieldStates, 'idle' | 'error'>;
message: string;
type: Exclude<FieldStates, 'idle' | 'error'>;
}

export interface FeedbackPasswordErrorType extends FeedbackErrorType {
Expand All @@ -28,11 +28,12 @@ export interface FeedbackPasswordInfoType extends FeedbackOtherType {
}

export type FieldDetails = {
checked?: boolean;
disabled?: boolean;
feedback?: FeedbackErrorType | FeedbackOtherType | FeedbackPasswordErrorType | FeedbackPasswordInfoType;
name?: string;
type: React.HTMLInputTypeAttribute;
value?: string | readonly string[] | number;
checked?: boolean;
feedback?: FeedbackErrorType | FeedbackOtherType | FeedbackPasswordErrorType | FeedbackPasswordInfoType;
};

export type FormFields = Map<string, FieldDetails>;
40 changes: 30 additions & 10 deletions packages/elements/src/internals/machines/sign-up/router.machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export const SignUpRouterMachine = setup({
},
hasClerkTransfer: ({ context }) => Boolean(context.router?.searchParams().get(SEARCH_PARAMS.transfer)),
hasResource: ({ context }) => Boolean(context.clerk.client.signUp),
hasTicket: ({ context }) => Boolean(context.ticket),

isLoggedInAndSingleSession: and(['isLoggedIn', 'isSingleSessionMode', not('isExampleMode')]),
isStatusAbandoned: needsStatus('abandoned'),
Expand Down Expand Up @@ -240,16 +241,24 @@ export const SignUpRouterMachine = setup({
Idle: {
on: {
INIT: {
actions: assign(({ event }) => ({
clerk: event.clerk,
router: event.router,
signInPath: event.signInPath || SIGN_IN_DEFAULT_BASE_PATH,
loading: {
isLoading: false,
},
exampleMode: event.exampleMode || false,
formRef: event.formRef,
})),
actions: assign(({ event }) => {
const searchParams = event.router?.searchParams();

return {
clerk: event.clerk,
router: event.router,
signInPath: event.signInPath || SIGN_IN_DEFAULT_BASE_PATH,
loading: {
isLoading: false,
},
exampleMode: event.exampleMode || false,
formRef: event.formRef,
ticket:
searchParams?.get(SEARCH_PARAMS.ticket) ||
searchParams?.get(SEARCH_PARAMS.invitationToken) ||
undefined,
};
}),
target: 'Init',
},
},
Expand Down Expand Up @@ -286,6 +295,11 @@ export const SignUpRouterMachine = setup({
guard: 'needsCallback',
target: 'Callback',
},
{
guard: 'hasTicket',
actions: { type: 'navigateInternal', params: { force: true, path: '/' } },
target: 'Start',
},
{
guard: 'needsVerification',
actions: { type: 'navigateInternal', params: { force: true, path: '/verify' } },
Expand All @@ -312,6 +326,7 @@ export const SignUpRouterMachine = setup({
basePath: context.router?.basePath,
formRef: context.formRef,
parent: self,
ticket: context.ticket,
}),
onDone: {
actions: 'raiseNext',
Expand All @@ -327,6 +342,11 @@ export const SignUpRouterMachine = setup({
guard: 'isStatusComplete',
actions: ['setActive', 'delayedReset'],
},
{
guard: and(['hasTicket', 'statusNeedsContinue']),
actions: { type: 'navigateInternal', params: { path: '/' } },
target: 'Start',
},
{
guard: 'statusNeedsVerification',
target: 'Verification',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface SignUpRouterContext extends BaseRouterContext {
formRef: ActorRefFrom<TFormMachine>;
loading: SignUpRouterLoadingContext;
signInPath: string;
ticket: string | undefined;
}

// ---------------------------------- Schema ---------------------------------- //
Expand Down
Loading

0 comments on commit 755b3d0

Please sign in to comment.