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 = {