From 195fd46bd5cff3b54161ede02848d1c08e695116 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 23 Jul 2024 18:15:44 -0400 Subject: [PATCH] feat(elements): Add support for signOutOfOtherSessions checkbox (#3791) --- .changeset/tasty-brooms-check.md | 5 +++++ .../internals/machines/form/form.machine.ts | 6 +++-- .../src/internals/machines/form/form.types.ts | 2 ++ .../sign-in/reset-password.machine.ts | 3 ++- .../utils/__tests__/fields-to-params.test.ts | 18 +++++++-------- .../elements/src/react/common/form/index.tsx | 22 ++++++++++++++----- 6 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 .changeset/tasty-brooms-check.md diff --git a/.changeset/tasty-brooms-check.md b/.changeset/tasty-brooms-check.md new file mode 100644 index 0000000000..712e58ad28 --- /dev/null +++ b/.changeset/tasty-brooms-check.md @@ -0,0 +1,5 @@ +--- +"@clerk/elements": patch +--- + +Add support for checkbox input usage and `signOutOfOtherSessions` functionality diff --git a/packages/elements/src/internals/machines/form/form.machine.ts b/packages/elements/src/internals/machines/form/form.machine.ts index aaa875b771..392dc2bf73 100644 --- a/packages/elements/src/internals/machines/form/form.machine.ts +++ b/packages/elements/src/internals/machines/form/form.machine.ts @@ -19,7 +19,7 @@ export interface FormMachineContext extends MachineContext { } export type FormMachineEvents = - | { type: 'FIELD.ADD'; field: Pick } + | { type: 'FIELD.ADD'; field: Pick } | { type: 'FIELD.REMOVE'; field: Pick } | { type: 'MARK_AS_PROGRESSIVE'; @@ -35,7 +35,7 @@ export type FormMachineEvents = | { type: 'UNMARK_AS_PROGRESSIVE' } | { type: 'FIELD.UPDATE'; - field: Pick; + field: Pick; } | { type: 'ERRORS.SET'; error: any } | { type: 'ERRORS.CLEAR' } @@ -160,6 +160,8 @@ export const FormMachine = setup({ 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; } return context.fields; diff --git a/packages/elements/src/internals/machines/form/form.types.ts b/packages/elements/src/internals/machines/form/form.types.ts index 7485896abf..7c7a8b0044 100644 --- a/packages/elements/src/internals/machines/form/form.types.ts +++ b/packages/elements/src/internals/machines/form/form.types.ts @@ -20,7 +20,9 @@ export interface FeedbackOtherType extends FeedbackBase { export type FieldDetails = { name?: string; + type: React.HTMLInputTypeAttribute; value?: string | readonly string[] | number; + checked?: boolean; feedback?: FeedbackErrorType | FeedbackOtherType; }; diff --git a/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts b/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts index 3c3e619ec9..7da26caae5 100644 --- a/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts +++ b/packages/elements/src/internals/machines/sign-in/reset-password.machine.ts @@ -18,7 +18,8 @@ export const SignInResetPasswordMachine = setup({ attempt: fromPromise( ({ input: { fields, parent } }) => { const password = (fields.get('password')?.value as string) || ''; - return parent.getSnapshot().context.clerk.client.signIn.resetPassword({ password }); + const signOutOfOtherSessions = fields.get('signOutOfOtherSessions')?.checked || false; + return parent.getSnapshot().context.clerk.client.signIn.resetPassword({ password, signOutOfOtherSessions }); }, ), }, diff --git a/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts b/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts index 0d5575fbe0..ffe2924253 100644 --- a/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts +++ b/packages/elements/src/internals/machines/sign-up/utils/__tests__/fields-to-params.test.ts @@ -3,9 +3,9 @@ import { fieldsToSignUpParams } from '../fields-to-params'; describe('fieldsToSignUpParams', () => { it('converts form fields to sign up params', () => { const fields = new Map([ - ['firstName', { value: 'John' }], - ['emailAddress', { value: 'john@example.com' }], - ['password', { value: 'password123' }], + ['firstName', { type: 'text', value: 'John' }], + ['emailAddress', { type: 'text', value: 'john@example.com' }], + ['password', { type: 'text', value: 'password123' }], ]); const params = fieldsToSignUpParams(fields); @@ -19,9 +19,9 @@ describe('fieldsToSignUpParams', () => { it('ignores undefined values', () => { const fields = new Map([ - ['firstName', { value: 'John' }], - ['emailAddress', { value: undefined }], - ['password', { value: 'password123' }], + ['firstName', { type: 'text', value: 'John' }], + ['emailAddress', { type: 'text', value: undefined }], + ['password', { type: 'text', value: 'password123' }], ]); const params = fieldsToSignUpParams(fields); @@ -34,9 +34,9 @@ describe('fieldsToSignUpParams', () => { it('ignores non-sign-up keys', () => { const fields = new Map([ - ['firstName', { value: 'John' }], - ['foo', { value: 'bar' }], - ['bar', { value: 'foo' }], + ['firstName', { type: 'text', value: 'John' }], + ['foo', { type: 'text', value: 'bar' }], + ['bar', { type: 'text', value: 'foo' }], ]); const params = fieldsToSignUpParams(fields); diff --git a/packages/elements/src/react/common/form/index.tsx b/packages/elements/src/react/common/form/index.tsx index 5cbfa021b0..677e4738d2 100644 --- a/packages/elements/src/react/common/form/index.tsx +++ b/packages/elements/src/react/common/form/index.tsx @@ -204,6 +204,7 @@ const useField = ({ name }: Partial>) => { const useInput = ({ name: inputName, value: providedValue, + checked: providedChecked, type: inputType, onChange: onChangeProp, onBlur: onBlurProp, @@ -264,19 +265,27 @@ const useInput = ({ const prevValue = usePrevious(value); const hasValue = Boolean(value); const type = inputType ?? determineInputTypeFromName(rawName); + let nativeFieldType = type; let shouldValidatePassword = false; if (type === 'password' || type === 'text') { shouldValidatePassword = Boolean((passthroughProps as PasswordInputProps).validatePassword); } + if (nativeFieldType === 'otp' || nativeFieldType === 'backup_code') { + nativeFieldType = 'text'; + } + // Register the field in the machine context React.useEffect(() => { if (!name) { return; } - ref.send({ type: 'FIELD.ADD', field: { name, value: providedValue } }); + ref.send({ + type: 'FIELD.ADD', + field: { name, type: nativeFieldType, value: providedValue, checked: providedChecked }, + }); return () => ref.send({ type: 'FIELD.REMOVE', field: { name } }); }, [ref]); // eslint-disable-line react-hooks/exhaustive-deps @@ -288,7 +297,7 @@ const useInput = ({ if (!name) { return; } - ref.send({ type: 'FIELD.UPDATE', field: { name, value: event.target.value } }); + ref.send({ type: 'FIELD.UPDATE', field: { name, value: event.target.value, checked: event.target.checked } }); if (shouldValidatePassword) { validatePassword(event.target.value); } @@ -321,10 +330,13 @@ const useInput = ({ return; } - if (providedValue !== undefined) { - ref.send({ type: 'FIELD.UPDATE', field: { name, value: providedValue } }); + if ( + (type === 'checkbox' && providedChecked !== undefined) || + (type !== 'checkbox' && providedValue !== undefined) + ) { + ref.send({ type: 'FIELD.UPDATE', field: { name, value: providedValue, checked: providedChecked } }); } - }, [name, ref, providedValue]); + }, [name, type, ref, providedValue, providedChecked]); // TODO: Implement clerk-js utils const shouldBeHidden = false;