diff --git a/src/app/(campaign)/campanha/webauthn/login/route.ts b/src/app/(campaign)/campanha/webauthn/login/route.ts index 53fd2ccb..ac9124df 100644 --- a/src/app/(campaign)/campanha/webauthn/login/route.ts +++ b/src/app/(campaign)/campanha/webauthn/login/route.ts @@ -2,6 +2,7 @@ import config from '@payload-config' import { NextResponse } from 'next/server' import { getPayload } from 'payload' +import { CAMPAIGN_BIOMETRIC_LOGIN_ERROR_MESSAGE } from '@/lib/campaignAuthCopy' import type { CampaignWebAuthnLoginResponse } from '@/lib/campaignWebAuthn' import { campaignWebAuthnLoginSchema } from '@/lib/schemas/campaignWebAuthn' import { setCampaignAuthCookie } from '@/utilities/campaignAuth' @@ -32,7 +33,7 @@ export const POST = campaignJsonMutationRoute( CAMPAIGN_WEBAUTHN_ACCOUNT_UNAVAILABLE_MESSAGE, CAMPAIGN_WEBAUTHN_ACCOUNT_LOCKED_MESSAGE, ], - genericMessage: 'Não foi possível entrar com a biometria. Use sua senha.', + genericMessage: CAMPAIGN_BIOMETRIC_LOGIN_ERROR_MESSAGE, }, async ({ credential }): Promise> => { const payload = await getPayload({ config }) diff --git a/src/components/campaign/auth/CampaignBiometricLoginButton.tsx b/src/components/campaign/auth/CampaignBiometricLoginButton.tsx index d4a64aeb..d3963759 100644 --- a/src/components/campaign/auth/CampaignBiometricLoginButton.tsx +++ b/src/components/campaign/auth/CampaignBiometricLoginButton.tsx @@ -8,11 +8,12 @@ import { useCampaignBiometricsAvailable } from '@/components/campaign/auth/useCa import { Button } from '@/components/ui/button' import { FieldError } from '@/components/ui/field' import { Spinner } from '@/components/ui/Spinner' -import { CAMPAIGN_BIOMETRIC_LOGIN_LABEL } from '@/lib/campaignAuthCopy' +import { + CAMPAIGN_BIOMETRIC_LOGIN_ERROR_MESSAGE, + CAMPAIGN_BIOMETRIC_LOGIN_LABEL, +} from '@/lib/campaignAuthCopy' import { CampaignWebAuthnError } from '@/lib/campaignWebAuthnSupport' -const FALLBACK_ERROR_MESSAGE = 'Não foi possível entrar com a biometria. Use sua senha.' - /** * Password-less sign-in on the login screen. It is mounted only when the server * says the relying party is configured for this origin (Vercel previews are @@ -47,7 +48,9 @@ export const CampaignBiometricLoginButton = () => { // A cancelled prompt is a decision, not a failure: say it plainly and // leave the password form untouched. setErrorMessage( - error instanceof CampaignWebAuthnError ? error.message : FALLBACK_ERROR_MESSAGE, + error instanceof CampaignWebAuthnError + ? error.message + : CAMPAIGN_BIOMETRIC_LOGIN_ERROR_MESSAGE, ) } setPending(false) diff --git a/src/lib/campaignAuthCopy.ts b/src/lib/campaignAuthCopy.ts index 9b8f821d..25079e2f 100644 --- a/src/lib/campaignAuthCopy.ts +++ b/src/lib/campaignAuthCopy.ts @@ -70,6 +70,14 @@ export const CAMPAIGN_BIOMETRIC_ENROLLED_MESSAGE = export const CAMPAIGN_BIOMETRIC_ENROLL_ERROR_MESSAGE = 'Não foi possível cadastrar este aparelho. Tente novamente.' +/** + * Twin of the one above for the sign-in half: the route's `genericMessage` and + * the login button's fallback are the same sentence in the same slot, reached + * depending on whether the server answered an envelope or the network broke. + */ +export const CAMPAIGN_BIOMETRIC_LOGIN_ERROR_MESSAGE = + 'Não foi possível entrar com a biometria. Use sua senha.' + export const CAMPAIGN_BIOMETRIC_REMOVED_MESSAGE = 'Aparelho removido. Ele não entra mais com biometria.' diff --git a/src/lib/campaignJsonRequest.ts b/src/lib/campaignJsonRequest.ts index c1eb094b..78df5714 100644 --- a/src/lib/campaignJsonRequest.ts +++ b/src/lib/campaignJsonRequest.ts @@ -1,8 +1,9 @@ /** - * Transport half of the list cells' quick edits: the preamble every control - * repeated around `fetch` (same-origin credentials, JSON headers, the cast of - * the discriminated response). Deliberately knows nothing about revert, retry - * or debounce — that is `useCampaignCellAutosave`'s, or the caller's. + * Transport half of every `/campanha` JSON POST — the list cells' quick edits + * and the WebAuthn ceremonies: the preamble each one repeated around `fetch` + * (same-origin credentials, JSON headers, the cast of the discriminated + * response). Deliberately knows nothing about revert, retry or debounce — that + * is `useCampaignCellAutosave`'s, or the caller's. * * `ok` travels next to the payload because a route can answer a real body with * a non-200 status (`engagement-level` returns its `blocked` state as 409), so diff --git a/src/lib/campaignWebAuthnClient.ts b/src/lib/campaignWebAuthnClient.ts index 17ca191b..1cf6f7d1 100644 --- a/src/lib/campaignWebAuthnClient.ts +++ b/src/lib/campaignWebAuthnClient.ts @@ -32,22 +32,20 @@ import { import { CampaignWebAuthnError } from '@/lib/campaignWebAuthnSupport' /** - * Transport is `postCampaignJson`; this adds the two things the ceremonies need - * on top of it. The status is dropped on purpose — a 403/409/500 from these - * routes still carries the typed envelope, so the discriminant decides — and a - * response that is not JSON at all (proxy error page, offline) surfaces as one - * sentence instead of a `SyntaxError` about position 0. A rejection that is not - * a parse failure is a network failure and is rethrown untouched. + * Transport is `postCampaignJson`; the only thing added here is dropping `ok`, + * because a 403/409/500 from these routes still carries the typed envelope and + * the discriminant is what decides. The `{}` default is load-bearing: the route + * shell always parses a body, so omitting it would answer 400. + * + * Nothing is caught. A transport failure — offline, or a proxy answering HTML + * that `response.json()` rejects — bubbles as a plain `Error`, which is the + * channel `CampaignWebAuthnError` reserves for "the island shows its own copy". + * An earlier version rewrote that case as "Resposta inválida do servidor." and + * the sentence never reached a screen, because all three islands read `message` + * only off a `CampaignWebAuthnError`. */ -const postJson = async (path: string, body?: unknown): Promise => { - try { - const { payload } = await postCampaignJson(path, body ?? {}) - return payload - } catch (error) { - if (error instanceof SyntaxError) throw new Error('Resposta inválida do servidor.') - throw error - } -} +const postJson = async (path: string, body?: unknown): Promise => + (await postCampaignJson(path, body ?? {})).payload /** * `navigator.credentials` rejects with DOM exceptions whose names are the only diff --git a/src/utilities/campaignJsonMutationRoute.ts b/src/utilities/campaignJsonMutationRoute.ts index ea4028db..e36af26c 100644 --- a/src/utilities/campaignJsonMutationRoute.ts +++ b/src/utilities/campaignJsonMutationRoute.ts @@ -73,8 +73,9 @@ const campaignJsonMutationErrorResponse = ( /** * Builds the `POST` of a `/campanha/**` JSON mutation route (the list cells' - * quick edits): same-origin check, body parse, schema parse and error mapping, - * leaving each route with its schema, its action call and its success payload. + * quick edits, and the WebAuthn ceremonies): same-origin check, body parse, + * schema parse and error mapping, leaving each route with its schema, its + * action call and its success payload. * * It is a wrapper and not a `guard(request)` helper because the origin check is * the kind of line a sixth route forgets, and forgetting it fails OPEN — a