diff --git a/.changeset/localize-too-many-unverified-identifications.md b/.changeset/localize-too-many-unverified-identifications.md new file mode 100644 index 00000000000..4be9ad168a0 --- /dev/null +++ b/.changeset/localize-too-many-unverified-identifications.md @@ -0,0 +1,6 @@ +--- +'@clerk/localizations': patch +'@clerk/shared': patch +--- + +Add a localized message for the `too_many_unverified_identifications` error, shown when adding an email address or phone number is blocked by pending verifications or an incomplete passkey setup. diff --git a/.changeset/passkey-abandoned-registration-cleanup.md b/.changeset/passkey-abandoned-registration-cleanup.md new file mode 100644 index 00000000000..39e4683603b --- /dev/null +++ b/.changeset/passkey-abandoned-registration-cleanup.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/ui': patch +--- + +Fix abandoned passkey registrations counting toward the limit on unverified identifications. Cancelling or failing the browser passkey prompt left a pending registration on the account that is hidden from the user's passkey list, so it could silently block adding an email address or phone number until it expired. The pending registration is now removed as soon as the prompt is abandoned. In ``, the "Add a passkey" button also shows a loading state while a registration is in progress, and a failed attempt no longer leaves its error banner on screen after a later attempt succeeds. diff --git a/packages/clerk-js/src/core/resources/Passkey.ts b/packages/clerk-js/src/core/resources/Passkey.ts index f3325604f67..fe2c3b346b0 100644 --- a/packages/clerk-js/src/core/resources/Passkey.ts +++ b/packages/clerk-js/src/core/resources/Passkey.ts @@ -13,6 +13,7 @@ import type { PublicKeyCredentialWithAuthenticatorAttestationResponse, UpdatePasskeyParams, } from '@clerk/shared/types'; +import { noop } from '@clerk/shared/utils'; import { isWebAuthnPlatformAuthenticatorSupported as isWebAuthnPlatformAuthenticatorSupportedOnWindow, isWebAuthnSupported as isWebAuthnSupportedOnWindow, @@ -78,32 +79,43 @@ export class Passkey extends BaseResource implements PasskeyResource { const passkey = await this.create(); - const { verification } = passkey; + let publicKeyCredential: PublicKeyCredentialWithAuthenticatorAttestationResponse; + try { + const { verification } = passkey; - const publicKey = verification?.publicKey; + const publicKey = verification?.publicKey; - // This should never occur, just a fail-safe - if (!publicKey) { - clerkMissingWebAuthnPublicKeyOptions('create'); - } + // This should never occur, just a fail-safe + if (!publicKey) { + clerkMissingWebAuthnPublicKeyOptions('create'); + } - if (publicKey.authenticatorSelection?.authenticatorAttachment === 'platform') { - if (!(await isWebAuthnPlatformAuthenticatorSupported())) { - throw new ClerkWebAuthnError( - 'Registration requires a platform authenticator but the device does not support it.', - { - code: 'passkey_pa_not_supported', - }, - ); + if (publicKey.authenticatorSelection?.authenticatorAttachment === 'platform') { + if (!(await isWebAuthnPlatformAuthenticatorSupported())) { + throw new ClerkWebAuthnError( + 'Registration requires a platform authenticator but the device does not support it.', + { + code: 'passkey_pa_not_supported', + }, + ); + } } - } - // Invoke the WebAuthn create() method. - const { publicKeyCredential, error } = await webAuthnCreateCredential(publicKey); + // Invoke the WebAuthn create() method. + const credential = await webAuthnCreateCredential(publicKey); - if (!publicKeyCredential) { + if (!credential.publicKeyCredential) { + throw credential.error; + } + + publicKeyCredential = credential.publicKeyCredential; + } catch (error) { + if (passkey.id) { + await passkey.delete().catch(noop); + } throw error; } + return this.attemptVerification(passkey.id, publicKeyCredential); } diff --git a/packages/clerk-js/src/core/resources/__tests__/Passkey.test.ts b/packages/clerk-js/src/core/resources/__tests__/Passkey.test.ts new file mode 100644 index 00000000000..37b3cb43a63 --- /dev/null +++ b/packages/clerk-js/src/core/resources/__tests__/Passkey.test.ts @@ -0,0 +1,130 @@ +import { ClerkWebAuthnError } from '@clerk/shared/error'; +import type { PasskeyJSON } from '@clerk/shared/types'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { clerkMock } from '../../../test/core-fixtures'; +import { BaseResource, Passkey } from '../internal'; + +const publicKeyOptions = (authenticatorAttachment?: 'platform' | 'cross-platform') => ({ + rp: { id: 'clerk.com', name: 'Clerk' }, + user: { id: 'dXNlcl8x', name: 'user@clerk.com', displayName: 'user' }, + challenge: 'Y2hhbGxlbmdl', + pubKeyCredParams: [{ type: 'public-key', alg: -7 }], + ...(authenticatorAttachment ? { authenticatorSelection: { authenticatorAttachment } } : {}), +}); + +const passkeyJSON = (authenticatorAttachment?: 'platform' | 'cross-platform') => + ({ + object: 'passkey', + id: 'idn_passkey', + name: 'Chrome on macOS', + last_used_at: null, + created_at: 1717430400000, + updated_at: 1717430400000, + verification: { + object: 'verification', + id: 'ver_1', + status: 'unverified', + strategy: 'passkey', + nonce: JSON.stringify(publicKeyOptions(authenticatorAttachment)), + attempts: 0, + expire_at: 1717431000000, + }, + }) as unknown as PasskeyJSON; + +const cancelledError = new ClerkWebAuthnError('Passkey registration was cancelled or timed out.', { + code: 'passkey_registration_cancelled', +}); + +const successfulCredential = { + type: 'public-key', + id: 'credential', + rawId: new ArrayBuffer(1), + authenticatorAttachment: 'platform', + response: { + clientDataJSON: new ArrayBuffer(1), + attestationObject: new ArrayBuffer(1), + getTransports: () => ['internal'], + }, + getClientExtensionResults: () => ({}), +}; + +const setupClerk = (createPublicCredentials: () => Promise, platformAuthenticatorSupported = true) => { + BaseResource.clerk = clerkMock({ + __internal_isWebAuthnSupported: () => true, + __internal_isWebAuthnPlatformAuthenticatorSupported: () => Promise.resolve(platformAuthenticatorSupported), + __internal_createPublicCredentials: createPublicCredentials, + } as any) as any; +}; + +const deleteCall = { method: 'DELETE', path: '/me/passkeys/idn_passkey' }; + +describe('Passkey', () => { + afterEach(() => { + BaseResource.clerk = null as any; + vi.restoreAllMocks(); + }); + + describe('registerPasskey', () => { + it('deletes the pending passkey when the browser prompt is cancelled', async () => { + setupClerk(() => Promise.resolve({ publicKeyCredential: null, error: cancelledError })); + const fetchMock = vi.fn().mockResolvedValue({ response: passkeyJSON() }); + (BaseResource as any)._fetch = fetchMock; + + await expect(Passkey.registerPasskey()).rejects.toBe(cancelledError); + + expect(fetchMock).toHaveBeenCalledWith(deleteCall); + expect(fetchMock).not.toHaveBeenCalledWith( + expect.objectContaining({ path: '/me/passkeys/idn_passkey/attempt_verification' }), + ); + }); + + it('surfaces the original error when the cleanup itself fails', async () => { + setupClerk(() => Promise.resolve({ publicKeyCredential: null, error: cancelledError })); + const fetchMock = vi.fn().mockImplementation(({ method }) => { + if (method === 'DELETE') { + return Promise.reject(new Error('session_reverification_required')); + } + return Promise.resolve({ response: passkeyJSON() }); + }); + (BaseResource as any)._fetch = fetchMock; + + await expect(Passkey.registerPasskey()).rejects.toBe(cancelledError); + }); + + it('deletes the pending passkey when the device lacks a platform authenticator', async () => { + const createPublicCredentials = vi.fn(); + setupClerk(createPublicCredentials as any, false); + const fetchMock = vi.fn().mockResolvedValue({ response: passkeyJSON('platform') }); + (BaseResource as any)._fetch = fetchMock; + + await expect(Passkey.registerPasskey()).rejects.toMatchObject({ code: 'passkey_pa_not_supported' }); + + expect(createPublicCredentials).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledWith(deleteCall); + }); + + it('does not attempt a delete when the created passkey has no id', async () => { + setupClerk(() => Promise.resolve({ publicKeyCredential: null, error: cancelledError })); + const fetchMock = vi.fn().mockResolvedValue(null); + (BaseResource as any)._fetch = fetchMock; + + await expect(Passkey.registerPasskey()).rejects.toThrow(); + + expect(fetchMock).not.toHaveBeenCalledWith(expect.objectContaining({ method: 'DELETE' })); + }); + + it('does not delete anything when registration succeeds', async () => { + setupClerk(() => Promise.resolve({ publicKeyCredential: successfulCredential, error: null })); + const fetchMock = vi.fn().mockResolvedValue({ response: passkeyJSON() }); + (BaseResource as any)._fetch = fetchMock; + + await Passkey.registerPasskey(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ method: 'POST', path: '/me/passkeys/idn_passkey/attempt_verification' }), + ); + expect(fetchMock).not.toHaveBeenCalledWith(expect.objectContaining({ method: 'DELETE' })); + }); + }); +}); diff --git a/packages/localizations/src/ar-SA.ts b/packages/localizations/src/ar-SA.ts index 8ecf259199e..7ab7fe9f682 100644 --- a/packages/localizations/src/ar-SA.ts +++ b/packages/localizations/src/ar-SA.ts @@ -2100,6 +2100,7 @@ export const arSA: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'لقد رفضت طلب التوقيع. يرجى المحاولة مرة أخرى للمتابعة.', web3_solana_signature_generation_failed: 'حدث خطأ أثناء إنشاء التوقيع. يرجى المحاولة مرة أخرى للمتابعة.', diff --git a/packages/localizations/src/be-BY.ts b/packages/localizations/src/be-BY.ts index b91d9fdb8d7..2ca6811c35c 100644 --- a/packages/localizations/src/be-BY.ts +++ b/packages/localizations/src/be-BY.ts @@ -2111,6 +2111,7 @@ export const beBY: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Вы адхілілі запыт на подпіс. Калі ласка, паспрабуйце яшчэ раз, каб працягнуць.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/bg-BG.ts b/packages/localizations/src/bg-BG.ts index e3cae98919a..4b46d8928c5 100644 --- a/packages/localizations/src/bg-BG.ts +++ b/packages/localizations/src/bg-BG.ts @@ -2104,6 +2104,7 @@ export const bgBG: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Отхвърлихте заявката за подпис. Моля, опитайте отново, за да продължите.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/bn-IN.ts b/packages/localizations/src/bn-IN.ts index 43af449869d..18778b578c7 100644 --- a/packages/localizations/src/bn-IN.ts +++ b/packages/localizations/src/bn-IN.ts @@ -2129,6 +2129,7 @@ export const bnIN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'একটি Web3 ওয়ালেট এক্সটেনশন পাওয়া যায়নি। চালিয়ে যেতে দয়া করে একটি ইনস্টল করুন।', web3_signature_request_rejected: 'আপনি সিগনেচার অনুরোধটি প্রত্যাখ্যান করেছেন। চালিয়ে যেতে অনুগ্রহ করে আবার চেষ্টা করুন।', diff --git a/packages/localizations/src/ca-ES.ts b/packages/localizations/src/ca-ES.ts index ae2eaa6432e..6a182d97bf4 100644 --- a/packages/localizations/src/ca-ES.ts +++ b/packages/localizations/src/ca-ES.ts @@ -2117,6 +2117,7 @@ export const caES: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Has rebutjat la sol·licitud de signatura. Torna-ho a provar per continuar.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/cs-CZ.ts b/packages/localizations/src/cs-CZ.ts index 63d0cff7de9..2c136eeed54 100644 --- a/packages/localizations/src/cs-CZ.ts +++ b/packages/localizations/src/cs-CZ.ts @@ -2116,6 +2116,7 @@ export const csCZ: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Rozšíření peněženky Web3 nebylo nalezeno. Pro pokračování prosím nainstalujte jednu.', web3_signature_request_rejected: 'Odmítli jste žádost o podpis. Chcete-li pokračovat, zkuste to prosím znovu.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/da-DK.ts b/packages/localizations/src/da-DK.ts index 17de000bb61..e6a3452295e 100644 --- a/packages/localizations/src/da-DK.ts +++ b/packages/localizations/src/da-DK.ts @@ -2101,6 +2101,7 @@ export const daDK: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Du har afvist signaturanmodningen. Prøv igen for at fortsætte.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/de-DE.ts b/packages/localizations/src/de-DE.ts index e78b8fc0207..a790bf6e48a 100644 --- a/packages/localizations/src/de-DE.ts +++ b/packages/localizations/src/de-DE.ts @@ -2134,6 +2134,7 @@ export const deDE: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Eine Web3 Wallet-Erweiterung wurde nicht gefunden. Bitte installieren Sie eine, um fortzufahren.', web3_signature_request_rejected: diff --git a/packages/localizations/src/el-GR.ts b/packages/localizations/src/el-GR.ts index 8af055a55ef..a81d77042e9 100644 --- a/packages/localizations/src/el-GR.ts +++ b/packages/localizations/src/el-GR.ts @@ -2125,6 +2125,7 @@ export const elGR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Δεν εντοπίστηκε αναγνωριστικό Web3. Παρακαλώ δοκιμάστε ξανά.', web3_signature_request_rejected: 'Απορρίψατε το αίτημα υπογραφής. Δοκιμάστε ξανά για να συνεχίσετε.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/en-GB.ts b/packages/localizations/src/en-GB.ts index 4538fdf8f82..c8f21c414d8 100644 --- a/packages/localizations/src/en-GB.ts +++ b/packages/localizations/src/en-GB.ts @@ -2108,6 +2108,7 @@ export const enGB: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'You have rejected the signature request. Please try again to continue.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 4066136089a..58523350769 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -2166,6 +2166,8 @@ export const enUS: LocalizationResource = { ticket_expired_code: 'This link has expired. Please start again or request a new link.', ticket_invalid_code: 'This link is no longer valid or has already been used. Please start again or request a new link.', + too_many_unverified_identifications: + 'Too many verifications are pending on this account. Remove an unverified email address, phone number, or wallet you no longer need, or wait a few minutes for an incomplete passkey setup to expire, then try again.', web3_missing_identifier: 'A Web3 Wallet extension cannot be found. Please install one to continue.', web3_signature_request_rejected: 'You have rejected the signature request. Please try again to continue.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/es-CR.ts b/packages/localizations/src/es-CR.ts index d00a4fb9e34..20fde8d7627 100644 --- a/packages/localizations/src/es-CR.ts +++ b/packages/localizations/src/es-CR.ts @@ -2116,6 +2116,7 @@ export const esCR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'No se puede encontrar la extension de la billetera Web3. Instala una para continuar', web3_signature_request_rejected: 'Has rechazado la solicitud de firma. Inténtalo de nuevo para continuar.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/es-ES.ts b/packages/localizations/src/es-ES.ts index 7668b8273f2..8a3e3b077c0 100644 --- a/packages/localizations/src/es-ES.ts +++ b/packages/localizations/src/es-ES.ts @@ -2117,6 +2117,7 @@ export const esES: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Has rechazado la solicitud de firma. Inténtalo de nuevo para continuar.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/es-MX.ts b/packages/localizations/src/es-MX.ts index 45b270b36ee..9fbd1657c41 100644 --- a/packages/localizations/src/es-MX.ts +++ b/packages/localizations/src/es-MX.ts @@ -2117,6 +2117,7 @@ export const esMX: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'No se puede encontrar la extension de la billetera Web3. Instala una para continuar', web3_signature_request_rejected: 'Has rechazado la solicitud de firma. Inténtalo de nuevo para continuar.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/es-UY.ts b/packages/localizations/src/es-UY.ts index 41a07137cdf..4642d64e309 100644 --- a/packages/localizations/src/es-UY.ts +++ b/packages/localizations/src/es-UY.ts @@ -2118,6 +2118,7 @@ export const esUY: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'No se encontró una extensión de cartera Web3. Por favor, instalá una para continuar.', web3_signature_request_rejected: 'Has rechazado la solicitud de firma. Inténtalo de nuevo para continuar.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/fa-IR.ts b/packages/localizations/src/fa-IR.ts index 2d8ec1d212d..c5394488e72 100644 --- a/packages/localizations/src/fa-IR.ts +++ b/packages/localizations/src/fa-IR.ts @@ -2115,6 +2115,7 @@ export const faIR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'افزونه‌ی کیف پول وب۳ پیدا نشد. برای ادامه، لطفاً یکی نصب کنید.', web3_signature_request_rejected: 'درخواست امضا را رد کرده‌اید. برای ادامه دوباره تلاش کنید.', web3_solana_signature_generation_failed: 'هنگام ایجاد امضا خطایی رخ داد. برای ادامه دوباره تلاش کنید.', diff --git a/packages/localizations/src/fi-FI.ts b/packages/localizations/src/fi-FI.ts index 82263f7b180..acf81048405 100644 --- a/packages/localizations/src/fi-FI.ts +++ b/packages/localizations/src/fi-FI.ts @@ -2133,6 +2133,7 @@ export const fiFI: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3-lompakkoa ei löytynyt. Asenna sellainen jatkaaksesi.', web3_signature_request_rejected: 'Hylkäsit allekirjoituspyynnön. Yritä uudelleen jatkaaksesi.', web3_solana_signature_generation_failed: 'Allekirjoitusta luotaessa tapahtui virhe. Yritä uudelleen jatkaaksesi.', diff --git a/packages/localizations/src/fr-FR.ts b/packages/localizations/src/fr-FR.ts index 57c087c439b..b53badd7457 100644 --- a/packages/localizations/src/fr-FR.ts +++ b/packages/localizations/src/fr-FR.ts @@ -2126,6 +2126,7 @@ export const frFR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Aucune extension de portefeuille Web3 trouvée. Veuillez en installer une pour continuer.', web3_signature_request_rejected: 'Vous avez refusé la demande de signature. Veuillez réessayer pour continuer.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/he-IL.ts b/packages/localizations/src/he-IL.ts index 6c66e2b782a..0feb281e01f 100644 --- a/packages/localizations/src/he-IL.ts +++ b/packages/localizations/src/he-IL.ts @@ -2092,6 +2092,7 @@ export const heIL: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'דחית את בקשת החתימה. נסה/י שוב כדי להמשיך.', web3_solana_signature_generation_failed: 'אירעה שגיאה בעת יצירת החתימה. נסה/י שוב כדי להמשיך.', diff --git a/packages/localizations/src/hi-IN.ts b/packages/localizations/src/hi-IN.ts index 5495af45e4b..eb8921f1811 100644 --- a/packages/localizations/src/hi-IN.ts +++ b/packages/localizations/src/hi-IN.ts @@ -2130,6 +2130,7 @@ export const hiIN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3 वॉलेट एक्सटेंशन नहीं मिल सका। जारी रखने के लिए कृपया एक इंस्टॉल करें।', web3_signature_request_rejected: 'आपने सिग्नेचर अनुरोध अस्वीकार कर दिया है। जारी रखने के लिए कृपया फिर से प्रयास करें।', diff --git a/packages/localizations/src/hr-HR.ts b/packages/localizations/src/hr-HR.ts index b43ad2b32a3..0e6b43d4eee 100644 --- a/packages/localizations/src/hr-HR.ts +++ b/packages/localizations/src/hr-HR.ts @@ -2135,6 +2135,7 @@ export const hrHR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Nije pronađeno proširenje za Web3 novčanik. Molimo instalirajte jedno za nastavak.', web3_signature_request_rejected: 'Odbili ste zahtjev za potpis. Pokušajte ponovno za nastavak.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/hu-HU.ts b/packages/localizations/src/hu-HU.ts index 840c05f7a30..2df3cf0f335 100644 --- a/packages/localizations/src/hu-HU.ts +++ b/packages/localizations/src/hu-HU.ts @@ -2134,6 +2134,7 @@ export const huHU: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Nem található Web3 tárca bővítmény. Kérlek telepíts egyet a folytatáshoz.', web3_signature_request_rejected: 'Elutasította az aláírási kérelmet. Kérjük, próbálja meg újra a folytatáshoz.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/id-ID.ts b/packages/localizations/src/id-ID.ts index f535eb018d4..8506ee752a1 100644 --- a/packages/localizations/src/id-ID.ts +++ b/packages/localizations/src/id-ID.ts @@ -2114,6 +2114,7 @@ export const idID: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Anda menolak permintaan tanda tangan. Silakan coba lagi untuk melanjutkan.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/is-IS.ts b/packages/localizations/src/is-IS.ts index b1cf4775d36..2725cb0ec66 100644 --- a/packages/localizations/src/is-IS.ts +++ b/packages/localizations/src/is-IS.ts @@ -2132,6 +2132,7 @@ export const isIS: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Ekki fannst Web3 veskisviðbót. Vinsamlegast settu upp eina til að halda áfram.', web3_signature_request_rejected: 'Þú hafðir hafnað beiðni um undirritun. Reyndu aftur til að halda áfram.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/it-IT.ts b/packages/localizations/src/it-IT.ts index 7254b4112e6..818aa2c99c8 100644 --- a/packages/localizations/src/it-IT.ts +++ b/packages/localizations/src/it-IT.ts @@ -2113,6 +2113,7 @@ export const itIT: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Hai rifiutato la richiesta di firma. Riprova per continuare.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/ja-JP.ts b/packages/localizations/src/ja-JP.ts index 9dfeec06a4f..4a5a0fe7001 100644 --- a/packages/localizations/src/ja-JP.ts +++ b/packages/localizations/src/ja-JP.ts @@ -2128,6 +2128,7 @@ export const jaJP: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3ウォレット拡張機能が見つかりません。続行するにはインストールしてください。', web3_signature_request_rejected: '署名リクエストを拒否しました。続行するにはもう一度お試しください。', web3_solana_signature_generation_failed: '署名の生成中にエラーが発生しました。続行するにはもう一度お試しください。', diff --git a/packages/localizations/src/kk-KZ.ts b/packages/localizations/src/kk-KZ.ts index 34fabe33f2d..7134dfb7483 100644 --- a/packages/localizations/src/kk-KZ.ts +++ b/packages/localizations/src/kk-KZ.ts @@ -2094,6 +2094,7 @@ export const kkKZ: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3 Wallet кеңейтуі табылмады. Орнатыңыз.', web3_signature_request_rejected: 'Сіз қолтаңба сұрауын қабылдамадыңыз. Жалғастыру үшін қайтадан көріңіз.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/ko-KR.ts b/packages/localizations/src/ko-KR.ts index 0c1326fc0fc..24de87d4e9d 100644 --- a/packages/localizations/src/ko-KR.ts +++ b/packages/localizations/src/ko-KR.ts @@ -2101,6 +2101,7 @@ export const koKR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3 지갑 확장 프로그램을 찾을 수 없어요. 계속하려면 설치해 주세요.', web3_signature_request_rejected: '서명 요청을 거부했어요. 계속하려면 다시 시도해 주세요.', web3_solana_signature_generation_failed: '서명을 생성하는 동안 오류가 발생했어요. 계속하려면 다시 시도해 주세요.', diff --git a/packages/localizations/src/mn-MN.ts b/packages/localizations/src/mn-MN.ts index eb3f619b3bc..108ba8014de 100644 --- a/packages/localizations/src/mn-MN.ts +++ b/packages/localizations/src/mn-MN.ts @@ -2106,6 +2106,7 @@ export const mnMN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Та гарын үсгийн хүсэлтийг цуцалсан байна. Үргэлжлүүлэхийн тулд дахин оролдоно уу.', diff --git a/packages/localizations/src/ms-MY.ts b/packages/localizations/src/ms-MY.ts index 17c20e97493..fda67db9c75 100644 --- a/packages/localizations/src/ms-MY.ts +++ b/packages/localizations/src/ms-MY.ts @@ -2139,6 +2139,7 @@ export const msMY: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Sambungan Dompet Web3 tidak dapat dijumpai. Sila pasang satu untuk meneruskan.', web3_signature_request_rejected: 'Anda telah menolak permintaan tandatangan. Sila cuba lagi untuk meneruskan.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/nb-NO.ts b/packages/localizations/src/nb-NO.ts index 24bb5be3b7f..58ee9d09a6d 100644 --- a/packages/localizations/src/nb-NO.ts +++ b/packages/localizations/src/nb-NO.ts @@ -2132,6 +2132,7 @@ export const nbNO: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'En Web3-lommebokutvidelse ble ikke funnet. Installer en for å fortsette.', web3_signature_request_rejected: 'Du avviste signaturforespørselen. Prøv igjen for å fortsette.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/nl-BE.ts b/packages/localizations/src/nl-BE.ts index b12667e42a6..03c02cecc3d 100644 --- a/packages/localizations/src/nl-BE.ts +++ b/packages/localizations/src/nl-BE.ts @@ -2105,6 +2105,7 @@ export const nlBE: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Je hebt het handtekeningverzoek afgewezen. Probeer het opnieuw om verder te gaan.', diff --git a/packages/localizations/src/nl-NL.ts b/packages/localizations/src/nl-NL.ts index 827f26616d6..8896e27a07b 100644 --- a/packages/localizations/src/nl-NL.ts +++ b/packages/localizations/src/nl-NL.ts @@ -2105,6 +2105,7 @@ export const nlNL: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Je hebt het handtekeningverzoek afgewezen. Probeer het opnieuw om verder te gaan.', diff --git a/packages/localizations/src/pl-PL.ts b/packages/localizations/src/pl-PL.ts index 629ae7230be..aa8adc2a60c 100644 --- a/packages/localizations/src/pl-PL.ts +++ b/packages/localizations/src/pl-PL.ts @@ -2113,6 +2113,7 @@ export const plPL: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Nie można znaleźć rozszerzenia Web3 Wallet. Zainstaluj je, aby kontynuować.', web3_signature_request_rejected: 'Odrzuciłeś prośbę o podpis. Spróbuj ponownie, aby kontynuować.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/pt-BR.ts b/packages/localizations/src/pt-BR.ts index 764bb071051..df49686106b 100644 --- a/packages/localizations/src/pt-BR.ts +++ b/packages/localizations/src/pt-BR.ts @@ -2123,6 +2123,7 @@ export const ptBR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Uma extensão de carteira Web3 não pode ser encontrada. Por favor, instale uma para continuar.', web3_signature_request_rejected: 'Você rejeitou a solicitação de assinatura. Tente novamente para continuar.', diff --git a/packages/localizations/src/pt-PT.ts b/packages/localizations/src/pt-PT.ts index 9c29d487a27..b6538ce7947 100644 --- a/packages/localizations/src/pt-PT.ts +++ b/packages/localizations/src/pt-PT.ts @@ -2129,6 +2129,7 @@ export const ptPT: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Falta um identificador Web3. Tente novamente.', web3_signature_request_rejected: 'Rejeitou o pedido de assinatura. Tente novamente para continuar.', web3_solana_signature_generation_failed: 'Ocorreu um erro ao gerar a assinatura. Tente novamente para continuar.', diff --git a/packages/localizations/src/ro-RO.ts b/packages/localizations/src/ro-RO.ts index 43268667827..20d5a8a8107 100644 --- a/packages/localizations/src/ro-RO.ts +++ b/packages/localizations/src/ro-RO.ts @@ -2124,6 +2124,7 @@ export const roRO: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Nu am găsit o extensie pentru portofel Web3. Te rugăm instalează una pentru a continua.', web3_signature_request_rejected: 'Ați respins solicitarea de semnătură. Încercați din nou pentru a continua.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/ru-RU.ts b/packages/localizations/src/ru-RU.ts index 4664a748889..216d06bd8f0 100644 --- a/packages/localizations/src/ru-RU.ts +++ b/packages/localizations/src/ru-RU.ts @@ -2121,6 +2121,7 @@ export const ruRU: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Вы отклонили запрос на подпись. Пожалуйста, попробуйте ещё раз, чтобы продолжить.', diff --git a/packages/localizations/src/sk-SK.ts b/packages/localizations/src/sk-SK.ts index 742e6ec56ba..857dd72424f 100644 --- a/packages/localizations/src/sk-SK.ts +++ b/packages/localizations/src/sk-SK.ts @@ -2112,6 +2112,7 @@ export const skSK: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Rozšírenie Web3 Peňaženky nebolo nájdené. Je potrebné ho nainštalovať.', web3_signature_request_rejected: 'Odmietli ste žiadosť o podpis. Skúste to znova, aby ste mohli pokračovať.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/sr-RS.ts b/packages/localizations/src/sr-RS.ts index c09369a7cf8..1e36e637efa 100644 --- a/packages/localizations/src/sr-RS.ts +++ b/packages/localizations/src/sr-RS.ts @@ -2104,6 +2104,7 @@ export const srRS: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Odbio/la si zahtev za potpis. Pokušaj ponovo da nastaviš.', web3_solana_signature_generation_failed: 'Došlo je do greške pri generisanju potpisa. Pokušaj ponovo da nastaviš.', diff --git a/packages/localizations/src/sv-SE.ts b/packages/localizations/src/sv-SE.ts index 508866d8765..be7b87b178a 100644 --- a/packages/localizations/src/sv-SE.ts +++ b/packages/localizations/src/sv-SE.ts @@ -2107,6 +2107,7 @@ export const svSE: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Du avvisade signaturbegäran. Försök igen för att fortsätta.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/ta-IN.ts b/packages/localizations/src/ta-IN.ts index de419b38460..73272043e6e 100644 --- a/packages/localizations/src/ta-IN.ts +++ b/packages/localizations/src/ta-IN.ts @@ -2140,6 +2140,7 @@ export const taIN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3 வாலட் நீட்டிப்பு காணப்படவில்லை. தொடர ஒன்றை நிறுவவும்.', web3_signature_request_rejected: 'நீங்கள் கையொப்ப கோரிக்கையை நிராகரித்துவிட்டீர்கள். தொடர மீண்டும் முயற்சிக்கவும்.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/te-IN.ts b/packages/localizations/src/te-IN.ts index c5dcaaafd3b..1256ecff435 100644 --- a/packages/localizations/src/te-IN.ts +++ b/packages/localizations/src/te-IN.ts @@ -2134,6 +2134,7 @@ export const teIN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3 వాలెట్ పొడిగింపు కనుగొనబడలేదు. కొనసాగించడానికి దయచేసి ఒకదాన్ని ఇన్‌స్టాల్ చేయండి.', web3_signature_request_rejected: 'మీరు సంతకం అభ్యర్థనను తిరస్కరించారు. కొనసాగేందుకు దయచేసి మళ్లీ ప్రయత్నించండి.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/th-TH.ts b/packages/localizations/src/th-TH.ts index 9c966c37b22..5a002a7b01b 100644 --- a/packages/localizations/src/th-TH.ts +++ b/packages/localizations/src/th-TH.ts @@ -2102,6 +2102,7 @@ export const thTH: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'ไม่พบส่วนขยาย Web3 Wallet โปรดติดตั้งเพื่อดำเนินการต่อ', web3_signature_request_rejected: 'คุณได้ปฏิเสธคำขอการลงลายเซ็น โปรดลองอีกครั้งเพื่อดำเนินการต่อ', web3_solana_signature_generation_failed: 'เกิดข้อผิดพลาดขณะสร้างลายเซ็น โปรดลองอีกครั้งเพื่อดำเนินการต่อ', diff --git a/packages/localizations/src/tr-TR.ts b/packages/localizations/src/tr-TR.ts index f91f29111d8..f1736ecb0e4 100644 --- a/packages/localizations/src/tr-TR.ts +++ b/packages/localizations/src/tr-TR.ts @@ -2108,6 +2108,7 @@ export const trTR: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Web3 için tanımlayıcı eksik.', web3_signature_request_rejected: 'İmza isteğini reddettiniz. Devam etmek için lütfen tekrar deneyin.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/uk-UA.ts b/packages/localizations/src/uk-UA.ts index fe89a0e14ea..73699280b51 100644 --- a/packages/localizations/src/uk-UA.ts +++ b/packages/localizations/src/uk-UA.ts @@ -2102,6 +2102,7 @@ export const ukUA: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: 'Ви відхилили запит на підпис. Будь ласка, спробуйте ще раз, щоб продовжити.', web3_solana_signature_generation_failed: diff --git a/packages/localizations/src/vi-VN.ts b/packages/localizations/src/vi-VN.ts index 1cb2a822051..1fae7626221 100644 --- a/packages/localizations/src/vi-VN.ts +++ b/packages/localizations/src/vi-VN.ts @@ -2125,6 +2125,7 @@ export const viVN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: 'Không tìm thấy phần mở rộng Web3 Wallet. Vui lòng cài đặt một phần mở rộng để tiếp tục.', web3_signature_request_rejected: 'Bạn đã từ chối yêu cầu ký. Vui lòng thử lại để tiếp tục.', web3_solana_signature_generation_failed: 'Đã xảy ra lỗi khi tạo chữ ký. Vui lòng thử lại để tiếp tục.', diff --git a/packages/localizations/src/zh-CN.ts b/packages/localizations/src/zh-CN.ts index 2b64031e69c..a3ac19925ce 100644 --- a/packages/localizations/src/zh-CN.ts +++ b/packages/localizations/src/zh-CN.ts @@ -2085,6 +2085,7 @@ export const zhCN: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: undefined, web3_signature_request_rejected: '您已拒绝签名请求。请重试以继续。', web3_solana_signature_generation_failed: '生成签名时发生错误。请重试以继续。', diff --git a/packages/localizations/src/zh-TW.ts b/packages/localizations/src/zh-TW.ts index 847b62ead44..54c411d38c2 100644 --- a/packages/localizations/src/zh-TW.ts +++ b/packages/localizations/src/zh-TW.ts @@ -2090,6 +2090,7 @@ export const zhTW: LocalizationResource = { sso_bypass_domain_not_served: undefined, ticket_expired_code: undefined, ticket_invalid_code: undefined, + too_many_unverified_identifications: undefined, web3_missing_identifier: '找不到 Web3 錢包擴充功能。請安裝以繼續。', web3_signature_request_rejected: '您已拒絕簽名請求。請再試一次以繼續。', web3_solana_signature_generation_failed: '產生簽名時發生錯誤。請再試一次以繼續。', diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 6d2c93a776d..cd015a831ff 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -2462,6 +2462,7 @@ type UnstableErrors = WithParamName<{ passkey_retrieval_cancelled: LocalizationValue; passkey_registration_cancelled: LocalizationValue; passkey_already_exists: LocalizationValue; + too_many_unverified_identifications: LocalizationValue; web3_missing_identifier: LocalizationValue; web3_solana_signature_generation_failed: LocalizationValue; web3_signature_request_rejected: LocalizationValue; diff --git a/packages/ui/src/components/UserProfile/PasskeySection.tsx b/packages/ui/src/components/UserProfile/PasskeySection.tsx index c3b09682e6f..8a2cd69c354 100644 --- a/packages/ui/src/components/UserProfile/PasskeySection.tsx +++ b/packages/ui/src/components/UserProfile/PasskeySection.tsx @@ -16,6 +16,7 @@ import { useFormControl } from '@/ui/utils/useFormControl'; import { Col, Flex, localizationKeys, Text, useLocalizations } from '../../customizables'; import { Action } from '../../elements/Action'; import { useActionContext } from '../../elements/Action/ActionRoot'; +import { useLoadingStatus } from '../../hooks'; import type { PropsOfComponent } from '../../styledSystem'; import { mqu } from '../../styledSystem'; import { RemovePasskeyForm } from './RemoveResourceForm'; @@ -202,16 +203,21 @@ const AddPasskeyButton = ({ onClick }: { onClick?: () => void }) => { const { isSatellite } = useClerk(); const { user } = useUser(); const createPasskey = useReverification(() => user?.createPasskey()); + const status = useLoadingStatus(); const handleCreatePasskey = async () => { onClick?.(); if (!user) { return; } + card.setError(undefined); + status.setLoading(); try { await createPasskey(); } catch (e: any) { handleError(e, [], card.setError); + } finally { + status.setIdle(); } }; @@ -224,6 +230,7 @@ const AddPasskeyButton = ({ onClick }: { onClick?: () => void }) => { id='passkeys' localizationKey={localizationKeys('userProfile.start.passkeysSection.primaryButton')} onClick={handleCreatePasskey} + isLoading={status.isLoading} /> ); }; diff --git a/packages/ui/src/components/UserProfile/__tests__/PasskeysSection.test.tsx b/packages/ui/src/components/UserProfile/__tests__/PasskeysSection.test.tsx index ca388374504..0bf07fee089 100644 --- a/packages/ui/src/components/UserProfile/__tests__/PasskeysSection.test.tsx +++ b/packages/ui/src/components/UserProfile/__tests__/PasskeysSection.test.tsx @@ -1,10 +1,11 @@ +import { ClerkWebAuthnError } from '@clerk/shared/error'; import type { PasskeyJSON, PasskeyResource } from '@clerk/shared/types'; import { act } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; import { bindCreateFixtures } from '@/test/create-fixtures'; -import { render, waitFor } from '@/test/utils'; -import { CardStateProvider } from '@/ui/elements/contexts'; +import { fireEvent, render, waitFor } from '@/test/utils'; +import { CardStateProvider, useCardState } from '@/ui/elements/contexts'; import { PasskeySection } from '../PasskeySection'; @@ -76,6 +77,73 @@ describe('PasskeySection', () => { await userEvent.click(getByRole('button', { name: 'Add a passkey' })); expect(fixtures.clerk.user?.createPasskey).toHaveBeenCalled(); }); + + it('ignores repeat clicks while a registration is in flight', async () => { + const { wrapper, fixtures } = await createFixtures(withPasskeys); + + fixtures.clerk.user?.createPasskey.mockImplementation(() => new Promise(() => {})); + const { getByRole, userEvent } = render( + + + , + { wrapper }, + ); + + const button = getByRole('button', { name: 'Add a passkey' }); + await userEvent.click(button); + fireEvent.click(button); + + expect(button).toBeDisabled(); + expect(fixtures.clerk.user?.createPasskey).toHaveBeenCalledTimes(1); + }); + + it('re-enables the button after a failed registration', async () => { + const { wrapper, fixtures } = await createFixtures(withPasskeys); + + fixtures.clerk.user?.createPasskey.mockRejectedValue( + new ClerkWebAuthnError('Passkey registration was cancelled or timed out.', { + code: 'passkey_registration_cancelled', + }), + ); + const { getByRole, userEvent } = render( + + + , + { wrapper }, + ); + + const button = getByRole('button', { name: 'Add a passkey' }); + await userEvent.click(button); + + await waitFor(() => expect(button).not.toBeDisabled()); + }); + + it('clears the previous error when a retry succeeds', async () => { + const { wrapper, fixtures } = await createFixtures(withPasskeys); + + fixtures.clerk.user?.createPasskey.mockRejectedValueOnce( + new ClerkWebAuthnError('Passkey registration was cancelled or timed out.', { + code: 'passkey_registration_cancelled', + }), + ); + const CardError = () => {useCardState().error ?? ''}; + const { getByRole, getByTestId, userEvent } = render( + + + + , + { wrapper }, + ); + + const button = getByRole('button', { name: 'Add a passkey' }); + await userEvent.click(button); + await waitFor(() => expect(getByTestId('card-error')).toHaveTextContent(/cancelled or timed out/i)); + + fixtures.clerk.user?.createPasskey.mockResolvedValueOnce({} as any); + await userEvent.click(button); + + await waitFor(() => expect(getByTestId('card-error')).toHaveTextContent('')); + }); }); describe('Update a passkey', () => {