Skip to content

Commit

Permalink
feat(clerk-js,types,nextjs,localizations,clerk-react): Introduce User…
Browse files Browse the repository at this point in the history
…Verification as experimental (#4016)
  • Loading branch information
panteliselef authored Aug 29, 2024
1 parent 11c3c41 commit afad9af
Show file tree
Hide file tree
Showing 45 changed files with 1,785 additions and 32 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/localizations": minor
---

Add localization keys for `<__experimental_UserVerification />` (experimental feature).
7 changes: 7 additions & 0 deletions .changeset/fifty-ravens-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@clerk/chrome-extension": minor
"@clerk/nextjs": minor
"@clerk/clerk-react": minor
---

Add `<__experimental_UserVerification />` component. This is an experimental feature and breaking changes can occur until it's marked as stable.
12 changes: 12 additions & 0 deletions .changeset/healthy-colts-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@clerk/clerk-js": minor
---

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.

New methods have been added:

- `__experimental_openUserVerification()`
- `__experimental_closeUserVerification()`
- `__experimental_mountUserVerification(targetNode: HTMLDivElement)`
- `__experimental_unmountUserVerification(targetNode: HTMLDivElement)`
13 changes: 13 additions & 0 deletions .changeset/proud-dryers-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@clerk/types": minor
---

Add types for newly introduced `<__experimental_UserVerification />` component (experimental feature). New types:

- `Appearance` has a new `userVerification` property
- `__experimental_UserVerificationProps` and `__experimental_UserVerificationModalProps`
- `__experimental_openUserVerification` method under the `Clerk` interface
- `__experimental_closeUserVerification` method under the `Clerk` interface
- `__experimental_mountUserVerification` method under the `Clerk` interface
- `__experimental_unmountUserVerification` method under the `Clerk` interface
- `__experimental_userVerification` property under `LocalizationResource`
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ exports[`public exports should not include a breaking change 1`] = `
"SignedOut",
"UserButton",
"UserProfile",
"__experimental_UserVerification",
"useAuth",
"useClerk",
"useEmailLink",
Expand Down
1 change: 1 addition & 0 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
{ "path": "./dist/signup*.js", "maxSize": "10KB" },
{ "path": "./dist/userbutton*.js", "maxSize": "5KB" },
{ "path": "./dist/userprofile*.js", "maxSize": "15KB" },
{ "path": "./dist/userverification*.js", "maxSize": "5KB" },
{ "path": "./dist/onetap*.js", "maxSize": "1KB" }
]
}
56 changes: 56 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { logger } from '@clerk/shared/logger';
import { eventPrebuiltComponentMounted, TelemetryCollector } from '@clerk/shared/telemetry';
import type {
__experimental_UserVerificationModalProps,
__experimental_UserVerificationProps,
ActiveSessionResource,
AuthenticateWithCoinbaseParams,
AuthenticateWithGoogleOneTapParams,
Expand Down Expand Up @@ -348,6 +350,7 @@ export class Clerk implements ClerkInterface {
};

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

public __experimental_openUserVerification = (props?: __experimental_UserVerificationModalProps): void => {
this.assertComponentsReady(this.#componentControls);
if (noUserExists(this)) {
if (this.#instanceType === 'development') {
throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, {
code: 'cannot_render_user_missing',
});
}
return;
}
void this.#componentControls
.ensureMounted({ preloadHint: 'UserVerification' })
.then(controls => controls.openModal('userVerification', props || {}));
};

public __experimental_closeUserVerification = (): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('userVerification'));
};

public openSignUp = (props?: SignUpProps): void => {
this.assertComponentsReady(this.#componentControls);
if (sessionExistsAndSingleSessionModeEnabled(this, this.environment)) {
Expand Down Expand Up @@ -489,6 +512,38 @@ export class Clerk implements ClerkInterface {
);
};

public __experimental_mountUserVerification = (
node: HTMLDivElement,
props?: __experimental_UserVerificationProps,
): void => {
this.assertComponentsReady(this.#componentControls);
if (noUserExists(this)) {
if (this.#instanceType === 'development') {
throw new ClerkRuntimeError(warnings.cannotOpenUserProfile, {
code: 'cannot_render_user_missing',
});
}
return;
}
void this.#componentControls.ensureMounted({ preloadHint: 'UserVerification' }).then(controls =>
controls.mountComponent({
name: 'UserVerification',
appearanceKey: 'userVerification',
node,
props,
}),
);
};

public __experimental_unmountUserVerification = (node: HTMLDivElement): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted().then(controls =>
controls.unmountComponent({
node,
}),
);
};

public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls =>
Expand Down Expand Up @@ -860,6 +915,7 @@ export class Clerk implements ClerkInterface {

return this.#authService.decorateUrlWithDevBrowserToken(toURL).href;
}

public buildSignInUrl(options?: SignInRedirectOptions): string {
return this.#buildUrl(
'signInUrl',
Expand Down
49 changes: 46 additions & 3 deletions packages/clerk-js/src/ui/Components.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createDeferredPromise } from '@clerk/shared';
import { useSafeLayoutEffect } from '@clerk/shared/react';
import type {
__experimental_UserVerificationProps,
Appearance,
Clerk,
ClerkOptions,
Expand Down Expand Up @@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand Down Expand Up @@ -55,13 +57,33 @@ export type ComponentControls = {
props?: unknown;
}) => void;
openModal: <
T extends 'googleOneTap' | 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization',
T extends
| 'googleOneTap'
| 'signIn'
| 'signUp'
| 'userProfile'
| 'organizationProfile'
| 'createOrganization'
| 'userVerification',
>(
modal: T,
props: T extends 'signIn' ? SignInProps : T extends 'signUp' ? SignUpProps : UserProfileProps,
props: T extends 'signIn'
? SignInProps
: T extends 'signUp'
? SignUpProps
: T extends 'userVerification'
? __experimental_UserVerificationProps
: UserProfileProps,
) => void;
closeModal: (
modal: 'googleOneTap' | 'signIn' | 'signUp' | 'userProfile' | 'organizationProfile' | 'createOrganization',
modal:
| 'googleOneTap'
| 'signIn'
| 'signUp'
| 'userProfile'
| 'organizationProfile'
| 'createOrganization'
| 'userVerification',
) => void;
// Special case, as the impersonation fab mounts automatically
mountImpersonationFab: () => void;
Expand All @@ -88,6 +110,7 @@ interface ComponentsState {
signInModal: null | SignInProps;
signUpModal: null | SignUpProps;
userProfileModal: null | UserProfileProps;
userVerificationModal: null | __experimental_UserVerificationProps;
organizationProfileModal: null | OrganizationProfileProps;
createOrganizationModal: null | CreateOrganizationProps;
nodes: Map<HTMLDivElement, HtmlNodeOptions>;
Expand Down Expand Up @@ -164,6 +187,7 @@ const Components = (props: ComponentsProps) => {
signInModal: null,
signUpModal: null,
userProfileModal: null,
userVerificationModal: null,
organizationProfileModal: null,
createOrganizationModal: null,
nodes: new Map(),
Expand All @@ -175,6 +199,7 @@ const Components = (props: ComponentsProps) => {
signInModal,
signUpModal,
userProfileModal,
userVerificationModal,
organizationProfileModal,
createOrganizationModal,
nodes,
Expand Down Expand Up @@ -297,6 +322,23 @@ const Components = (props: ComponentsProps) => {
</LazyModalRenderer>
);

const mountedUserVerificationModal = (
<LazyModalRenderer
globalAppearance={state.appearance}
appearanceKey={'userVerification'}
componentAppearance={userVerificationModal?.appearance}
flowName={'userVerification'}
onClose={() => componentsControls.closeModal('userVerification')}
onExternalNavigate={() => componentsControls.closeModal('userVerification')}
startPath={buildVirtualRouterUrl({ base: '/user-verification', path: urlStateParam?.path })}
componentName={'UserVerificationModal'}
modalContainerSx={{ alignItems: 'center' }}
modalContentSx={t => ({ height: `min(${t.sizes.$176}, calc(100% - ${t.sizes.$12}))`, margin: 0 })}
>
<UserVerificationModal {...userVerificationModal} />
</LazyModalRenderer>
);

const mountedOrganizationProfileModal = (
<LazyModalRenderer
globalAppearance={state.appearance}
Expand Down Expand Up @@ -359,6 +401,7 @@ const Components = (props: ComponentsProps) => {
{signInModal && mountedSignInModal}
{signUpModal && mountedSignUpModal}
{userProfileModal && mountedUserProfileModal}
{userVerificationModal && mountedUserVerificationModal}
{organizationProfileModal && mountedOrganizationProfileModal}
{createOrganizationModal && mountedCreateOrganizationModal}
{state.impersonationFab && (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SignInFactor } from '@clerk/types';
import React from 'react';

import { useCoreSignIn } from '../../contexts';
import type { LocalizationKey } from '../../customizables';
import { Button, Col, descriptors, Flex, Flow, localizationKeys } from '../../customizables';
import { ArrowBlockButton, BackLink, Card, Divider, Header } from '../../elements';
Expand Down Expand Up @@ -33,8 +34,10 @@ const AlternativeMethodsList = (props: AlternativeMethodListProps) => {
const { onBackLinkClick, onHavingTroubleClick, onFactorSelected, mode = 'default' } = props;
const card = useCardState();
const resetPasswordFactor = useResetPasswordFactor();
const { supportedFirstFactors } = useCoreSignIn();
const { firstPartyFactors, hasAnyStrategy } = useAlternativeStrategies({
filterOutFactor: props?.currentFactor,
supportedFirstFactors: supportedFirstFactors,
});

const flowPart = determineFlowPart(mode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function _SignInFactorOne(): JSX.Element {
const availableFactors = signIn.supportedFirstFactors;
const router = useRouter();
const card = useCardState();
const { supportedFirstFactors } = useCoreSignIn();

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

const { hasAnyStrategy } = useAlternativeStrategies({
filterOutFactor: currentFactor,
supportedFirstFactors,
});

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

switch (currentFactor?.strategy) {
// @ts-ignore
case 'passkey':
return (
<SignInFactorOnePasskey
Expand Down
1 change: 0 additions & 1 deletion packages/clerk-js/src/ui/components/SignIn/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export function determineSalutation(signIn: Partial<SignInResource>): string {
return titleize(signIn.userData?.firstName) || titleize(signIn.userData?.lastName) || signIn?.identifier || '';
}

// @ts-ignore
const localStrategies: SignInStrategy[] = ['passkey', 'email_code', 'password', 'phone_code', 'email_link'];

export function factorHasLocalStrategy(factor: SignInFactor | undefined | null): boolean {
Expand Down
Loading

0 comments on commit afad9af

Please sign in to comment.