Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
eb38825
Introduce UserVerification
panteliselef Jul 22, 2024
70faa65
Expose mounted UserVerification
panteliselef Jul 23, 2024
0ac4c86
Throttle loading to avoid flashes and navigate after verification
panteliselef Jul 23, 2024
ecb1afc
respect status of SessionVerification
panteliselef Jul 29, 2024
ac04692
WIP UV respect FAPI
panteliselef Aug 22, 2024
0c520cd
Improve types
panteliselef Aug 23, 2024
80165e5
Prepare and attempt second factor
panteliselef Aug 23, 2024
515e1c9
fix on complete first factor
panteliselef Aug 23, 2024
a7c361e
fix build issue
panteliselef Aug 24, 2024
1545760
changeset
panteliselef Aug 26, 2024
ee6706a
mark apis as experimental
panteliselef Aug 26, 2024
d29d30c
chore(clerk-js): Remove sessionId parameter from all verify session m…
panteliselef Aug 27, 2024
02aea2a
cleanup handling responses and set the cached data after each response
panteliselef Aug 27, 2024
f79953a
add tests for UV first factor
panteliselef Aug 27, 2024
5d7fc48
fix typo
panteliselef Aug 27, 2024
8700692
Add tests for 2fa
panteliselef Aug 27, 2024
17431b3
comment for reviewers
panteliselef Aug 27, 2024
df685a2
clean up duplicate code
panteliselef Aug 27, 2024
71bf279
clean up and add comments
panteliselef Aug 27, 2024
07aa94c
add bundlewatch
panteliselef Aug 27, 2024
8cb52f6
update localizations for user verification
panteliselef Aug 27, 2024
c0fa2c3
remove noop code
panteliselef Aug 27, 2024
f471aba
fix localization issues
panteliselef Aug 27, 2024
61360c7
duplicate HavingTrouble.tsx under UV
panteliselef Aug 27, 2024
6e2ebc7
fix nullable types
panteliselef Aug 28, 2024
a130256
remove references to sign in
panteliselef Aug 28, 2024
0e71ab1
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 28, 2024
b08ae51
add changesets
panteliselef Aug 28, 2024
22129a0
formatting
panteliselef Aug 28, 2024
f6059ff
address pr reviews
panteliselef Aug 28, 2024
1a9dc66
update changesets
panteliselef Aug 28, 2024
d7fce4f
update localization texts
panteliselef Aug 29, 2024
0e5d8d2
Merge branch 'refs/heads/main' into elef/user-607-userverification-ui…
panteliselef Aug 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-zoos-sort.md
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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,
AuthenticateWithGoogleOneTapParams,
AuthenticateWithMetamaskParams,
Expand DownExpand Up@@ -342,6 +344,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 DownExpand Up@@ -373,6 +376,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 DownExpand Up@@ -483,6 +506,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 DownExpand Up@@ -854,6 +909,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 numberDiff line numberDiff 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 DownExpand Up@@ -28,6 +29,7 @@ import {
SignInModal,
SignUpModal,
UserProfileModal,
UserVerificationModal,
} from './lazyModules/components';
import {
LazyComponentRenderer,
Expand DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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