Skip to content
Merged
9 changes: 9 additions & 0 deletions .changeset/pretty-months-wonder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@clerk/react-router': patch
'@clerk/clerk-js': patch
'@clerk/nextjs': patch
'@clerk/clerk-react': patch
'@clerk/remix': patch
---

Export `isReverificationCancelledError` error helper
73 changes: 73 additions & 0 deletions .changeset/tricky-deers-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
'@clerk/shared': minor
'@clerk/clerk-js': patch
---

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.

```tsx {{ filename: 'src/components/MyButton.tsx' }}
import { useReverification } from '@clerk/clerk-react'
import { isReverificationCancelledError } from '@clerk/clerk-react/error'

type MyData = {
balance: number
}

export function MyButton() {
const fetchMyData = () => fetch('/api/balance').then(res=> res.json() as Promise<MyData>)
const enhancedFetcher = useReverification(fetchMyData);

const handleClick = async () => {
try {
const myData = await enhancedFetcher()
// ^ is typed as `MyData`
} catch (e) {
// Handle error returned from the fetcher here
// You can also handle cancellation with the following
if (isReverificationCancelledError(err)) {
// Handle the cancellation error here
}
}
}

return <button onClick={handleClick}>Update User</button>
}
```

These changes are also adding a new handler in options called `onNeedsReverification`, which can be used to create a custom UI
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.


