Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(clerk-js,shared): Refactor useReverification to support custom UIs #5396

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