Skip to content

Commit 5bd8d8b

Browse files
committed
Implement email verification step
1 parent 4c44aa6 commit 5bd8d8b

6 files changed

Lines changed: 195 additions & 23 deletions

File tree

packages/localizations/src/en-US.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,26 @@ export const enUS: LocalizationResource = {
204204
navbar: {
205205
title: 'Configure Single Sign-On (SSO)',
206206
},
207+
verifyEmailDomainStep: {
208+
title: 'Verify email address',
209+
subtitle: 'Verify the email address you want to enable the enterprise connection on.',
210+
addEmailAddress: {
211+
formTitle: 'We need your email',
212+
formSubtitle: 'In order to start we will need your email address',
213+
inputPlaceholder: 'name@company.com',
214+
inputLabel: 'Email address',
215+
},
216+
emailCode: {
217+
formTitle: 'Verify your email address',
218+
formSubtitle: 'Enter the verification code sent to {{identifier}}',
219+
resendButton: "Didn't receive a code? Resend",
220+
verified: {
221+
title: 'We got your email',
222+
subtitle: "You've verified your email address with the following email",
223+
inputLabel: 'Verified email address',
224+
},
225+
},
226+
},
207227
},
208228
createOrganization: {
209229
formButtonSubmit: 'Create organization',

packages/shared/src/types/localization.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,26 @@ export type __internal_LocalizationResource = {
12961296
navbar: {
12971297
title: LocalizationValue;
12981298
};
1299+
verifyEmailDomainStep: {
1300+
title: LocalizationValue;
1301+
subtitle: LocalizationValue;
1302+
addEmailAddress: {
1303+
formTitle: LocalizationValue;
1304+
formSubtitle: LocalizationValue;
1305+
inputPlaceholder: LocalizationValue;
1306+
inputLabel: LocalizationValue;
1307+
};
1308+
emailCode: {
1309+
formTitle: LocalizationValue;
1310+
formSubtitle: LocalizationValue<'identifier'>;
1311+
resendButton: LocalizationValue;
1312+
verified: {
1313+
title: LocalizationValue;
1314+
subtitle: LocalizationValue;
1315+
inputLabel: LocalizationValue;
1316+
};
1317+
};
1318+
};
12991319
};
13001320
apiKeys: {
13011321
formTitle: LocalizationValue;

packages/ui/src/components/ConfigureSSO/steps/StepLayout.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import React from 'react';
22

3-
import { Col, Flex, Heading, Text } from '@/customizables';
3+
import { Col, Flex, Heading, type LocalizationKey, Text } from '@/customizables';
44

55
import { ConfigureSSOWizard } from '../wizard';
66

77
interface StepLayoutProps {
8-
title?: React.ReactNode;
9-
subtitle?: React.ReactNode;
8+
title?: LocalizationKey | string;
9+
subtitle?: LocalizationKey | string;
1010
children: React.ReactNode;
1111
}
1212

@@ -38,18 +38,16 @@ export const StepLayout = ({ title, subtitle, children }: StepLayoutProps): JSX.
3838
<Heading
3939
textVariant='h3'
4040
sx={theme => ({ color: theme.colors.$colorForeground, fontSize: theme.fontSizes.$lg })}
41-
>
42-
{title}
43-
</Heading>
41+
localizationKey={title}
42+
/>
4443

4544
{subtitle ? (
4645
<Text
4746
as='p'
4847
variant='body'
4948
sx={theme => ({ color: theme.colors.$colorMutedForeground })}
50-
>
51-
{subtitle}
52-
</Text>
49+
localizationKey={subtitle}
50+
/>
5351
) : null}
5452
</Col>
5553
) : null}

packages/ui/src/components/ConfigureSSO/steps/VerifyDomainStep.tsx

Lines changed: 138 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,154 @@
1-
import { Flow, Text } from '@/customizables';
1+
import { useReverification, useUser } from '@clerk/shared/react';
2+
import React from 'react';
3+
4+
import { Col, Flow, Heading, Icon, Input, localizationKeys, Text, useLocalizations } from '@/customizables';
5+
import { useFieldOTP } from '@/elements/CodeControl';
6+
import { useCardState } from '@/elements/contexts';
7+
import { Form } from '@/elements/Form';
8+
import { handleError } from '@/utils/errorHandler';
29

310
import { useConfigureSSOWizard, useRegisterContinueAction } from '../wizard';
411
import { StepLayout } from './StepLayout';
12+
import { DuotoneAtSymbol } from '@/icons';
513

