Skip to content

Commit e513333

Browse files
authored
feat(clerk-js,shared): Refactor useReverification to support custom UIs (#5396)
1 parent 9dfaade commit e513333

File tree

30 files changed

+221
-146
lines changed

30 files changed

+221
-146
lines changed

.changeset/pretty-months-wonder.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@clerk/react-router': patch
3+
'@clerk/clerk-js': patch
4+
'@clerk/nextjs': patch
5+
'@clerk/clerk-react': patch
6+
'@clerk/remix': patch
7+
---
8+
9+
Export `isReverificationCancelledError` error helper

.changeset/tricky-deers-know.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
'@clerk/shared': minor
3+
'@clerk/clerk-js': patch
4+
---
5+
6+
This introducing changes to `useReverification`, the changes include removing the array and returning the fetcher directly and also the dropping the options `throwOnCancel` and `onCancel` in favour of always throwing the cancellation error.
7+
8+
```tsx {{ filename: 'src/components/MyButton.tsx' }}
9+
import { useReverification } from '@clerk/clerk-react'
10+
import { isReverificationCancelledError } from '@clerk/clerk-react/error'
11+
12+
type MyData = {
13+
balance: number
14+
}
15+
16+
export function MyButton() {
17+
const fetchMyData = () => fetch('/api/balance').then(res=> res.json() as Promise<MyData>)
18+
const enhancedFetcher = useReverification(fetchMyData);
19+
20+
const handleClick = async () => {
21+
try {
22+
const myData = await enhancedFetcher()
23+
// ^ is typed as `MyData`
24+
} catch (e) {
25+
// Handle error returned from the fetcher here
26+
// You can also handle cancellation with the following
27+
if (isReverificationCancelledError(err)) {
28+
// Handle the cancellation error here
29+
}
30+
}
31+
}
32+
33+
return <button onClick={handleClick}>Update User</button>
34+
}
35+
```
36+
37+
These changes are also adding a new handler in options called `onNeedsReverification`, which can be used to create a custom UI
38+
to handle re-verification flow. When the handler is passed the default UI our AIO components provide will not be triggered so you will have to create and handle the re-verification process.
39+
40+
41+
```tsx {{ filename: 'src/components/MyButtonCustom.tsx' }}
42+
import { useReverification } from '@clerk/clerk-react'
43+
import { isReverificationCancelledError } from '@clerk/clerk-react/error'
44+
45+
type MyData = {
46+
balance: number
47+
}
48+
49+
export function MyButton() {
50+
const fetchMyData = () => fetch('/api/balance').then(res=> res.json() as Promise<MyData>)
51+
const enhancedFetcher = useReverification(fetchMyData, {
52+
onNeedsReverification: ({ complete, cancel, level }) => {
53+
// e.g open a modal here and handle the re-verification flow
54+
}
55+
})
56+
57+
const handleClick = async () => {
58+
try {
59+
const myData = await enhancedFetcher()
60+
// ^ is typed as `MyData`
61+
} catch (e) {
62+
// Handle error returned from the fetcher here
63+
64+
// You can also handle cancellation with the following
65+
if (isReverificationCancelledError(err)) {
66+
// Handle the cancellation error here
67+
}
68+
}
69+
}
70+
71+
return <button onClick={handleClick}>Update User</button>
72+
}
73+
```

integration/templates/next-app-router/src/app/(reverification)/action-with-use-reverification/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useReverification } from '@clerk/nextjs';
44
import { logUserIdActionReverification } from '@/app/(reverification)/actions';
55

66
function Page() {
7-
const [logUserWithReverification] = useReverification(logUserIdActionReverification);
7+
const logUserWithReverification = useReverification(logUserIdActionReverification);
88
const [pending, startTransition] = useTransition();
99
const [res, setRes] = useState(null);
1010

integration/tests/reverification.test.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withReverification] })(
144144
await u.page.getByRole('button', { name: /^add$/i }).click();
145145

146146
await u.po.userVerification.waitForMounted();
147-
148147
await u.po.userVerification.closeReverificationModal();
149-
150148
await u.po.userVerification.waitForClosed();
151-
await u.po.userProfile.enterTestOtpCode();
152149

153150
await expect(u.page.locator('.cl-profileSectionItem__emailAddresses')).not.toContainText(newFakeEmail);
154151
});

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"files": [
33
{ "path": "./dist/clerk.js", "maxSize": "580kB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "78.5kB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "78.6kB" },
55
{ "path": "./dist/clerk.headless.js", "maxSize": "55KB" },
66
{ "path": "./dist/ui-common*.js", "maxSize": "94KB" },
77
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },

