Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
octoper committed Oct 29, 2024
1 parent 01258ed commit bcb2dc6
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { snakeToCamel } from '@clerk/shared/underscore';
import type { SignUpResource } from '@clerk/types';
import type { DoneActorEvent } from 'xstate';
import { fromPromise, setup } from 'xstate';
import { fromPromise, not, or, setup } from 'xstate';

import { SIGN_UP_DEFAULT_BASE_PATH } from '~/internals/constants';
import type { FormDefaultValues, FormFields } from '~/internals/machines/form';
Expand Down Expand Up @@ -62,6 +62,21 @@ export const SignUpContinueMachine = setup({
context.parent.send({ type: 'NEXT', resource: (event as unknown as DoneActorEvent<SignUpResource>).output }),
sendToLoading,
},
guards: {
isMissingRequirements: ({ context }) =>
context.parent.getSnapshot().context.clerk?.client?.signUp?.status === 'missing_requirements',
requirementsMet: ({ context }) => {
const signUp = context.parent.getSnapshot().context.clerk.client.signUp;

const fields = context.formRef.getSnapshot().context.fields;
const signUpMissingFields = signUp.missingFields.map(snakeToCamel);
const missingFields = Array.from(context.formRef.getSnapshot().context.fields.keys()).filter(key => {
return !signUpMissingFields.includes(key) && !fields.get(key)?.value && !fields.get(key)?.checked;
});

return missingFields.length === 0;
},
},
types: {} as SignUpContinueSchema,
}).createMachine({
id: SignUpContinueMachineId,
Expand All @@ -82,6 +97,7 @@ export const SignUpContinueMachine = setup({
description: 'Waiting for user input',
on: {
SUBMIT: {
guard: or(['requirementsMet', not('isMissingRequirements')]),
target: 'Attempting',
reenter: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ import type { SignUpCreateParams, SignUpUpdateParams } from '@clerk/types';

import type { FormFields } from '~/internals/machines/form';

const SignUpAdditionalKeys = ['firstName', 'lastName', 'emailAddress', 'username', 'password', 'phoneNumber'] as const;
const SignUpAdditionalKeys = [
'firstName',
'lastName',
'emailAddress',
'username',
'password',
'phoneNumber',
'__experimental_legalAccepted',
] as const;

type SignUpAdditionalKeys = (typeof SignUpAdditionalKeys)[number];

Expand All @@ -17,10 +25,14 @@ export function fieldsToSignUpParams<T extends SignUpCreateParams | SignUpUpdate
): Pick<T, SignUpAdditionalKeys> {
const params: SignUpUpdateParams = {};

fields.forEach(({ value }, key) => {
fields.forEach(({ value, checked }, key) => {
if (isSignUpParam(key) && value !== undefined) {
params[key] = value as string;
}

if (isSignUpParam(key) && checked !== undefined) {
params[key] = checked as boolean;
}
});

return params;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,17 @@ export const ThirdPartyMachine = setup({
input: ({ context, event }) => {
assertEvent(event, 'REDIRECT');

const legalAcceptedField = context.formRef
.getSnapshot()
.context.fields.get('__experimental_legalAccepted')?.checked;

return {
basePath: context.basePath,
flow: context.flow,
params: event.params,
params: {
...event.params,
__experimental_legalAccepted: legalAcceptedField || undefined,
},
parent: context.parent,
};
},
Expand Down
1 change: 1 addition & 0 deletions packages/elements/src/react/common/connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const useConnectionContext = () => {
export interface ConnectionProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
name: OAuthProvider | Web3Provider | SamlStrategy;
legalAcceptence?: boolean;
}

/**
Expand Down
68 changes: 68 additions & 0 deletions packages/ui/src/common/legal-accepted.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import * as Common from '@clerk/elements/common';
import React from 'react';

import { useAppearance } from '~/contexts';
import { useEnvironment } from '~/hooks/use-environment';
import { useLocalizations } from '~/hooks/use-localizations';
import * as Field from '~/primitives/field';

import { LinkRenderer } from './link-renderer';

export function LegalAcceptedField(props: Omit<React.ComponentProps<CheckboxField>, 'type'>) {
const { t } = useLocalizations();
const { displayConfig } = useEnvironment();
const { parsedAppearance } = useAppearance();
const termsUrl = parsedAppearance.options.termsPageUrl || displayConfig.termsUrl;
const privacyPolicyUrl = parsedAppearance.options.privacyPageUrl || displayConfig.privacyPolicyUrl;

let localizedText: string | undefined;

if (termsUrl && privacyPolicyUrl) {
localizedText = t('signUp.__experimental_legalConsent.checkbox.label__termsOfServiceAndPrivacyPolicy', {
termsOfServiceLink: termsUrl,
privacyPolicyLink: privacyPolicyUrl,
});
} else if (termsUrl) {
localizedText = t('signUp.__experimental_legalConsent.checkbox.label__onlyTermsOfService', {
termsOfServiceLink: termsUrl,
});
} else if (privacyPolicyUrl) {
localizedText = t('signUp.__experimental_legalConsent.checkbox.label__onlyPrivacyPolicy', {
privacyPolicyLink: privacyPolicyUrl,
});
}

return (
<Common.Field
name='__experimental_legalAccepted'
asChild
>
<Field.Root>
<div className='flex gap-2'>
<Common.Input
type='checkbox'
asChild
{...props}
>
<Field.Checkbox />
</Common.Input>

<Common.Label asChild>
<Field.Label className='!block'>
<LinkRenderer
text={localizedText || ''}
className='underline underline-offset-2'
/>
</Field.Label>
</Common.Label>
</div>

<Common.FieldError asChild>
{({ message }) => {
return <Field.Message intent='error'>{message}</Field.Message>;
}}
</Common.FieldError>
</Field.Root>
</Common.Field>
);
}
44 changes: 44 additions & 0 deletions packages/ui/src/common/link-renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { memo, useMemo } from 'react';

interface LinkRendererProps extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'children' | 'class'> {
text: string;
className?: string;
}

const LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g; // parses [text](url)

export const LinkRenderer: React.FC<LinkRendererProps> = 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(
<a
{...memoizedLinkProps}
href={url}
target='_blank'
rel='noopener noreferrer'
key={offset}
>
{linkText}
</a>,
);
lastIndex = offset + match.length;
return match;
});

if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}

return parts;
}, [text, memoizedLinkProps]);

return renderedContent;
});
8 changes: 8 additions & 0 deletions packages/ui/src/components/sign-up/steps/continue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { EmailField } from '~/common/email-field';
import { FirstNameField } from '~/common/first-name-field';
import { GlobalError } from '~/common/global-error';
import { LastNameField } from '~/common/last-name-field';
import { LegalAcceptedField } from '~/common/legal-accepted';
import { PasswordField } from '~/common/password-field';
import { PhoneNumberField } from '~/common/phone-number-field';
import { RouterLink } from '~/common/router-link';
Expand All @@ -14,6 +15,7 @@ import { LOCALIZATION_NEEDED } from '~/constants/localizations';
import { useAttributes } from '~/hooks/use-attributes';
import { useCard } from '~/hooks/use-card';
import { useDevModeWarning } from '~/hooks/use-dev-mode-warning';
import { useEnvironment } from '~/hooks/use-environment';
import { useLocalizations } from '~/hooks/use-localizations';
import { useOptions } from '~/hooks/use-options';
import { Button } from '~/primitives/button';
Expand All @@ -24,11 +26,15 @@ export function SignUpContinue() {
const clerk = useClerk();
const { signInUrl } = useOptions();
const { t } = useLocalizations();
const environment = useEnvironment();
const { client } = useClerk();
const { enabled: firstNameEnabled, required: firstNameRequired } = useAttributes('first_name');
const { enabled: lastNameEnabled, required: lastNameRequired } = useAttributes('last_name');
const { enabled: usernameEnabled, required: usernameRequired } = useAttributes('username');
const { enabled: phoneNumberEnabled, required: phoneNumberRequired } = useAttributes('phone_number');
const { enabled: passwordEnabled, required: passwordRequired } = useAttributes('password');
const legalConsentEnabled =
environment.userSettings.signUp.legal_consent_enabled && client.signUp.missingFields.includes('legal_accepted');
const isDev = useDevModeWarning();
const { logoProps, footerProps } = useCard();

Expand Down Expand Up @@ -92,6 +98,8 @@ export function SignUpContinue() {
disabled={isGlobalLoading}
/>
) : null}

{legalConsentEnabled && <LegalAcceptedField />}
</div>
</Card.Body>
<Card.Actions>
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/components/sign-up/steps/start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { EmailOrPhoneNumberField } from '~/common/email-or-phone-number-field';
import { FirstNameField } from '~/common/first-name-field';
import { GlobalError } from '~/common/global-error';
import { LastNameField } from '~/common/last-name-field';
import { LegalAcceptedField } from '~/common/legal-accepted';
import { PasswordField } from '~/common/password-field';
import { PhoneNumberField } from '~/common/phone-number-field';
import { RouterLink } from '~/common/router-link';
Expand Down Expand Up @@ -54,6 +55,7 @@ export function SignUpStart() {
const isDev = useDevModeWarning();
const { options } = useAppearance().parsedAppearance;
const { logoProps, footerProps } = useCard();
const legalConsentEnabled = userSettings.signUp.legal_consent_enabled;

return (
<Common.Loading scope='global'>
Expand Down Expand Up @@ -144,10 +146,13 @@ export function SignUpStart() {

{options.socialButtonsPlacement === 'bottom' ? connectionsWithSeperator.reverse() : null}

{!(hasConnection && hasIdentifier && legalConsentEnabled) && <LegalAcceptedField />}

{userSettings.signUp.captcha_enabled ? <SignUp.Captcha className='empty:hidden' /> : null}
</Card.Body>
{hasConnection || hasIdentifier ? (
{hasIdentifier ? (
<Card.Actions>
{legalConsentEnabled && hasIdentifier && <LegalAcceptedField />}
<Common.Loading scope='submit'>
{isSubmitting => {
return (
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/utils/make-localizable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,16 @@ const numeric = (val: Date | number | string, locale?: string) => {
}
};

const link = (val: string, label?: string) => {
return `[${label}](${val})`;
};

const MODIFIERS = {
titleize,
timeString,
weekday,
numeric,
link,
} as const;

const applyTokenExpressions = (s: string, expressions: TokenExpression[], tokens: Tokens) => {
Expand Down

0 comments on commit bcb2dc6

Please sign in to comment.