614
export const VerifyDomainStep = (): JSX.Element => {
715
const { goNext } = useConfigureSSOWizard();
16+
const card = useCardState();
17+
const { t } = useLocalizations();
18+
const { user } = useUser();
19+
20+
const primaryEmailAddress = user?.primaryEmailAddress;
21+
const isVerified = primaryEmailAddress?.verification.status === 'verified';
22+
23+
const prepareEmailVerification = useReverification(() =>
24+
primaryEmailAddress?.prepareVerification({ strategy: 'email_code' }),
25+
);
26+
const attemptEmailVerification = useReverification((code: string) =>
27+
primaryEmailAddress?.attemptVerification({ code }),
28+
);
29+
30+
const prepare = React.useCallback(
31+
() => prepareEmailVerification()?.catch(err => handleError(err, [], card.setError)),
32+
[prepareEmailVerification, card],
33+
);
834

9-
useRegisterContinueAction({
10-
handler: () => goNext(),
11-
// TODO: Implement verification
12-
isDisabled: true,
35+
const codeSubmittedRef = React.useRef(false);
36+
37+
const otp = useFieldOTP({
38+
onCodeEntryFinished: (code, resolve, reject) => {
39+
codeSubmittedRef.current = true;
40+
attemptEmailVerification(code)
41+
.then(() => resolve())
42+
.catch(reject);
43+
},
44+
onResendCodeClicked: () => {
45+
void prepare();
46+
},
47+
onResolve: () => {
48+
void goNext();
49+
},
1350
});
1451

52+
const { values, length } = otp.otpControl.otpInputProps;
53+
const isCodeComplete = values.filter(Boolean).length === length;
54+
const showVerifiedView = isVerified && !codeSubmittedRef.current;
55+
56+
useRegisterContinueAction(
57+
showVerifiedView
58+
? {
59+
handler: () => {
60+
void goNext();
61+
},
62+
}
63+
: {
64+
handler: otp.onFakeContinue,
65+
isDisabled: !isCodeComplete,
66+
isLoading: otp.isLoading,
67+
},
68+
);
69+
70+
// Send the first code on mount (only when there's something to verify),
71+
// and clear any stale card error that could be lingering from a previous step
72+
React.useEffect(() => {
73+
if (!isVerified) {
74+
void prepare();
75+
}
76+
card.setError(undefined);
77+
return () => card.setError(undefined);
78+
// eslint-disable-next-line react-hooks/exhaustive-deps
79+
}, []);
80+
1581
return (
1682
<Flow.Part part='verifyDomain'>
1783
<StepLayout
18-
title='Verify your domain'
19-
subtitle='Verify the domain you want to enable the enterprise connection on.'
84+
title={localizationKeys('configureSSO.verifyEmailDomainStep.title')}
85+
subtitle={localizationKeys('configureSSO.verifyEmailDomainStep.subtitle')}
2086
>
21-
<Text>UI goes here</Text>
87+
<Col
88+
align='center'
89+
sx={t => ({
90+
flex: 1,
91+
justifyContent: 'center',
92+
gap: t.space.$2,
93+
paddingBlock: t.space.$8,
94+
})}
95+
>
96+
{showVerifiedView && primaryEmailAddress ? (
97+
<>
98+
<Icon
99+
icon={DuotoneAtSymbol}
100+
sx={t => ({
101+
width: t.sizes.$8,
102+
height: t.sizes.$8,
103+
color: t.colors.$neutralAlpha600,
104+
})}
105+
/>
106+
<Col sx={t => ({ gap: t.space.$1, textAlign: 'center', maxWidth: t.sizes.$66 })}>
107+
<Heading
108+
textVariant='h1'
109+
sx={t => ({ color: t.colors.$colorForeground, fontSize: t.fontSizes.$sm })}
110+
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.verified.title')}
111+
/>
112+
<Text
113+
as='p'
114+
variant='body'
115+
sx={t => ({ color: t.colors.$colorMutedForeground })}
116+
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.verified.subtitle')}
117+
/>
118+
</Col>
119+
<Input
120+
type='email'
121+
value={primaryEmailAddress.emailAddress}
122+
readOnly
123+
aria-label={t(localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.verified.inputLabel'))}
124+
sx={t => ({ width: '100%', maxWidth: t.sizes.$66, backgroundColor: t.colors.$neutralAlpha50 })}
125+
/>
126+
</>
127+
) : (
128+
<>
129+
<Col sx={t => ({ gap: t.space.$1, textAlign: 'center' })}>
130+
<Heading
131+
textVariant='h1'
132+
sx={t => ({ color: t.colors.$colorForeground, fontSize: t.fontSizes.$sm })}
133+
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.formTitle')}
134+
/>
135+
<Text
136+
as='p'
137+
variant='body'
138+
sx={t => ({ color: t.colors.$colorMutedForeground })}
139+
localizationKey={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.formSubtitle', {
140+
identifier: primaryEmailAddress?.emailAddress ?? '',
141+
})}
142+
/>
143+
</Col>
144+
145+
<Form.OTPInput
146+
{...otp}
147+
resendButton={localizationKeys('configureSSO.verifyEmailDomainStep.emailCode.resendButton')}
148+
/>
149+
</>
150+
)}
151+
</Col>
22152
</StepLayout>
23153
</Flow.Part>
24154
);
Lines changed: 3 additions & 0 deletions
Loading

packages/ui/src/icons/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ export { default as AuthApp } from './auth-app.svg';
1414
export { default as Billing } from './billing.svg';
1515
export { default as Block } from './block.svg';
1616
export { default as BoxIcon } from './box.svg';
17-
export { default as Caret } from './caret.svg';
1817
export { default as CaretLeft } from './caret-left.svg';
1918
export { default as CaretRight } from './caret-right.svg';
19+
export { default as Caret } from './caret.svg';
2020
export { default as ChatAltIcon } from './chat-alt.svg';
21-
export { default as Check } from './check.svg';
2221
export { default as CheckCircle } from './check-circle.svg';
22+
export { default as Check } from './check.svg';
2323
export { default as CheckmarkFilled } from './checkmark-filled.svg';
2424
export { default as ChevronDown } from './chevron-down.svg';
2525
export { default as ChevronUpDown } from './chevron-up-down.svg';
26-
export { default as Clipboard } from './clipboard.svg';
2726
export { default as ClipboardOutline } from './clipboard-outline.svg';
27+
export { default as Clipboard } from './clipboard.svg';
2828
export { default as Close } from './close.svg';
2929
export { default as Code } from './code.svg';
3030
export { default as CogFilled } from './cog-filled.svg';
@@ -34,11 +34,12 @@ export { default as DeviceLaptop } from './device-laptop.svg';
3434
export { default as DeviceMobile } from './device-mobile.svg';
3535
export { default as DotCircle } from './dot-circle-horizontal.svg';
3636
export { default as Download } from './download.svg';
37+
export { default as DuotoneAtSymbol } from './duotone-at-symbol.svg';
3738
export { default as Email } from './email.svg';
3839
export { default as ExclamationCircle } from './exclamation-circle.svg';
3940
export { default as ExclamationTriangle } from './exclamation-triangle.svg';
40-
export { default as Eye } from './eye.svg';
4141
export { default as EyeSlash } from './eye-slash.svg';
42+
export { default as Eye } from './eye.svg';
4243
export { default as Fingerprint } from './fingerprint.svg';
4344
export { default as Folder } from './folder.svg';
4445
export { default as GenericPayment } from './generic-pay.svg';
@@ -52,17 +53,17 @@ export { default as Menu } from './menu.svg';
5253
export { default as Minus } from './minus.svg';
5354
export { default as Mobile } from './mobile-small.svg';
5455
export { default as Organization } from './organization.svg';
55-
export { default as Pencil } from './pencil.svg';
5656
export { default as PencilEdit } from './pencil-edit.svg';
57+
export { default as Pencil } from './pencil.svg';
5758
export { default as Plans } from './plans.svg';
5859
export { default as Plus } from './plus.svg';
5960
export { default as Print } from './print.svg';
6061
export { default as QuestionMark } from './question-mark.svg';
6162
export { default as RequestAuthIcon } from './request-auth.svg';
6263
export { default as RotateLeftRight } from './rotate-left-right.svg';
6364
export { default as Selector } from './selector.svg';
64-
export { default as SignOut } from './signout.svg';
6565
export { default as SignOutDouble } from './signout-double.svg';
66+
export { default as SignOut } from './signout.svg';
6667
export { default as SpinnerJumbo } from './spinner-jumbo.svg';
6768
export { default as SwitchArrowRight } from './switch-arrow-right.svg';
6869
export { default as SwitchArrows } from './switch-arrows.svg';

0 commit comments

Comments
 (0)