packages/clerk-js/src/ui/common/RemoveResourceForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type RemoveFormProps = FormProps & {
1717
export const RemoveResourceForm = withCardStateProvider((props: RemoveFormProps) => {
1818
const { title, messageLine1, messageLine2, deleteResource, onSuccess, onReset } = props;
1919
const card = useCardState();
20-
const [deleteWithReverification] = useReverification(deleteResource);
20+
const deleteWithReverification = useReverification(deleteResource);
2121

2222
const handleSubmit = async () => {
2323
try {

packages/clerk-js/src/ui/components/UserProfile/ActiveDevicesSection.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { FullHeightLoader, ProfileSection, ThreeDotsMenu } from '../../elements'
66
import { useFetch, useLoadingStatus } from '../../hooks';
77
import { DeviceLaptop, DeviceMobile } from '../../icons';
88
import { mqu, type PropsOfComponent } from '../../styledSystem';
9-
import { getRelativeToNowDateKey } from '../../utils';
9+
import { getRelativeToNowDateKey, handleError } from '../../utils';
1010
import { currentSessionFirst } from './utils';
1111

1212
export const ActiveDevicesSection = () => {
@@ -54,19 +54,16 @@ const isSignedInStatus = (status: string): status is SignedInSessionResource['st
5454
const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) => {
5555
const isCurrent = useSession().session?.id === session.id;
5656
const status = useLoadingStatus();
57-
const [revokeSession] = useReverification(session.revoke.bind(session));
57+
const revokeSession = useReverification(session.revoke.bind(session));
5858

5959
const revoke = async () => {
6060
if (isCurrent || !session) {
6161
return;
6262
}
6363
status.setLoading();
64-
return (
65-
revokeSession()
66-
// TODO-STEPUP: Properly handler the response with a setCardError
67-
.catch(() => {})
68-
.finally(() => status.setIdle())
69-
);
64+
return revokeSession()
65+
.catch(err => handleError(err, [], status.setError))
66+
.finally(() => status.setIdle());
7067
};
7168

7269
return (
@@ -76,15 +73,14 @@ const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) =>
7673
elementId={isCurrent ? descriptors.activeDeviceListItem.setId('current') : undefined}
7774
sx={{
7875
alignItems: 'flex-start',
76+
opacity: status.isLoading ? 0.5 : 1,
7977
}}
78+
isDisabled={status.isLoading}
8079
>
81-
{status.isLoading && <FullHeightLoader />}
82-
{!status.isLoading && (
83-
<>
84-
<DeviceInfo session={session} />
85-
{!isCurrent && <ActiveDeviceMenu revoke={revoke} />}
86-
</>
87-
)}
80+
<>
81+
<DeviceInfo session={session} />
82+
{!isCurrent && <ActiveDeviceMenu revoke={revoke} />}
83+
</>
8884
</ProfileSection.Item>
8985
);
9086
};

packages/clerk-js/src/ui/components/UserProfile/AddAuthenticatorApp.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const AddAuthenticatorApp = withCardStateProvider((props: AddAuthenticato
2828
const { title, onSuccess, onReset } = props;
2929
const { user } = useUser();
3030
const card = useCardState();
31-
const [createTOTP] = useReverification(() => user?.createTOTP());
31+
const createTOTP = useReverification(() => user?.createTOTP());
3232
const { close } = useActionContext();
3333
const [totp, setTOTP] = React.useState<TOTPResource | undefined>(undefined);
3434
const [displayFormat, setDisplayFormat] = React.useState<DisplayFormat>('qr');

packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const ConnectMenuButton = (props: { strategy: OAuthStrategy; onClick?: () => voi
1919
const { additionalOAuthScopes, componentName, mode } = useUserProfileContext();
2020
const isModal = mode === 'modal';
2121

22-
const [createExternalAccount] = useReverification(() => {
22+
const createExternalAccount = useReverification(() => {
2323
const socialProvider = strategy.replace('oauth_', '') as OAuthProvider;
2424
const redirectUrl = isModal
2525
? appendModalState({ url: window.location.href, componentName, socialProvider: socialProvider })

packages/clerk-js/src/ui/components/UserProfile/ConnectedAccountsSection.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) =>
104104
})
105105
: window.location.href;
106106

107-
const [createExternalAccount] = useReverification(() =>
107+
const createExternalAccount = useReverification(() =>
108108
user?.createExternalAccount({
109109
strategy: account.verification!.strategy as OAuthStrategy,
110110
redirectUrl,

0 commit comments

Comments
 (0)