```tsx {{ filename: 'src/components/MyButtonCustom.tsx' }}
import { useReverification } from '@clerk/clerk-react'
import { isReverificationCancelledError } from '@clerk/clerk-react/error'

type MyData = {
balance: number
}

export function MyButton() {
const fetchMyData = () => fetch('/api/balance').then(res=> res.json() as Promise<MyData>)
const enhancedFetcher = useReverification(fetchMyData, {
onNeedsReverification: ({ complete, cancel, level }) => {
// e.g open a modal here and handle the re-verification flow
}
})

const handleClick = async () => {
try {
const myData = await enhancedFetcher()
// ^ is typed as `MyData`
} catch (e) {
// Handle error returned from the fetcher here

// You can also handle cancellation with the following
if (isReverificationCancelledError(err)) {
// Handle the cancellation error here
}
}
}

return <button onClick={handleClick}>Update User</button>
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useReverification } from '@clerk/nextjs';
import { logUserIdActionReverification } from '@/app/(reverification)/actions';

function Page() {
const [logUserWithReverification] = useReverification(logUserIdActionReverification);
const logUserWithReverification = useReverification(logUserIdActionReverification);
const [pending, startTransition] = useTransition();
const [res, setRes] = useState(null);

Expand Down
3 changes: 0 additions & 3 deletions integration/tests/reverification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withReverification] })(
await u.page.getByRole('button', { name: /^add$/i }).click();

await u.po.userVerification.waitForMounted();

await u.po.userVerification.closeReverificationModal();

await u.po.userVerification.waitForClosed();
await u.po.userProfile.enterTestOtpCode();

await expect(u.page.locator('.cl-profileSectionItem__emailAddresses')).not.toContainText(newFakeEmail);
});
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "580kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "78.5kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "78.6kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "94KB" },
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/common/RemoveResourceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type RemoveFormProps = FormProps & {
export const RemoveResourceForm = withCardStateProvider((props: RemoveFormProps) => {
const { title, messageLine1, messageLine2, deleteResource, onSuccess, onReset } = props;
const card = useCardState();
const [deleteWithReverification] = useReverification(deleteResource);
const deleteWithReverification = useReverification(deleteResource);

const handleSubmit = async () => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FullHeightLoader, ProfileSection, ThreeDotsMenu } from '../../elements'
import { useFetch, useLoadingStatus } from '../../hooks';
import { DeviceLaptop, DeviceMobile } from '../../icons';
import { mqu, type PropsOfComponent } from '../../styledSystem';
import { getRelativeToNowDateKey } from '../../utils';
import { getRelativeToNowDateKey, handleError } from '../../utils';
import { currentSessionFirst } from './utils';

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

const revoke = async () => {
if (isCurrent || !session) {
return;
}
status.setLoading();
return (
revokeSession()
// TODO-STEPUP: Properly handler the response with a setCardError
.catch(() => {})
.finally(() => status.setIdle())
);
return revokeSession()
.catch(err => handleError(err, [], status.setError))
.finally(() => status.setIdle());
};

return (
Expand All @@ -76,15 +73,14 @@ const DeviceItem = ({ session }: { session: SessionWithActivitiesResource }) =>
elementId={isCurrent ? descriptors.activeDeviceListItem.setId('current') : undefined}
sx={{
alignItems: 'flex-start',
opacity: status.isLoading ? 0.5 : 1,
}}
isDisabled={status.isLoading}
>
{status.isLoading && <FullHeightLoader />}
{!status.isLoading && (
<>
<DeviceInfo session={session} />
{!isCurrent && <ActiveDeviceMenu revoke={revoke} />}
</>
)}
<>
<DeviceInfo session={session} />
{!isCurrent && <ActiveDeviceMenu revoke={revoke} />}
</>
</ProfileSection.Item>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const AddAuthenticatorApp = withCardStateProvider((props: AddAuthenticato
const { title, onSuccess, onReset } = props;
const { user } = useUser();
const card = useCardState();
const [createTOTP] = useReverification(() => user?.createTOTP());
const createTOTP = useReverification(() => user?.createTOTP());
const { close } = useActionContext();
const [totp, setTOTP] = React.useState<TOTPResource | undefined>(undefined);
const [displayFormat, setDisplayFormat] = React.useState<DisplayFormat>('qr');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const ConnectMenuButton = (props: { strategy: OAuthStrategy; onClick?: () => voi
const { additionalOAuthScopes, componentName, mode } = useUserProfileContext();
const isModal = mode === 'modal';

const [createExternalAccount] = useReverification(() => {
const createExternalAccount = useReverification(() => {
const socialProvider = strategy.replace('oauth_', '') as OAuthProvider;
const redirectUrl = isModal
? appendModalState({ url: window.location.href, componentName, socialProvider: socialProvider })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const ConnectedAccount = ({ account }: { account: ExternalAccountResource }) =>
})
: window.location.href;

const [createExternalAccount] = useReverification(() =>
const createExternalAccount = useReverification(() =>
user?.createExternalAccount({
strategy: account.verification!.strategy as OAuthStrategy,
redirectUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const DeleteUserForm = withCardStateProvider((props: DeleteUserFormProps)
const { t } = useLocalizations();
const { otherSessions } = useMultipleSessions({ user });
const { setActive } = useClerk();
const [deleteUserWithReverification] = useReverification(() => user?.delete());
const deleteUserWithReverification = useReverification(() => user?.delete());

const confirmationField = useFormControl('deleteConfirmation', '', {
type: 'text',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const EmailForm = withCardStateProvider((props: EmailFormProps) => {
const { user } = useUser();
const environment = useEnvironment();

const [createEmailAddress] = useReverification((email: string) => user?.createEmailAddress({ email }));
const createEmailAddress = useReverification((email: string) => user?.createEmailAddress({ email }));

const emailAddressRef = React.useRef<EmailAddressResource | undefined>(user?.emailAddresses.find(a => a.id === id));
const strategy = getEmailAddressVerificationStrategy(emailAddressRef.current, environment);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ const EmailMenu = ({ email }: { email: EmailAddressResource }) => {
const emailId = email.id;
const isPrimary = user?.primaryEmailAddressId === emailId;
const isVerified = email.verification.status === 'verified';
const [setPrimary] = useReverification(() => {
const setPrimary = useReverification(() => {
return user?.update({ primaryEmailAddressId: emailId });
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isReverificationCancelledError } from '@clerk/shared/error';
import { useReverification, useUser } from '@clerk/shared/react';
import type { BackupCodeResource } from '@clerk/types';
import React from 'react';
Expand All @@ -16,10 +17,10 @@ import { MfaBackupCodeList } from './MfaBackupCodeList';

type MfaBackupCodeCreateFormProps = FormProps;
export const MfaBackupCodeCreateForm = withCardStateProvider((props: MfaBackupCodeCreateFormProps) => {
const { onSuccess } = props;
const { onSuccess, onReset } = props;
const { user } = useUser();
const card = useCardState();
const [createBackupCode] = useReverification(() => user?.createBackupCode());
const createBackupCode = useReverification(() => user?.createBackupCode());
const [backupCode, setBackupCode] = React.useState<BackupCodeResource | undefined>(undefined);

React.useEffect(() => {
Expand All @@ -29,7 +30,13 @@ export const MfaBackupCodeCreateForm = withCardStateProvider((props: MfaBackupCo

void createBackupCode()
.then(backupCode => setBackupCode(backupCode))
.catch(err => handleError(err, [], card.setError));
.catch(err => {
if (isReverificationCancelledError(err)) {
return onReset();
}

handleError(err, [], card.setError);
});
}, []);

if (card.error) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const EnableMFAButtonForPhone = (
) => {
const { phone, onSuccess, onUnverifiedPhoneClick, resourceRef } = props;
const card = useCardState();
const [setReservedForSecondFactor] = useReverification(() => phone.setReservedForSecondFactor({ reserved: true }));
const setReservedForSecondFactor = useReverification(() => phone.setReservedForSecondFactor({ reserved: true }));

const { country } = getCountryFromPhoneString(phone.phoneNumber);
const formattedPhone = stringToFormattedPhoneString(phone.phoneNumber);
Expand Down Expand Up @@ -134,7 +134,7 @@ export const MFAVerifyPhone = (props: MFAVerifyPhoneProps) => {
const { title, onSuccess, resourceRef, onReset } = props;
const card = useCardState();
const phone = resourceRef.current;
const [setReservedForSecondFactor] = useReverification(() => phone?.setReservedForSecondFactor({ reserved: true }));
const setReservedForSecondFactor = useReverification(() => phone?.setReservedForSecondFactor({ reserved: true }));

const enableMfa = async () => {
card.setLoading(phone?.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ const AddPasskeyButton = ({ onClick }: { onClick?: () => void }) => {
const card = useCardState();
const { isSatellite } = useClerk();
const { user } = useUser();
const [createPasskey] = useReverification(() => user?.createPasskey());
const createPasskey = useReverification(() => user?.createPasskey());

const handleCreatePasskey = async () => {
onClick?.();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type PasswordFormProps = FormProps;
export const PasswordForm = withCardStateProvider((props: PasswordFormProps) => {
const { onSuccess, onReset } = props;
const { user } = useUser();
const [updatePasswordWithReverification] = useReverification(
const updatePasswordWithReverification = useReverification(
(user: UserResource, opts: Parameters<UserResource['updatePassword']>) => user.updatePassword(...opts),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const AddPhone = (props: AddPhoneProps) => {
const { title, onSuccess, onReset, onUseExistingNumberClick, resourceRef } = props;
const card = useCardState();
const { user } = useUser();
const [createPhoneNumber] = useReverification(
const createPhoneNumber = useReverification(
(user: UserResource, opt: Parameters<UserResource['createPhoneNumber']>[0]) => user.createPhoneNumber(opt),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const UsernameForm = withCardStateProvider((props: UsernameFormProps) =>
const { onSuccess, onReset } = props;
const { user } = useUser();

const [updateUsername] = useReverification((username: string) => user?.update({ username }));
const updateUsername = useReverification((username: string) => user?.update({ username }));

const { userSettings } = useEnvironment();
const card = useCardState();
Expand Down
13 changes: 8 additions & 5 deletions packages/clerk-js/src/ui/components/UserProfile/Web3Form.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useUser } from '@clerk/shared/react';
import { useReverification, useUser } from '@clerk/shared/react';
import type { Web3Provider, Web3Strategy } from '@clerk/types';

import { generateWeb3Signature, getWeb3Identifier } from '../../../utils/web3';
Expand All @@ -17,6 +17,9 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
const unconnectedStrategies = enabledStrategies.filter(strategy => {
return !connectedStrategies.includes(strategy);
});
const createWeb3Wallet = useReverification((identifier: string) =>
user?.createWeb3Wallet({ web3Wallet: identifier }),
);

const connect = async (strategy: Web3Strategy) => {
const provider = strategy.replace('web3_', '').replace('_signature', '') as Web3Provider;
Expand All @@ -30,11 +33,11 @@ export const AddWeb3WalletActionMenu = withCardStateProvider(({ onClick }: { onC
throw new Error('user is not defined');
}

let web3Wallet = await user.createWeb3Wallet({ web3Wallet: identifier });
web3Wallet = await web3Wallet.prepareVerification({ strategy });
const message = web3Wallet.verification.message as string;
let web3Wallet = await createWeb3Wallet(identifier);
web3Wallet = await web3Wallet?.prepareVerification({ strategy });
const message = web3Wallet?.verification.message as string;
const signature = await generateWeb3Signature({ identifier, nonce: message, provider });
await web3Wallet.attemptVerification({ signature });
await web3Wallet?.attemptVerification({ signature });
card.setIdle();
} catch (err) {
card.setIdle();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const Web3WalletMenu = ({ walletId }: { walletId: string }) => {
const { open } = useActionContext();
const { user } = useUser();
const isPrimary = user?.primaryWeb3WalletId === walletId;
const [setPrimary] = useReverification(() => {
const setPrimary = useReverification(() => {
return user?.update({ primaryWeb3WalletId: walletId });
});

Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/client-boundary/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export {
isEmailLinkError,
isKnownError,
isMetamaskError,
isReverificationCancelledError,
EmailLinkErrorCode,
EmailLinkErrorCodeStatus,
} from '@clerk/clerk-react/errors';
Expand Down
1 change: 1 addition & 0 deletions packages/nextjs/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
isClerkRuntimeError,
isEmailLinkError,
isKnownError,
isReverificationCancelledError,
isMetamaskError,
EmailLinkErrorCode,
EmailLinkErrorCodeStatus,
Expand Down
1 change: 1 addition & 0 deletions packages/react-router/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
isEmailLinkError,
isKnownError,
isMetamaskError,
isReverificationCancelledError,
EmailLinkErrorCode,
EmailLinkErrorCodeStatus,
} from '@clerk/clerk-react/errors';
1 change: 1 addition & 0 deletions packages/react/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export {
isEmailLinkError,
isKnownError,
isMetamaskError,
isReverificationCancelledError,
EmailLinkErrorCode,
EmailLinkErrorCodeStatus,
} from '@clerk/shared/error';
1 change: 1 addition & 0 deletions packages/remix/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export {
isEmailLinkError,
isKnownError,
isMetamaskError,
isReverificationCancelledError,
EmailLinkErrorCode,
EmailLinkErrorCodeStatus,
} from '@clerk/clerk-react/errors';
Loading