Skip to content

Commit afad9af

Browse files
authored
feat(clerk-js,types,nextjs,localizations,clerk-react): Introduce UserVerification as experimental (#4016)
1 parent 11c3c41 commit afad9af

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1785
-32
lines changed

.changeset/calm-zoos-sort.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/localizations": minor
3+
---
4+
5+
Add localization keys for `<__experimental_UserVerification />` (experimental feature).

.changeset/fifty-ravens-attack.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@clerk/chrome-extension": minor
3+
"@clerk/nextjs": minor
4+
"@clerk/clerk-react": minor
5+
---
6+
7+
Add `<__experimental_UserVerification />` component. This is an experimental feature and breaking changes can occur until it's marked as stable.

.changeset/healthy-colts-look.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@clerk/clerk-js": minor
3+
---
4+
5+
Add new `UserVerification` component (experimental feature). This UI component allows for a user to "re-enter" their credentials (first factor and/or second factor) which results in them being re-verified.
6+
7+
New methods have been added:
8+
9+
- `__experimental_openUserVerification()`
10+
- `__experimental_closeUserVerification()`
11+
- `__experimental_mountUserVerification(targetNode: HTMLDivElement)`
12+
- `__experimental_unmountUserVerification(targetNode: HTMLDivElement)`

.changeset/proud-dryers-smile.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@clerk/types": minor
3+
---
4+
5+
Add types for newly introduced `<__experimental_UserVerification />` component (experimental feature). New types:
6+
7+
- `Appearance` has a new `userVerification` property
8+
- `__experimental_UserVerificationProps` and `__experimental_UserVerificationModalProps`
9+
- `__experimental_openUserVerification` method under the `Clerk` interface
10+
- `__experimental_closeUserVerification` method under the `Clerk` interface
11+
- `__experimental_mountUserVerification` method under the `Clerk` interface
12+
- `__experimental_unmountUserVerification` method under the `Clerk` interface
13+
- `__experimental_userVerification` property under `LocalizationResource`

packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ exports[`public exports should not include a breaking change 1`] = `
2727
"SignedOut",
2828
"UserButton",
2929
"UserProfile",
30+
"__experimental_UserVerification",
3031
"useAuth",
3132
"useClerk",
3233
"useEmailLink",

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
{ "path": "./dist/signup*.js", "maxSize": "10KB" },
1414
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
1515
{ "path": "./dist/userprofile*.js", "maxSize": "15KB" },
16+
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
1617
{ "path": "./dist/onetap*.js", "maxSize": "1KB" }
1718
]
1819
}

packages/clerk-js/src/core/clerk.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
import { logger } from '@clerk/shared/logger';
1818
import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry';
1919
import type {
20+
__experimental_UserVerificationModalProps,
21+
__experimental_UserVerificationProps,
2022
ActiveSessionResource,
2123
AuthenticateWithCoinbaseParams,
2224
AuthenticateWithGoogleOneTapParams,
@@ -348,6 +350,7 @@ export class Clerk implements ClerkInterface {
348350
};
349351

350352
public openGoogleOneTap = (props?: GoogleOneTapProps): void => {
353+
// TODO: add telemetry
351354
this.assertComponentsReady(this.#componentControls);
352355
void this.#componentControls
353356
.ensureMounted({ preloadHint: 'GoogleOneTap' })
@@ -379,6 +382,26 @@ export class Clerk implements ClerkInterface {
379382
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('signIn'));
380383
};
381384

385+
public __experimental_openUserVerification = (props?: __experimental_UserVerificationModalProps): void => {
386+
this.assertComponentsReady(this.#componentControls);
387+
if (noUserExists(this)) {
388+
if (this.#instanceType === 'development') {
389+
throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, {
390+
code: 'cannot_render_user_missing',
391+
});
392+
}
393+
return;
394+
}
395+
void this.#componentControls
396+
.ensureMounted({ preloadHint: 'UserVerification' })
397+
.then(controls => controls.openModal('userVerification', props || {}));
398+
};
399+
400+
public __experimental_closeUserVerification = (): void => {
401+
this.assertComponentsReady(this.#componentControls);
402+
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification'));
403+
};
404+
382405
public openSignUp = (props?: SignUpProps): void => {
383406
this.assertComponentsReady(this.#componentControls);
384407
if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) {
@@ -489,6 +512,38 @@ export class Clerk implements ClerkInterface {
489512
);
490513
};
491514

515+
public __experimental_mountUserVerification = (
516+
node: HTMLDivElement,
517+
props?: __experimental_UserVerificationProps,
518+
): void => {
519+
this.assertComponentsReady(this.#componentControls);
520+
if (noUserExists(this)) {
521+
if (this.#instanceType === 'development') {
522+
throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, {
523+
code: 'cannot_render_user_missing',
524+
});
525+
}
526+
return;
527+
}
528+
void this.#componentControls.ensureMounted({ preloadHint: 'UserVerification' }).then(controls =>
529+
controls.mountComponent({
530+
name: 'UserVerification',
531+
appearanceKey: 'userVerification',
532+
node,
533+
props,
534+
}),
535+
);
536+
};
537+
538+
public __experimental_unmountUserVerification = (node: HTMLDivElement): void => {
539+
this.assertComponentsReady(this.#componentControls);
540+
void this.#componentControls.ensureMounted().then(controls =>
541+
controls.unmountComponent({
542+
node,
543+
}),
544+
);
545+
};
546+
492547
public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => {
493548
this.assertComponentsReady(this.#componentControls);
494549
void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls =>
@@ -860,6 +915,7 @@ export class Clerk implements ClerkInterface {
860915

861916
return this.#authService.decorateUrlWithDevBrowserToken(toURL).href;
862917
}
918+
863919
public buildSignInUrl(options?: SignInRedirectOptions): string {
864920
return this.#buildUrl(
865921
'signInUrl',

packages/clerk-js/src/ui/Components.tsx

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createDeferredPromise } from '@clerk/shared';
22
import { useSafeLayoutEffect } from '@clerk/shared/react';
33
import type {
4+
__experimental_UserVerificationProps,
45
Appearance,
56
Clerk,
67
ClerkOptions,
@@ -28,6 +29,7 @@ import {
2829
SignInModal,
2930
SignUpModal,
3031
UserProfileModal,
32+
UserVerificationModal,
3133
} from './lazyModules/components';
3234
import {
3335
LazyComponentRenderer,
@@ -55,13 +57,33 @@ export type ComponentControls = {
5557
props?: unknown;
5658
}) => void;
5759
openModal: <
58-
T extends 'googleOneTap' | 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization',
60+
T extends
61+
| 'googleOneTap'
62+
| 'signIn'
63+
| 'signUp'
64+
| 'userProfile'
65+
| 'organizationProfile'
66+
| 'createOrganization'
67+
| 'userVerification',
5968
>(
6069
modal: T,
61-
props: T extends 'signIn' ? SignInProps : T extends 'signUp' ? SignUpProps : UserProfileProps,
70+
props: T extends 'signIn'
71+
? SignInProps
72+
: T extends 'signUp'
73+
? SignUpProps
74+
: T extends 'userVerification'
75+
? __experimental_UserVerificationProps
76+
: UserProfileProps,
6277
) => void;
6378
closeModal: (
64-
modal: 'googleOneTap' | 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization',
79+
modal:
80+
| 'googleOneTap'
81+
| 'signIn'
82+
| 'signUp'
83+
| 'userProfile'
84+
| 'organizationProfile'
85+
| 'createOrganization'
86+
| 'userVerification',
6587
) => void;
6688
// Special case, as the impersonation fab mounts automatically
6789
mountImpersonationFab: () => void;
@@ -88,6 +110,7 @@ interface ComponentsState {
88110
signInModal: null | SignInProps;
89111
signUpModal: null | SignUpProps;
90112
userProfileModal: null | UserProfileProps;
113+
userVerificationModal: null | __experimental_UserVerificationProps;
91114
organizationProfileModal: null | OrganizationProfileProps;
92115
createOrganizationModal: null | CreateOrganizationProps;
93116
nodes: Map<HTMLDivElement, HtmlNodeOptions>;
@@ -164,6 +187,7 @@ const Components = (props: ComponentsProps) => {
164187
signInModal: null,
165188
signUpModal: null,
166189
userProfileModal: null,
190+
userVerificationModal: null,
167191
organizationProfileModal: null,
168192
createOrganizationModal: null,
169193
nodes: new Map(),
@@ -175,6 +199,7 @@ const Components = (props: ComponentsProps) => {
175199
signInModal,
176200
signUpModal,
177201
userProfileModal,
202+
userVerificationModal,
178203
organizationProfileModal,
179204
createOrganizationModal,
180205
nodes,
@@ -297,6 +322,23 @@ const Components = (props: ComponentsProps) => {
297322
</LazyModalRenderer>
298323
);
299324

325+
const mountedUserVerificationModal = (
326+
<LazyModalRenderer
327+
globalAppearance={state.appearance}
328+
appearanceKey={'userVerification'}
329+
componentAppearance={userVerificationModal?.appearance}
330+
flowName={'userVerification'}
331+
onClose={() => componentsControls.closeModal('userVerification')}
332+
onExternalNavigate={() => componentsControls.closeModal('userVerification')}
333+
startPath={buildVirtualRouterUrl({ base: '/user-verification', path: urlStateParam?.path })}
334+
componentName={'UserVerificationModal'}
335+
modalContainerSx={{ alignItems: 'center' }}
336+
modalContentSx={t => ({ height: `min(${t.sizes.$176}, calc(100% - ${t.sizes.$12}))`, margin: 0 })}
337+
>
338+
<UserVerificationModal {...userVerificationModal} />
339+
</LazyModalRenderer>
340+
);
341+
300342
const mountedOrganizationProfileModal = (
301343
<LazyModalRenderer
302344
globalAppearance={state.appearance}
@@ -359,6 +401,7 @@ const Components = (props: ComponentsProps) => {
359401
{signInModal && mountedSignInModal}
360402
{signUpModal && mountedSignUpModal}
361403
{userProfileModal && mountedUserProfileModal}
404+
{userVerificationModal && mountedUserVerificationModal}
362405
{organizationProfileModal && mountedOrganizationProfileModal}
363406
{createOrganizationModal && mountedCreateOrganizationModal}
364407
{state.impersonationFab && (

packages/clerk-js/src/ui/components/SignIn/AlternativeMethods.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { SignInFactor } from '@clerk/types';
22
import React from 'react';
33

4+
import { useCoreSignIn } from '../../contexts';
45
import type { LocalizationKey } from '../../customizables';
56
import { Button, Col, descriptors, Flex, Flow, localizationKeys } from '../../customizables';
67
import { ArrowBlockButton, BackLink, Card, Divider, Header } from '../../elements';
@@ -33,8 +34,10 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
3334
const { onBackLinkClick, onHavingTroubleClick, onFactorSelected, mode = 'default' } = props;
3435
const card = useCardState();
3536
const resetPasswordFactor = useResetPasswordFactor();
37+
const { supportedFirstFactors } = useCoreSignIn();
3638
const { firstPartyFactors, hasAnyStrategy } = useAlternativeStrategies({
3739
filterOutFactor: props?.currentFactor,
40+
supportedFirstFactors: supportedFirstFactors,
3841
});
3942

4043
const flowPart = determineFlowPart(mode);

packages/clerk-js/src/ui/components/SignIn/SignInFactorOne.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function _SignInFactorOne(): JSX.Element {
3737
const availableFactors = signIn.supportedFirstFactors;
3838
const router = useRouter();
3939
const card = useCardState();
40+
const { supportedFirstFactors } = useCoreSignIn();
4041

4142
const lastPreparedFactorKeyRef = React.useRef('');
4243
const [{ currentFactor }, setFactor] = React.useState<{
@@ -49,6 +50,7 @@ export function _SignInFactorOne(): JSX.Element {
4950

5051
const { hasAnyStrategy } = useAlternativeStrategies({
5152
filterOutFactor: currentFactor,
53+
supportedFirstFactors,
5254
});
5355

5456
const [showAllStrategies, setShowAllStrategies] = React.useState<boolean>(
@@ -123,7 +125,6 @@ export function _SignInFactorOne(): JSX.Element {
123125
}
124126

125127
switch (currentFactor?.strategy) {
126-
// @ts-ignore
127128
case 'passkey':
128129
return (
129130
<SignInFactorOnePasskey

0 commit comments

Comments
 (0)