diff --git a/.changeset/curvy-ghosts-relax.md b/.changeset/curvy-ghosts-relax.md new file mode 100644 index 00000000000..378f799a0ff --- /dev/null +++ b/.changeset/curvy-ghosts-relax.md @@ -0,0 +1,7 @@ +--- +"@clerk/localizations": minor +"@clerk/clerk-js": minor +"@clerk/types": minor +--- + +Adding experimental support for legal consent for `` component diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 7e0a980381c..7f6ab8562b5 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.browser.js", "maxSize": "68kB" }, { "path": "./dist/clerk.headless.js", "maxSize": "44kB" }, - { "path": "./dist/ui-common*.js", "maxSize": "86KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "87KB" }, { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/coinbase*.js", "maxSize": "58KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 5ed5a1034b7..36ff3a1eef0 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -1400,6 +1400,7 @@ export class Clerk implements ClerkInterface { return this.client?.signUp.create({ strategy: 'google_one_tap', token: params.token, + __experimental_legalAccepted: params.__experimental_legalAccepted, }); } throw err; @@ -1420,6 +1421,7 @@ export class Clerk implements ClerkInterface { customNavigate, unsafeMetadata, strategy, + __experimental_legalAccepted, }: ClerkAuthenticateWithWeb3Params): Promise => { if (!this.client || !this.environment) { return; @@ -1442,6 +1444,7 @@ export class Clerk implements ClerkInterface { generateSignature, unsafeMetadata, strategy, + __experimental_legalAccepted, }); if ( diff --git a/packages/clerk-js/src/core/resources/DisplayConfig.ts b/packages/clerk-js/src/core/resources/DisplayConfig.ts index c5165acb447..73150b2e37e 100644 --- a/packages/clerk-js/src/core/resources/DisplayConfig.ts +++ b/packages/clerk-js/src/core/resources/DisplayConfig.ts @@ -44,6 +44,8 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource afterCreateOrganizationUrl!: string; googleOneTapClientId?: string; showDevModeWarning!: boolean; + termsUrl!: string; + privacyPolicyUrl!: string; public constructor(data: DisplayConfigJSON) { super(); @@ -87,6 +89,8 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource this.afterCreateOrganizationUrl = data.after_create_organization_url; this.googleOneTapClientId = data.google_one_tap_client_id; this.showDevModeWarning = data.show_devmode_warning; + this.termsUrl = data.terms_url; + this.privacyPolicyUrl = data.privacy_policy_url; return this; } } diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index bf7a06932ea..2dbaf053378 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -70,6 +70,7 @@ export class SignUp extends BaseResource implements SignUpResource { createdSessionId: string | null = null; createdUserId: string | null = null; abandonAt: number | null = null; + legalAcceptedAt: number | null = null; constructor(data: SignUpJSON | null = null) { super(); @@ -111,6 +112,12 @@ export class SignUp extends BaseResource implements SignUpResource { paramsWithCaptcha.strategy = SignUp.clerk.client?.signIn.firstFactorVerification.strategy; } + // TODO(@vaggelis): Remove this once the legalAccepted is stable + if (typeof params.__experimental_legalAccepted !== 'undefined') { + paramsWithCaptcha.legalAccepted = params.__experimental_legalAccepted; + paramsWithCaptcha.__experimental_legalAccepted = undefined; + } + return this._basePost({ path: this.pathRoot, body: normalizeUnsafeMetadata(paramsWithCaptcha), @@ -190,9 +197,18 @@ export class SignUp extends BaseResource implements SignUpResource { }; public authenticateWithWeb3 = async ( - params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, + params: AuthenticateWithWeb3Params & { + unsafeMetadata?: SignUpUnsafeMetadata; + __experimental_legalAccepted?: boolean; + }, ): Promise => { - const { generateSignature, identifier, unsafeMetadata, strategy = 'web3_metamask_signature' } = params || {}; + const { + generateSignature, + identifier, + unsafeMetadata, + strategy = 'web3_metamask_signature', + __experimental_legalAccepted, + } = params || {}; const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider; if (!(typeof generateSignature === 'function')) { @@ -200,7 +216,7 @@ export class SignUp extends BaseResource implements SignUpResource { } const web3Wallet = identifier || this.web3wallet!; - await this.create({ web3Wallet, unsafeMetadata }); + await this.create({ web3Wallet, unsafeMetadata, __experimental_legalAccepted: __experimental_legalAccepted }); await this.prepareWeb3WalletVerification({ strategy }); const { message } = this.verifications.web3Wallet; @@ -229,18 +245,25 @@ export class SignUp extends BaseResource implements SignUpResource { return this.attemptWeb3WalletVerification({ signature, strategy }); }; - public authenticateWithMetamask = async (params?: SignUpAuthenticateWithWeb3Params): Promise => { + public authenticateWithMetamask = async ( + params?: SignUpAuthenticateWithWeb3Params & { + __experimental_legalAccepted?: boolean; + }, + ): Promise => { const identifier = await getMetamaskIdentifier(); return this.authenticateWithWeb3({ identifier, generateSignature: generateSignatureWithMetamask, unsafeMetadata: params?.unsafeMetadata, strategy: 'web3_metamask_signature', + __experimental_legalAccepted: params?.__experimental_legalAccepted, }); }; public authenticateWithCoinbaseWallet = async ( - params?: SignUpAuthenticateWithWeb3Params, + params?: SignUpAuthenticateWithWeb3Params & { + __experimental_legalAccepted?: boolean; + }, ): Promise => { const identifier = await getCoinbaseWalletIdentifier(); return this.authenticateWithWeb3({ @@ -248,6 +271,7 @@ export class SignUp extends BaseResource implements SignUpResource { generateSignature: generateSignatureWithCoinbaseWallet, unsafeMetadata: params?.unsafeMetadata, strategy: 'web3_coinbase_wallet_signature', + __experimental_legalAccepted: params?.__experimental_legalAccepted, }); }; @@ -258,6 +282,7 @@ export class SignUp extends BaseResource implements SignUpResource { continueSignUp = false, unsafeMetadata, emailAddress, + __experimental_legalAccepted, }: AuthenticateWithRedirectParams & { unsafeMetadata?: SignUpUnsafeMetadata; }): Promise => { @@ -268,6 +293,7 @@ export class SignUp extends BaseResource implements SignUpResource { actionCompleteRedirectUrl: redirectUrlComplete, unsafeMetadata, emailAddress, + __experimental_legalAccepted, }; return continueSignUp && this.id ? this.update(params) : this.create(params); }; @@ -294,6 +320,13 @@ export class SignUp extends BaseResource implements SignUpResource { }; update = (params: SignUpUpdateParams): Promise => { + // TODO(@vaggelis): Remove this once the legalAccepted is stable + if (typeof params.__experimental_legalAccepted !== 'undefined') { + // @ts-expect-error - We need to remove the __experimental_legalAccepted key from the params + params.legalAccepted = params.__experimental_legalAccepted; + params.__experimental_legalAccepted = undefined; + } + return this._basePatch({ body: normalizeUnsafeMetadata(params), }); @@ -328,6 +361,7 @@ export class SignUp extends BaseResource implements SignUpResource { this.createdUserId = data.created_user_id; this.abandonAt = data.abandon_at; this.web3wallet = data.web3_wallet; + this.legalAcceptedAt = data.legal_accepted_at; } return this; } diff --git a/packages/clerk-js/src/core/resources/User.ts b/packages/clerk-js/src/core/resources/User.ts index 7fe40eda9c6..c46c4180c22 100644 --- a/packages/clerk-js/src/core/resources/User.ts +++ b/packages/clerk-js/src/core/resources/User.ts @@ -87,6 +87,7 @@ export class User extends BaseResource implements UserResource { createOrganizationsLimit: number | null = null; deleteSelfEnabled = false; lastSignInAt: Date | null = null; + legalAcceptedAt: Date | null = null; updatedAt: Date | null = null; createdAt: Date | null = null; @@ -360,6 +361,10 @@ export class User extends BaseResource implements UserResource { this.lastSignInAt = unixEpochToDate(data.last_sign_in_at); } + if (data.legal_accepted_at) { + this.legalAcceptedAt = unixEpochToDate(data.legal_accepted_at); + } + this.updatedAt = unixEpochToDate(data.updated_at); this.createdAt = unixEpochToDate(data.created_at); return this; diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx index 7ac0c7188e7..8cec478f8dc 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpContinue.tsx @@ -1,5 +1,5 @@ import { useClerk } from '@clerk/shared/react'; -import React from 'react'; +import React, { useMemo } from 'react'; import { useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts'; import { descriptors, Flex, Flow, localizationKeys } from '../../customizables'; @@ -38,12 +38,6 @@ function _SignUpContinue() { getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), ); - // Redirect to sign-up if there is no persisted sign-up - if (!signUp.id) { - void navigate(displayConfig.signUpUrl); - return ; - } - // TODO: This form should be shared between SignUpStart and SignUpContinue const formState = { firstName: useFormControl('firstName', initialValues.firstName || '', { @@ -77,8 +71,25 @@ function _SignUpContinue() { placeholder: localizationKeys('formFieldInputPlaceholder__password'), validatePassword: true, }), + __experimental_legalAccepted: useFormControl('__experimental_legalAccepted', '', { + type: 'checkbox', + label: '', + defaultChecked: false, + isRequired: userSettings.signUp.legal_consent_enabled || false, + }), } as const; + const onlyLegalConsentMissing = useMemo( + () => signUp.missingFields && signUp.missingFields.length === 1 && signUp.missingFields[0] === 'legal_accepted', + [signUp.missingFields], + ); + + // Redirect to sign-up if there is no persisted sign-up + if (!signUp.id) { + void navigate(displayConfig.signUpUrl); + return ; + } + const hasEmail = !!formState.emailAddress.value; const hasVerifiedExternalAccount = signUp.verifications?.externalAccount?.status == 'verified'; const hasVerifiedWeb3 = signUp.verifications?.web3Wallet?.status == 'verified'; @@ -89,6 +100,7 @@ function _SignUpContinue() { activeCommIdentifierType, signUp, isProgressiveSignUp, + legalConsentRequired: userSettings.signUp.legal_consent_enabled, }); minimizeFieldsForExistingSignup(fields, signUp); @@ -131,12 +143,13 @@ function _SignUpContinue() { !phoneNumberProvided && emailOrPhone(attributes, isProgressiveSignUp) ) { - fieldsToSubmit.push(formState['emailAddress']); - fieldsToSubmit.push(formState['phoneNumber']); + fieldsToSubmit.push(formState.emailAddress); + fieldsToSubmit.push(formState.phoneNumber); } card.setLoading(); card.setError(undefined); + return signUp .update(buildRequest(fieldsToSubmit)) .then(res => @@ -156,13 +169,21 @@ function _SignUpContinue() { const showOauthProviders = !hasVerifiedExternalAccount && oauthOptions.length > 0; const showWeb3Providers = !hasVerifiedWeb3 && web3Options.length > 0; + const headerTitle = !onlyLegalConsentMissing + ? localizationKeys('signUp.continue.title') + : localizationKeys('signUp.__experimental_legalConsent.continue.title'); + + const headerSubtitle = !onlyLegalConsentMissing + ? localizationKeys('signUp.continue.subtitle') + : localizationKeys('signUp.__experimental_legalConsent.continue.subtitle'); + return ( - - + + {card.error} - {(showOauthProviders || showWeb3Providers) && ( + {(showOauthProviders || showWeb3Providers) && !onlyLegalConsentMissing && ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx index 25de5e805c7..e23778e1843 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpForm.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Col, localizationKeys, useAppearance } from '../../customizables'; -import { Form } from '../../elements'; +import { Form, LegalCheckbox } from '../../elements'; import { CaptchaElement } from '../../elements/CaptchaElement'; import { mqu } from '../../styledSystem'; import type { FormControlState } from '../../utils'; @@ -13,10 +13,18 @@ type SignUpFormProps = { formState: Record, FormControlState>; canToggleEmailPhone: boolean; handleEmailPhoneToggle: (type: ActiveIdentifier) => void; + onlyLegalAcceptedMissing?: boolean; }; export const SignUpForm = (props: SignUpFormProps) => { - const { handleSubmit, fields, formState, canToggleEmailPhone, handleEmailPhoneToggle } = props; + const { + handleSubmit, + fields, + formState, + canToggleEmailPhone, + onlyLegalAcceptedMissing = false, + handleEmailPhoneToggle, + } = props; const { showOptionalFields } = useAppearance().parsedLayout; const shouldShow = (name: keyof typeof fields) => { @@ -34,80 +42,97 @@ export const SignUpForm = (props: SignUpFormProps) => { onSubmit={handleSubmit} gap={8} > - - {(shouldShow('firstName') || shouldShow('lastName')) && ( - - {shouldShow('firstName') && ( + {!onlyLegalAcceptedMissing && ( + + {(shouldShow('firstName') || shouldShow('lastName')) && ( + + {shouldShow('firstName') && ( + + )} + {shouldShow('lastName') && ( + + )} + + )} + {shouldShow('username') && ( + - )} - {shouldShow('lastName') && ( + + )} + {shouldShow('emailAddress') && ( + handleEmailPhoneToggle('phoneNumber') : undefined} /> - )} - - )} - {shouldShow('username') && ( - - - - )} - {shouldShow('emailAddress') && ( - - handleEmailPhoneToggle('phoneNumber') : undefined} - /> - - )} - {shouldShow('phoneNumber') && ( - - handleEmailPhoneToggle('emailAddress') : undefined} - /> - - )} - {shouldShow('password') && ( - - - - )} - + + )} + {shouldShow('phoneNumber') && ( + + handleEmailPhoneToggle('emailAddress') : undefined} + /> + + )} + {shouldShow('password') && ( + + + + )} + + )} - + + {shouldShow('__experimental_legalAccepted') && ( + + + + )} + + ); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx index 5c600cc5aff..c9483fb51a6 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpSocialButtons.tsx @@ -11,7 +11,7 @@ import { SocialButtons } from '../../elements/SocialButtons'; import { useRouter } from '../../router'; import { handleError } from '../../utils'; -export type SignUpSocialButtonsProps = SocialButtonsProps & { continueSignUp?: boolean }; +export type SignUpSocialButtonsProps = SocialButtonsProps & { continueSignUp?: boolean; legalAccepted?: boolean }; export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) => { const clerk = useClerk(); @@ -35,6 +35,7 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) redirectUrlComplete, strategy, unsafeMetadata: ctx.unsafeMetadata, + __experimental_legalAccepted: props.legalAccepted, }) .catch(err => handleError(err, [], card.setError)); }} @@ -46,6 +47,7 @@ export const SignUpSocialButtons = React.memo((props: SignUpSocialButtonsProps) signUpContinueUrl: 'continue', unsafeMetadata: ctx.unsafeMetadata, strategy, + __experimental_legalAccepted: props.legalAccepted, }) .catch(err => handleError(err, [], card.setError)); }} diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx index 6a76ca18898..1d748d56c7e 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUpStart.tsx @@ -8,7 +8,9 @@ import { useCoreSignUp, useEnvironment, useSignUpContext } from '../../contexts' import { descriptors, Flex, Flow, localizationKeys, useAppearance, useLocalizations } from '../../customizables'; import { Card, + Form, Header, + LegalCheckbox, LoadingCard, SocialButtonsReversibleContainerWithDivider, withCardStateProvider, @@ -79,6 +81,12 @@ function _SignUpStart(): JSX.Element { label: localizationKeys('formFieldLabel__phoneNumber'), placeholder: localizationKeys('formFieldInputPlaceholder__phoneNumber'), }), + __experimental_legalAccepted: useFormControl('__experimental_legalAccepted', '', { + type: 'checkbox', + label: '', + defaultChecked: false, + isRequired: userSettings.signUp.legal_consent_enabled || false, + }), password: useFormControl('password', '', { type: 'password', label: localizationKeys('formFieldLabel__password'), @@ -102,6 +110,7 @@ function _SignUpStart(): JSX.Element { hasEmail, activeCommIdentifierType, isProgressiveSignUp, + legalConsentRequired: userSettings.signUp.legal_consent_enabled, }); const handleTokenFlow = () => { @@ -187,19 +196,17 @@ function _SignUpStart(): JSX.Element { e.preventDefault(); type FormStateKey = keyof typeof formState; - const fieldsToSubmit = Object.entries(fields).reduce( - (acc, [k, v]) => [...acc, ...(v && formState[k as FormStateKey] ? [formState[k as FormStateKey]] : [])], - [] as Array, - ); + const fieldsToSubmit = Object.entries(fields).reduce((acc, [k, v]) => { + acc.push(...(v && formState[k as FormStateKey] ? [formState[k as FormStateKey]] : [])); + return acc; + }, [] as Array); if (unsafeMetadata) { fieldsToSubmit.push({ id: 'unsafeMetadata', value: unsafeMetadata } as any); } if (fields.ticket) { - const noop = () => { - // - }; + const noop = () => {}; // fieldsToSubmit: Constructing a fake fields object for strategy. fieldsToSubmit.push({ id: 'strategy', value: 'ticket', setValue: noop, onChange: noop, setError: noop } as any); } @@ -210,8 +217,8 @@ function _SignUpStart(): JSX.Element { const phoneNumberProvided = !!(fieldsToSubmit.find(f => f.id === 'phoneNumber')?.value || ''); if (!emailAddressProvided && !phoneNumberProvided && emailOrPhone(attributes, isProgressiveSignUp)) { - fieldsToSubmit.push(formState['emailAddress']); - fieldsToSubmit.push(formState['phoneNumber']); + fieldsToSubmit.push(formState.emailAddress); + fieldsToSubmit.push(formState.phoneNumber); } card.setLoading(); @@ -273,6 +280,7 @@ function _SignUpStart(): JSX.Element { enableOAuthProviders={showOauthProviders} enableWeb3Providers={showWeb3Providers} continueSignUp={missingRequirementsWithTicket} + legalAccepted={Boolean(formState.__experimental_legalAccepted.checked)} /> )} {shouldShowForm && ( @@ -285,6 +293,14 @@ function _SignUpStart(): JSX.Element { /> )} + {!shouldShowForm && ( + + + + )} {!shouldShowForm && } diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx index 75e6b33fa5a..e058ca32fdc 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpContinue.test.tsx @@ -78,6 +78,22 @@ describe('SignUpContinue', () => { expect(button.parentElement?.tagName.toUpperCase()).toBe('BUTTON'); }); + it('renders the component if there is a persisted sign up and legal accepted is missing', async () => { + const { wrapper } = await createFixtures(f => { + f.withEmailAddress({ required: true }); + f.withPassword({ required: true }); + f.startSignUpWithMissingLegalAccepted(); + f.withLegalConsent(); + f.withTermsPrivacyPolicyUrls({ + privacyPolicy: 'https://clerk.dev/privacy', + termsOfService: 'https://clerk.dev/tos', + }); + }); + const screen = render(, { wrapper }); + screen.getByText(/Terms Of Service/i); + screen.getByText(/Privacy Policy/i); + }); + it.each(OAUTH_PROVIDERS)('shows the "Continue with $name" social OAuth button', async ({ provider, name }) => { const { wrapper } = await createFixtures(f => { f.withEmailAddress({ required: true }); diff --git a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx index 4aaec2a2d85..a79adcde688 100644 --- a/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/__tests__/SignUpStart.test.tsx @@ -256,6 +256,22 @@ describe('SignUpStart', () => { }); }); + describe('Legal consent', () => { + it('shows sign up component with legal consent checkbox', async () => { + const { wrapper } = await createFixtures(f => { + f.withLegalConsent(); + f.withTermsPrivacyPolicyUrls({ + privacyPolicy: 'https://clerk.dev/privacy', + termsOfService: 'https://clerk.dev/tos', + }); + }); + + render(, { wrapper }); + screen.getByText('Terms of Service'); + screen.getByText('Privacy Policy'); + }); + }); + describe('ticket flow', () => { it('calls the appropriate resource function upon detecting the ticket', async () => { const { wrapper, fixtures } = await createFixtures(f => { diff --git a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts index 7258de0b994..5318a5890ad 100644 --- a/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts +++ b/packages/clerk-js/src/ui/components/SignUp/signUpFormHelpers.ts @@ -8,7 +8,16 @@ import type { FieldState } from '../../common'; */ export type ActiveIdentifier = 'emailAddress' | 'phoneNumber' | null | undefined; -const FieldKeys = ['emailAddress', 'phoneNumber', 'username', 'firstName', 'lastName', 'password', 'ticket']; +const FieldKeys = [ + 'emailAddress', + 'phoneNumber', + 'username', + 'firstName', + 'lastName', + 'password', + 'ticket', + '__experimental_legalAccepted', +] as const; export type FieldKey = (typeof FieldKeys)[number]; export type FormState = { @@ -34,10 +43,11 @@ type FieldDeterminationProps = { hasEmail?: boolean; signUp?: SignUpResource | undefined; isProgressiveSignUp: boolean; + legalConsentRequired?: boolean; }; export function determineActiveFields(fieldProps: FieldDeterminationProps): Fields { - return FieldKeys.reduce((fields: Fields, fieldKey: string) => { + return FieldKeys.reduce((fields: Fields, fieldKey: FieldKey) => { const field = getField(fieldKey, fieldProps); if (field) { fields[fieldKey] = field; @@ -50,10 +60,11 @@ export function determineActiveFields(fieldProps: FieldDeterminationProps): Fiel export function minimizeFieldsForExistingSignup(fields: Fields, signUp: SignUpResource) { if (signUp) { const hasEmailFilled = !!signUp.emailAddress; - const hasVerifiedEmail = signUp.verifications?.emailAddress?.status == 'verified'; - const hasVerifiedPhone = signUp.verifications?.phoneNumber?.status == 'verified'; - const hasVerifiedExternalAccount = signUp.verifications?.externalAccount?.status == 'verified'; - const hasVerifiedWeb3Wallet = signUp.verifications?.web3Wallet?.status == 'verified'; + const hasVerifiedEmail = signUp.verifications?.emailAddress?.status === 'verified'; + const hasVerifiedPhone = signUp.verifications?.phoneNumber?.status === 'verified'; + const hasVerifiedExternalAccount = signUp.verifications?.externalAccount?.status === 'verified'; + const hasVerifiedWeb3Wallet = signUp.verifications?.web3Wallet?.status === 'verified'; + const hasLegalAccepted = signUp.legalAcceptedAt !== null; if (hasEmailFilled && hasVerifiedEmail) { delete fields.emailAddress; @@ -79,10 +90,14 @@ export function minimizeFieldsForExistingSignup(fields: Fields, signUp: SignUpRe delete fields.username; } + if (hasLegalAccepted) { + delete fields.__experimental_legalAccepted; + } + // Hide any non-required fields Object.entries(fields).forEach(([k, v]) => { if (v && !v.required) { - delete fields[k]; + delete fields[k as FieldKey]; } }); } @@ -133,6 +148,8 @@ function getField(fieldKey: FieldKey, fieldProps: FieldDeterminationProps): Fiel return getPasswordField(fieldProps.attributes); case 'ticket': return getTicketField(fieldProps.hasTicket); + case '__experimental_legalAccepted': + return getLegalAcceptedField(fieldProps.legalConsentRequired); case 'username': case 'firstName': case 'lastName': @@ -174,7 +191,7 @@ function getEmailAddressField({ (!hasTicket || (hasTicket && hasEmail)) && attributes.email_address.enabled && attributes.email_address.used_for_first_factor && - activeCommIdentifierType == 'emailAddress'; + activeCommIdentifierType === 'emailAddress'; if (!show) { return; @@ -215,7 +232,7 @@ function getPhoneNumberField({ !hasTicket && attributes.phone_number.enabled && attributes.phone_number.used_for_first_factor && - activeCommIdentifierType == 'phoneNumber'; + activeCommIdentifierType === 'phoneNumber'; if (!show) { return; @@ -249,16 +266,26 @@ function getTicketField(hasTicket?: boolean): Field | undefined { }; } +function getLegalAcceptedField(legalConsentRequired?: boolean): Field | undefined { + if (!legalConsentRequired) { + return; + } + + return { + required: true, + }; +} + function getGenericField(fieldKey: FieldKey, attributes: Attributes): Field | undefined { const attrKey = camelToSnake(fieldKey); - // @ts-ignore + // @ts-expect-error - TS doesn't know that the key exists if (!attributes[attrKey].enabled) { return; } return { - // @ts-ignore + // @ts-expect-error - TS doesn't know that the key exists required: attributes[attrKey].required, }; } diff --git a/packages/clerk-js/src/ui/elements/Form.tsx b/packages/clerk-js/src/ui/elements/Form.tsx index dfa5397f462..20a62c60d8e 100644 --- a/packages/clerk-js/src/ui/elements/Form.tsx +++ b/packages/clerk-js/src/ui/elements/Form.tsx @@ -201,6 +201,8 @@ const InputGroup = ( const Checkbox = ( props: CommonFieldRootProps & { description?: string | LocalizationKey; + termsLink?: string; + privacyLink?: string; }, ) => { return ( diff --git a/packages/clerk-js/src/ui/elements/LegalConsentCheckbox.tsx b/packages/clerk-js/src/ui/elements/LegalConsentCheckbox.tsx new file mode 100644 index 00000000000..aabf7e4fb76 --- /dev/null +++ b/packages/clerk-js/src/ui/elements/LegalConsentCheckbox.tsx @@ -0,0 +1,92 @@ +import { useEnvironment } from '../../ui/contexts'; +import type { LocalizationKey } from '../customizables'; +import { + descriptors, + Flex, + FormLabel, + localizationKeys, + Text, + useAppearance, + useLocalizations, +} from '../customizables'; +import type { PropsOfComponent } from '../styledSystem'; +import { Field } from './FieldControl'; +import { LinkRenderer } from './LinkRenderer'; + +const LegalCheckboxLabel = (props: { termsUrl?: string; privacyPolicyUrl?: string }) => { + const { termsUrl, privacyPolicyUrl } = props; + const { t } = useLocalizations(); + let localizationKey: LocalizationKey | undefined; + + if (termsUrl && privacyPolicyUrl) { + localizationKey = localizationKeys( + 'signUp.__experimental_legalConsent.checkbox.label__termsOfServiceAndPrivacyPolicy', + { + termsOfServiceLink: props.termsUrl, + privacyPolicyLink: props.privacyPolicyUrl, + }, + ); + } else if (termsUrl) { + localizationKey = localizationKeys('signUp.__experimental_legalConsent.checkbox.label__onlyTermsOfService', { + termsOfServiceLink: props.termsUrl, + }); + } else if (privacyPolicyUrl) { + localizationKey = localizationKeys('signUp.__experimental_legalConsent.checkbox.label__onlyPrivacyPolicy', { + privacyPolicyLink: props.privacyPolicyUrl, + }); + } + + return ( + + ({ + textDecoration: 'underline', + textUnderlineOffset: t.space.$1, + })} + /> + + ); +}; + +type CommonFieldRootProps = Omit, 'children' | 'elementDescriptor' | 'elementId'>; + +export const LegalCheckbox = ( + props: CommonFieldRootProps & { + description?: string | LocalizationKey; + }, +) => { + const { displayConfig } = useEnvironment(); + const { parsedLayout } = useAppearance(); + + const termsLink = parsedLayout.termsPageUrl || displayConfig.termsUrl; + const privacyPolicy = parsedLayout.privacyPageUrl || displayConfig.privacyPolicyUrl; + + return ( + + + + ({ + paddingLeft: t.space.$1x5, + textAlign: 'left', + })} + > + + + + + ); +}; diff --git a/packages/clerk-js/src/ui/elements/LinkRenderer.tsx b/packages/clerk-js/src/ui/elements/LinkRenderer.tsx new file mode 100644 index 00000000000..896ba557112 --- /dev/null +++ b/packages/clerk-js/src/ui/elements/LinkRenderer.tsx @@ -0,0 +1,44 @@ +import React, { memo, useMemo } from 'react'; + +import { Link } from '../customizables'; +import type { PropsOfComponent } from '../styledSystem'; + +interface LinkRendererProps extends Omit, 'href' | 'children'> { + text: string; +} + +const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g; // parses [text](url) + +export const LinkRenderer: React.FC = memo(({ text, ...linkProps }) => { + const memoizedLinkProps = useMemo(() => linkProps, [linkProps]); + + const renderedContent = useMemo(() => { + const parts: (string | JSX.Element)[] = []; + let lastIndex = 0; + + text.replace(LINK_REGEX, (match, linkText, url, offset) => { + if (offset > lastIndex) { + parts.push(text.slice(lastIndex, offset)); + } + parts.push( + + {linkText} + , + ); + lastIndex = offset + match.length; + return match; + }); + + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)); + } + + return parts; + }, [text, memoizedLinkProps]); + + return renderedContent; +}); diff --git a/packages/clerk-js/src/ui/elements/__tests__/LinkRenderer.test.tsx b/packages/clerk-js/src/ui/elements/__tests__/LinkRenderer.test.tsx new file mode 100644 index 00000000000..3c6ebab89ec --- /dev/null +++ b/packages/clerk-js/src/ui/elements/__tests__/LinkRenderer.test.tsx @@ -0,0 +1,44 @@ +import { describe, it } from '@jest/globals'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import { bindCreateFixtures } from '../../utils/test/createFixtures'; +import { LinkRenderer } from '../LinkRenderer'; + +const { createFixtures } = bindCreateFixtures('SignUp'); + +describe('LinkRenderer', () => { + it('renders a simple link', async () => { + const { wrapper } = await createFixtures(); + + const screen = render(, { + wrapper, + }); + + expect(screen.queryByRole('link', { name: 'Terms of Service' })).toBeInTheDocument(); + }); + + it('renders multiple links', async () => { + const { wrapper } = await createFixtures(); + + const screen = render( + , + { wrapper }, + ); + + expect(screen.queryByRole('link', { name: 'Terms of Service' })).toBeInTheDocument(); + expect(screen.queryByRole('link', { name: 'Privacy Policy' })).toBeInTheDocument(); + }); + + it('does not render links with broken format', async () => { + const { wrapper } = await createFixtures(); + + const screen = render( + , + { wrapper }, + ); + + screen.findByText('[Terms of Service]https://example.com/terms)'); + expect(screen.queryByRole('link', { name: 'Privacy Policy' })).toBeInTheDocument(); + }); +}); diff --git a/packages/clerk-js/src/ui/elements/index.ts b/packages/clerk-js/src/ui/elements/index.ts index 6d86e286eea..0836b5465e7 100644 --- a/packages/clerk-js/src/ui/elements/index.ts +++ b/packages/clerk-js/src/ui/elements/index.ts @@ -56,3 +56,4 @@ export * from './ProfileCard'; export * from './Gauge'; export * from './Animated'; export * from './DevModeNotice'; +export * from './LegalConsentCheckbox'; diff --git a/packages/clerk-js/src/ui/localization/localizationModifiers.ts b/packages/clerk-js/src/ui/localization/localizationModifiers.ts index 1e84382923c..ed266d10ea3 100644 --- a/packages/clerk-js/src/ui/localization/localizationModifiers.ts +++ b/packages/clerk-js/src/ui/localization/localizationModifiers.ts @@ -27,9 +27,14 @@ const numeric = (val: Date | number | string, locale?: string) => { } }; +const link = (val: string, label?: string) => { + return `[${label}](${val})`; +}; + export const MODIFIERS = { titleize, timeString, weekday, numeric, + link, } as const; diff --git a/packages/clerk-js/src/ui/polishedAppearance.ts b/packages/clerk-js/src/ui/polishedAppearance.ts index fc56872c229..623aeec6e80 100644 --- a/packages/clerk-js/src/ui/polishedAppearance.ts +++ b/packages/clerk-js/src/ui/polishedAppearance.ts @@ -37,6 +37,30 @@ const inputShadowStyles = ( }; }; +const checkboxShadowStyles = ( + theme: InternalTheme, + colors: { idle1: string; idle2: string; hover1: string; hover2: string; focus: string }, +) => { + const idleShadow = [ + `0px 0px 0px 1px ${colors.idle1}`, + theme.shadows.$input.replace('{{color}}', colors.idle2), + ].toString(); + const hoverShadow = [ + `0px 0px 0px 1px ${colors.hover1}`, + theme.shadows.$input.replace('{{color}}', colors.hover2), + ].toString(); + + return { + boxShadow: idleShadow, + '&:hover': { + boxShadow: hoverShadow, + }, + '&:focus-visible': { + boxShadow: [hoverShadow, theme.shadows.$focusRing.replace('{{color}}', colors.focus)].toString(), + }, + }; +}; + const inputStyles = (theme: InternalTheme) => ({ borderWidth: 0, ...inputShadowStyles(theme, { @@ -143,6 +167,29 @@ export const polishedAppearance: Appearance = { ...inputStyles(theme), }, }, + checkbox: { + ...checkboxShadowStyles(theme, { + idle1: theme.colors.$neutralAlpha150, + idle2: theme.colors.$neutralAlpha100, + hover1: theme.colors.$neutralAlpha300, + hover2: theme.colors.$neutralAlpha150, + focus: theme.colors.$neutralAlpha150, + }), + padding: theme.space.$1, + width: theme.sizes.$3x5, + height: theme.sizes.$3x5, + appearance: 'none', + borderRadius: theme.radii.$sm, + border: 'none', + '&:checked': { + backgroundImage: `url("data:image/svg+xml,%3Csvg width='16' height='14' viewBox='0 0 14 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.25 8L6.5 9.75L9.75 4.25' stroke='${theme.colors.$whiteAlpha900}' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3C/path%3E%3C/svg%3E")`, + borderColor: theme.colors.$transparent, + backgroundColor: theme.colors.$primary900, + backgroundSize: '100% 100%', + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + }, + }, tagInputContainer: { ...inputStyles(theme), }, diff --git a/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts b/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts index 58b662d3568..2a595481de0 100644 --- a/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts +++ b/packages/clerk-js/src/ui/utils/test/fixtureHelpers.ts @@ -262,7 +262,16 @@ const createSignUpFixtureHelpers = (baseClient: ClientJSON) => { } as SignUpJSON; }; - return { startSignUpWithEmailAddress, startSignUpWithPhoneNumber }; + const startSignUpWithMissingLegalAccepted = () => { + baseClient.sign_up = { + id: 'sua_2HseAXFGN12eqlwARPMxyyUa9o9', + status: 'missing_requirements', + legal_accepted_at: null, + missing_fields: ['legal_accepted'], + } as SignUpJSON; + }; + + return { startSignUpWithEmailAddress, startSignUpWithPhoneNumber, startSignUpWithMissingLegalAccepted }; }; const createAuthConfigFixtureHelpers = (environment: EnvironmentJSON) => { @@ -285,7 +294,15 @@ const createDisplayConfigFixtureHelpers = (environment: EnvironmentJSON) => { const withPreferredSignInStrategy = (opts: { strategy: DisplayConfigJSON['preferred_sign_in_strategy'] }) => { dc.preferred_sign_in_strategy = opts.strategy; }; - return { withSupportEmail, withoutClerkBranding, withPreferredSignInStrategy }; + + const withTermsPrivacyPolicyUrls = (opts: { + termsOfService?: DisplayConfigJSON['terms_url']; + privacyPolicy?: DisplayConfigJSON['privacy_policy_url']; + }) => { + dc.terms_url = opts.termsOfService || ''; + dc.privacy_policy_url = opts.privacyPolicy || ''; + }; + return { withSupportEmail, withoutClerkBranding, withPreferredSignInStrategy, withTermsPrivacyPolicyUrls }; }; const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON) => { @@ -487,6 +504,10 @@ const createUserSettingsFixtureHelpers = (environment: EnvironmentJSON) => { us.sign_up.mode = SIGN_UP_MODES.RESTRICTED; }; + const withLegalConsent = () => { + us.sign_up.legal_consent_enabled = true; + }; + // TODO: Add the rest, consult pkg/generate/auth_config.go return { @@ -505,5 +526,6 @@ const createUserSettingsFixtureHelpers = (environment: EnvironmentJSON) => { withPasskey, withPasskeySettings, withRestrictedMode, + withLegalConsent, }; }; diff --git a/packages/clerk-js/src/ui/utils/useFormControl.ts b/packages/clerk-js/src/ui/utils/useFormControl.ts index dddb5060631..885596a56b1 100644 --- a/packages/clerk-js/src/ui/utils/useFormControl.ts +++ b/packages/clerk-js/src/ui/utils/useFormControl.ts @@ -181,12 +181,12 @@ export const useFormControl = ( return { props, ...props, buildErrorMessage, setError, setValue, setChecked }; }; -type FormControlStateLike = Pick; +type FormControlStateLike = Pick; export const buildRequest = (fieldStates: Array): Record => { const request: { [x: string]: any } = {}; fieldStates.forEach(x => { - request[x.id] = x.value; + request[x.id] = x.type !== 'checkbox' ? x.value : x.checked; }); return request; }; diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 7611951666d..be3a0961b0f 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -515,6 +515,18 @@ export const enUS: LocalizationResource = { actionText: 'Already have an account?', blockButton__emailSupport: 'Email support', }, + __experimental_legalConsent: { + continue: { + subtitle: 'Please read and accept the terms to continue', + title: 'Legal consent', + }, + checkbox: { + label__termsOfServiceAndPrivacyPolicy: + 'I agree to the {{ termsOfServiceLink || link("Terms of Service") }} and {{ privacyPolicyLink || link("Privacy Policy") }}', + label__onlyTermsOfService: 'I agree to the {{ termsOfServiceLink || link("Terms of Service") }}', + label__onlyPrivacyPolicy: 'I agree to the {{ privacyPolicyLink || link("Privacy Policy") }}', + }, + }, }, socialButtonsBlockButton: 'Continue with {{provider|titleize}}', socialButtonsBlockButtonManyInView: '{{provider|titleize}}', diff --git a/packages/types/src/attributes.ts b/packages/types/src/attributes.ts index 44d170d3396..7cc23cbcc46 100644 --- a/packages/types/src/attributes.ts +++ b/packages/types/src/attributes.ts @@ -1,3 +1,4 @@ export type FirstNameAttribute = 'first_name'; export type LastNameAttribute = 'last_name'; export type PasswordAttribute = 'password'; +export type LegalAcceptedAttribute = 'legal_accepted'; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 841ddb1e255..8dbcb282057 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -1287,6 +1287,7 @@ export interface ClerkAuthenticateWithWeb3Params { signUpContinueUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; strategy: Web3Strategy; + __experimental_legalAccepted?: boolean; } export interface AuthenticateWithMetamaskParams { @@ -1294,6 +1295,7 @@ export interface AuthenticateWithMetamaskParams { redirectUrl?: string; signUpContinueUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; + __experimental_legalAccepted?: boolean; } export interface AuthenticateWithCoinbaseWalletParams { @@ -1301,10 +1303,12 @@ export interface AuthenticateWithCoinbaseWalletParams { redirectUrl?: string; signUpContinueUrl?: string; unsafeMetadata?: SignUpUnsafeMetadata; + __experimental_legalAccepted?: boolean; } export interface AuthenticateWithGoogleOneTapParams { token: string; + __experimental_legalAccepted?: boolean; } export interface LoadedClerk extends Clerk { diff --git a/packages/types/src/displayConfig.ts b/packages/types/src/displayConfig.ts index 8d3f2c7a58a..bd25208f273 100644 --- a/packages/types/src/displayConfig.ts +++ b/packages/types/src/displayConfig.ts @@ -38,6 +38,8 @@ export interface DisplayConfigJSON { after_create_organization_url: string; google_one_tap_client_id?: string; show_devmode_warning: boolean; + terms_url: string; + privacy_policy_url: string; } export interface DisplayConfigResource extends ClerkResource { @@ -78,4 +80,6 @@ export interface DisplayConfigResource extends ClerkResource { afterCreateOrganizationUrl: string; googleOneTapClientId?: string; showDevModeWarning: boolean; + termsUrl: string; + privacyPolicyUrl: string; } diff --git a/packages/types/src/elementIds.ts b/packages/types/src/elementIds.ts index d07c2918926..69bc36ce171 100644 --- a/packages/types/src/elementIds.ts +++ b/packages/types/src/elementIds.ts @@ -20,7 +20,8 @@ export type FieldId = | 'deleteOrganizationConfirmation' | 'enrollmentMode' | 'affiliationEmailAddress' - | 'deleteExistingInvitationsSuggestions'; + | 'deleteExistingInvitationsSuggestions' + | '__experimental_legalAccepted'; export type ProfileSectionId = | 'profile' | 'username' diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts index 01f1a8b0c22..6e249fdaa83 100644 --- a/packages/types/src/json.ts +++ b/packages/types/src/json.ts @@ -97,6 +97,7 @@ export interface SignUpJSON extends ClerkResourceJSON { created_session_id: string | null; created_user_id: string | null; abandon_at: number | null; + legal_accepted_at: number | null; verifications: SignUpVerificationsJSON | null; } @@ -233,6 +234,7 @@ export interface UserJSON extends ClerkResourceJSON { create_organization_enabled: boolean; create_organizations_limit: number | null; delete_self_enabled: boolean; + legal_accepted_at: number | null; updated_at: number; created_at: number; } diff --git a/packages/types/src/localization.ts b/packages/types/src/localization.ts index 27caaedfc50..ef95d7564ab 100644 --- a/packages/types/src/localization.ts +++ b/packages/types/src/localization.ts @@ -151,6 +151,17 @@ type _LocalizationResource = { actionText: LocalizationValue; blockButton__emailSupport: LocalizationValue; }; + __experimental_legalConsent: { + continue: { + title: LocalizationValue; + subtitle: LocalizationValue; + }; + checkbox: { + label__termsOfServiceAndPrivacyPolicy: LocalizationValue; + label__onlyPrivacyPolicy: LocalizationValue; + label__onlyTermsOfService: LocalizationValue; + }; + }; }; signIn: { start: { diff --git a/packages/types/src/redirects.ts b/packages/types/src/redirects.ts index cfd52ede8e2..9e9137d67be 100644 --- a/packages/types/src/redirects.ts +++ b/packages/types/src/redirects.ts @@ -79,6 +79,11 @@ export type AuthenticateWithRedirectParams = { * Email address to use for targeting a SAML connection at sign-up */ emailAddress?: string; + + /** + * Whether the user has accepted the legal requirements. + */ + __experimental_legalAccepted?: boolean; }; export type RedirectUrlProp = { diff --git a/packages/types/src/signUp.ts b/packages/types/src/signUp.ts index 9f060bae842..ed269259c13 100644 --- a/packages/types/src/signUp.ts +++ b/packages/types/src/signUp.ts @@ -1,4 +1,4 @@ -import type { FirstNameAttribute, LastNameAttribute, PasswordAttribute } from './attributes'; +import type { FirstNameAttribute, LastNameAttribute, LegalAcceptedAttribute, PasswordAttribute } from './attributes'; import type { AttemptEmailAddressVerificationParams, PrepareEmailAddressVerificationParams } from './emailAddress'; import type { EmailAddressIdentifier, @@ -59,6 +59,7 @@ export interface SignUpResource extends ClerkResource { createdSessionId: string | null; createdUserId: string | null; abandonAt: number | null; + legalAcceptedAt: number | null; create: (params: SignUpCreateParams) => Promise; @@ -89,7 +90,10 @@ export interface SignUpResource extends ClerkResource { ) => Promise; authenticateWithWeb3: ( - params: AuthenticateWithWeb3Params & { unsafeMetadata?: SignUpUnsafeMetadata }, + params: AuthenticateWithWeb3Params & { + unsafeMetadata?: SignUpUnsafeMetadata; + __experimental_legalAccepted?: boolean; + }, ) => Promise; authenticateWithMetamask: (params?: SignUpAuthenticateWithWeb3Params) => Promise; @@ -135,7 +139,7 @@ export type AttemptVerificationParams = signature: string; }; -export type SignUpAttributeField = FirstNameAttribute | LastNameAttribute | PasswordAttribute; +export type SignUpAttributeField = FirstNameAttribute | LastNameAttribute | PasswordAttribute | LegalAcceptedAttribute; // TODO: SignUpVerifiableField or SignUpIdentifier? export type SignUpVerifiableField = @@ -161,7 +165,8 @@ export type SignUpCreateParams = Partial< unsafeMetadata: SignUpUnsafeMetadata; ticket: string; token: string; - } & SnakeToCamel> + __experimental_legalAccepted: boolean; + } & Omit>, 'legalAccepted'> >; export type SignUpUpdateParams = SignUpCreateParams; diff --git a/packages/types/src/userSettings.ts b/packages/types/src/userSettings.ts index 9edc2755e70..fafebce3fb4 100644 --- a/packages/types/src/userSettings.ts +++ b/packages/types/src/userSettings.ts @@ -54,6 +54,7 @@ export type SignUpData = { progressive: boolean; captcha_enabled: boolean; mode: SignUpModes; + legal_consent_enabled: boolean; }; export type PasswordSettingsData = {