@12-apps/payments-frontend 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +2 -2
  2. package/src/activation/charge-copy.ts +10 -0
  3. package/src/activation/use-activation-charge.ts +10 -8
  4. package/src/card/copy.ts +115 -0
  5. package/src/card/cpf.ts +12 -5
  6. package/src/card/fields.tsx +17 -12
  7. package/src/card/format.ts +32 -16
  8. package/src/card/index.ts +3 -1
  9. package/src/card/pt-BR.ts +51 -0
  10. package/src/card/stripe-token.ts +5 -6
  11. package/src/card/tokenize.ts +28 -32
  12. package/src/components/CheckoutPayment.tsx +37 -201
  13. package/src/components/ConfirmCredentialSave.tsx +8 -10
  14. package/src/components/ConnectionCard.tsx +23 -18
  15. package/src/components/ConnectionProbe.tsx +3 -1
  16. package/src/components/CredentialFieldStack.tsx +8 -2
  17. package/src/components/CredentialFields.tsx +13 -6
  18. package/src/components/CredentialFormAlerts.tsx +28 -19
  19. package/src/components/EnvironmentTabs.tsx +23 -15
  20. package/src/components/OAuthPanel.tsx +5 -6
  21. package/src/components/PaymentProviderSettings.tsx +45 -13
  22. package/src/components/ProviderConnection.tsx +37 -28
  23. package/src/components/ProviderList.tsx +6 -3
  24. package/src/components/ProviderPriorityList.tsx +18 -16
  25. package/src/components/ProviderStatusBar.tsx +24 -24
  26. package/src/components/SetupGuideSection.tsx +15 -6
  27. package/src/components/checkout/apple-pay-button.tsx +19 -9
  28. package/src/components/checkout/buyer-fields.ts +5 -3
  29. package/src/components/checkout/buyer-gate.ts +68 -0
  30. package/src/components/checkout/buyer-info-form.tsx +14 -15
  31. package/src/components/checkout/card-instruments.ts +31 -12
  32. package/src/components/checkout/card-view.tsx +11 -7
  33. package/src/components/checkout/checkout-flow.tsx +4 -1
  34. package/src/components/checkout/checkout-steps.tsx +3 -1
  35. package/src/components/checkout/client-context.tsx +28 -16
  36. package/src/components/checkout/client.ts +43 -16
  37. package/src/components/checkout/copy-context.tsx +83 -0
  38. package/src/components/checkout/google-pay-button.tsx +19 -7
  39. package/src/components/checkout/method-picker.tsx +34 -10
  40. package/src/components/checkout/payer-summary.tsx +8 -6
  41. package/src/components/checkout/payment-error-panel.tsx +8 -5
  42. package/src/components/checkout/pix-view.tsx +16 -9
  43. package/src/components/checkout/providers/hosted-link.tsx +26 -16
  44. package/src/components/checkout/pt-BR.ts +27 -0
  45. package/src/components/checkout/screens-copy.ts +201 -0
  46. package/src/components/checkout/screens-pt-BR.ts +101 -0
  47. package/src/components/checkout/transport.ts +34 -8
  48. package/src/components/checkout/use-card-checkout.ts +13 -5
  49. package/src/components/checkout/use-checkout-controller.ts +5 -48
  50. package/src/components/checkout/view-copy.ts +13 -0
  51. package/src/components/checkout/wallet-pane.tsx +13 -8
  52. package/src/components/checkout-payment-copy.ts +88 -0
  53. package/src/components/checkout-payment-pt-BR.ts +50 -0
  54. package/src/components/checkout-payment-types.ts +25 -0
  55. package/src/components/checkout-payment-views.tsx +225 -0
  56. package/src/components/connection-state.ts +22 -10
  57. package/src/components/credential-rules.ts +8 -3
  58. package/src/components/platform/ConnectApplicationPanel.tsx +6 -2
  59. package/src/components/platform/ConnectEnvironmentCard.tsx +19 -15
  60. package/src/components/platform/HomologacaoGuideCard.tsx +23 -19
  61. package/src/components/platform/HomologacaoOutcomeCard.tsx +24 -16
  62. package/src/components/platform/PlatformHomologacao.tsx +12 -8
  63. package/src/components/settings-copy-context.tsx +50 -0
  64. package/src/components/settings-copy.ts +304 -0
  65. package/src/components/settings-pt-BR.ts +169 -0
  66. package/src/flows/copy.ts +12 -0
  67. package/src/flows/create-payment-flows.tsx +8 -1
  68. package/src/flows/runtime.tsx +12 -4
  69. package/src/flows/screens-vault.tsx +10 -10
  70. package/src/flows/types.ts +5 -2
  71. package/src/flows/use-add-card.ts +10 -5
  72. package/src/index.ts +63 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Every sentence the buyer's checkout SCREENS render (FUT-760).
3
+ *
4
+ * The step wrappers already took theirs through `CheckoutViewCopy`; this is
5
+ * what sits inside them — the method tiles, the PIX and card panes, the wallet
6
+ * buttons, the hosted handover, and the refusals any of them can produce.
7
+ *
8
+ * Required, with no defaults. The split is the one the whole port draws: WHICH
9
+ * situation the buyer is in — a charge still settling, one taking longer than
10
+ * it should, a wallet this store cannot start, a provider page they are about
11
+ * to be sent to — is knowledge of the payment lifecycle, and it stays in the
12
+ * package. The words are the host's.
13
+ *
14
+ * Two members are LOCALES rather than sentences (`pix.expiryLocale`,
15
+ * `wallet.googlePay.buttonLocale`). They are here for the same reason: both
16
+ * were hard-coded to Brazilian Portuguese, and both decide what a buyer reads
17
+ * — one formats the PIX expiry clock, the other is the language Google draws
18
+ * its own button in. A host that translates every string below and still ships
19
+ * `"pt"` to Google has a checkout in two languages.
20
+ */
21
+
22
+ /** The method tiles, and what each one promises. */
23
+ export interface MethodPickerCopy {
24
+ /** The group's heading, and its `aria-label` — one string, said once. */
25
+ groupLabel: string;
26
+ /**
27
+ * What each method is CALLED. "PIX" is a Brazilian instrument and keeps its
28
+ * name everywhere, the way "Visa" does; "Cartão" is just the word for card,
29
+ * and was the tile's label — so both are stated rather than one assumed.
30
+ */
31
+ pixLabel: string;
32
+ cardLabel: string;
33
+ /** What each method is like to use — shown under its name. */
34
+ pixDescription: string;
35
+ cardDescription: string;
36
+ /** This store's providers cannot take that method. */
37
+ unavailableHere: string;
38
+ }
39
+
40
+ /**
41
+ * What a charge in flight says, shared by the PIX, card and wallet panes.
42
+ *
43
+ * One slice rather than three, because these ARE the same four states — and
44
+ * three copies of them is how a product ends up telling a buyer two different
45
+ * things about one situation depending on which button they pressed.
46
+ */
47
+ export interface SettlingCopy {
48
+ /** The provider answered, and the answer was not a payment. */
49
+ cannotConfirm: string;
50
+ /** Still open past the point where a buyer starts to worry. */
51
+ takingLonger: string;
52
+ /**
53
+ * The one sentence in here that prevents money moving twice: a buyer who
54
+ * pays again because the screen went quiet has been charged twice, and only
55
+ * this line stands between them and that.
56
+ */
57
+ takingLongerHelp: string;
58
+ /** The charge is on its way to the provider. */
59
+ processing: string;
60
+ /** It arrived, and settlement is being confirmed. */
61
+ confirming: string;
62
+ /** It came back refused. */
63
+ cannotPay: string;
64
+ }
65
+
66
+ /** The PIX pane: the QR, the copyable code, and the wait. */
67
+ export interface PixPaneCopy {
68
+ heading: string;
69
+ /** What to do with the QR, carrying the order total the buyer is paying. */
70
+ instructions(totalLabel: string): string;
71
+ /** The QR image's alternative text — the only thing a screen reader gets. */
72
+ qrAlt: string;
73
+ copyAction: string;
74
+ /** The same button for the two seconds after a successful copy. */
75
+ copiedAction: string;
76
+ /**
77
+ * How long the code lasts. Takes the already-formatted clock time, and
78
+ * {@link expiryLocale} is what formatted it.
79
+ */
80
+ validUntil(time: string): string;
81
+ /**
82
+ * The BCP-47 tag the expiry clock is formatted in — `'pt-BR'` here, and a
83
+ * hard-coded one before this port. Beside the sentence it feeds rather than
84
+ * in a config elsewhere, because they are one decision.
85
+ */
86
+ expiryLocale: string;
87
+ /** The live footer while nothing has been paid yet. */
88
+ awaiting: string;
89
+ /** The order came back with no PIX charge on it — nothing to show. */
90
+ chargeMissing: string;
91
+ }
92
+
93
+ /** The card pane's own heading. Everything else in it is {@link SettlingCopy}. */
94
+ export interface CardPaneCopy {
95
+ heading: string;
96
+ }
97
+
98
+ /** Who is paying, as the checkout has understood it. */
99
+ export interface PayerSummaryCopy {
100
+ taxId(formatted: string): string;
101
+ /** The store already holds this CPF — nothing to re-enter. */
102
+ taxIdAlreadyKnown: string;
103
+ payingAs(name: string): string;
104
+ payingWithSavedDetails: string;
105
+ /** Reopen the buyer-details step to pay as somebody else. */
106
+ changeAction: string;
107
+ }
108
+
109
+ /** The refusal panel, and what a buyer can do about it. */
110
+ export interface PaymentErrorCopy {
111
+ confirming: string;
112
+ cannotContinue: string;
113
+ /** Only offered for a refusal that is SAFE to retry — never an unresolved one. */
114
+ retryAction: string;
115
+ emailLabel: string;
116
+ /** Why the store's own address will not do. */
117
+ emailMustDifferHint: string;
118
+ useEmailAction: string;
119
+ }
120
+
121
+ /** The two wallet buttons — what they say when they cannot run. */
122
+ export interface WalletCopy {
123
+ applePay: {
124
+ /** The line item Apple's own sheet shows above the amount. */
125
+ orderTotal: string;
126
+ /** This store cannot offer Apple Pay at all. */
127
+ cannotStart: string;
128
+ /** It started and failed. */
129
+ cannotComplete: string;
130
+ payAction: string;
131
+ };
132
+ googlePay: {
133
+ cannotComplete: string;
134
+ /**
135
+ * The language Google draws its button's own label in. Google owns those
136
+ * pixels, so this is the only say a host has over them.
137
+ */
138
+ buttonLocale: string;
139
+ };
140
+ /** The divider under the wallet buttons, pointing at the form below. */
141
+ orPayWithCard: string;
142
+ }
143
+
144
+ /** The interstitial before a buyer leaves for a provider's own page. */
145
+ export interface HostedHandoverCopy {
146
+ /**
147
+ * Where they are going. Named when the adapter says who it is, generic
148
+ * otherwise — "a página de pagamento do provedor" is the same sentence with
149
+ * the one useful word removed.
150
+ */
151
+ destinationNamed(displayName: string): string;
152
+ destinationGeneric: string;
153
+ /** The methods the hosted page will offer, when we know them. */
154
+ methodsChoice(methods: string): string;
155
+ /** Which methods those are — read from what the provider actually declared. */
156
+ pixAndCard: string;
157
+ pixOnly: string;
158
+ cardOnly: string;
159
+ /** The whole sentence, composed from the two above. */
160
+ handoff(destination: string, choice: string): string;
161
+ /** What happens after they pay over there. */
162
+ afterwards: string;
163
+ startAction: string;
164
+ preparing: string;
165
+ }
166
+
167
+ /** What the browser says when the wire itself failed. */
168
+ export interface CheckoutTransportCopy {
169
+ /** A refusal whose envelope carried no sentence of its own. */
170
+ failed: string;
171
+ /** A 2xx that was not the shape the route promises. */
172
+ invalidResponse: string;
173
+ /** The request never got out — the one of the three a buyer really meets. */
174
+ offline: string;
175
+ }
176
+
177
+ /** The buyer-details validators, field by field. */
178
+ export interface CheckoutValidationCopy {
179
+ taxIdInvalid: string;
180
+ nameRequired: string;
181
+ emailInvalid: string;
182
+ phoneInvalid: string;
183
+ /** Any other declared field the store demands and the buyer left empty. */
184
+ required: string;
185
+ }
186
+
187
+ /** Everything above, in the shape the checkout context carries. */
188
+ export interface CheckoutScreensCopy {
189
+ method: MethodPickerCopy;
190
+ settling: SettlingCopy;
191
+ pix: PixPaneCopy;
192
+ card: CardPaneCopy;
193
+ payer: PayerSummaryCopy;
194
+ error: PaymentErrorCopy;
195
+ wallet: WalletCopy;
196
+ hosted: HostedHandoverCopy;
197
+ transport: CheckoutTransportCopy;
198
+ validation: CheckoutValidationCopy;
199
+ /** The step wrapper's own wait, while the charge is being raised. */
200
+ generatingPayment: string;
201
+ }
@@ -0,0 +1,101 @@
1
+ import type { CheckoutScreensCopy } from './screens-copy';
2
+
3
+ /**
4
+ * The pt-BR pack for the buyer's checkout screens — a NAMED constant a host
5
+ * passes by hand, never a default.
6
+ *
7
+ * The filename is what exempts this file from the copy-portability gate:
8
+ * Portuguese may ship, it may not be silent. Every sentence is VERBATIM what
9
+ * the screens used to render, so a host adopting it sees no change — what
10
+ * changes is that the words are chosen in a diff.
11
+ */
12
+ export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
13
+ method: {
14
+ groupLabel: 'Forma de pagamento',
15
+ pixLabel: 'PIX',
16
+ cardLabel: 'Cartão',
17
+ pixDescription: 'Aprovação imediata',
18
+ cardDescription: 'Crédito à vista',
19
+ unavailableHere: 'Indisponível nesta loja',
20
+ },
21
+ settling: {
22
+ cannotConfirm: 'Não foi possível confirmar o pagamento',
23
+ takingLonger: 'O pagamento está demorando mais que o esperado',
24
+ takingLongerHelp:
25
+ 'Você pode aguardar ou verificar seu pedido em instantes — não realize um novo pagamento.',
26
+ processing: 'Processando pagamento…',
27
+ confirming: 'Estamos confirmando seu pagamento',
28
+ cannotPay: 'Não foi possível pagar',
29
+ },
30
+ pix: {
31
+ heading: 'Pague com PIX',
32
+ instructions: (totalLabel) =>
33
+ `Escaneie o QR code no app do seu banco ou copie o código. Total ${totalLabel}.`,
34
+ qrAlt: 'QR Code PIX para pagamento',
35
+ copyAction: 'Copiar',
36
+ copiedAction: 'Copiado!',
37
+ validUntil: (time) => `Válido até ${time}. A confirmação é automática.`,
38
+ expiryLocale: 'pt-BR',
39
+ awaiting: 'Aguardando pagamento…',
40
+ chargeMissing: 'Não foi possível gerar o código PIX.',
41
+ },
42
+ card: {
43
+ heading: 'Pague com cartão',
44
+ },
45
+ payer: {
46
+ taxId: (formatted) => `CPF ${formatted}`,
47
+ taxIdAlreadyKnown: 'CPF já cadastrado',
48
+ payingAs: (name) => `Pagando como ${name}`,
49
+ payingWithSavedDetails: 'Pagando com os seus dados salvos',
50
+ changeAction: 'Alterar',
51
+ },
52
+ error: {
53
+ confirming: 'Estamos confirmando seu pagamento',
54
+ cannotContinue: 'Não foi possível continuar',
55
+ retryAction: 'Tentar novamente',
56
+ emailLabel: 'E-mail para o pagamento',
57
+ emailMustDifferHint: 'use um e-mail diferente do da loja',
58
+ useEmailAction: 'Usar este e-mail e continuar',
59
+ },
60
+ wallet: {
61
+ applePay: {
62
+ orderTotal: 'Total do pedido',
63
+ cannotStart: 'Não foi possível iniciar o Apple Pay nesta loja. Pague com cartão.',
64
+ cannotComplete:
65
+ 'Não foi possível iniciar o Apple Pay. Tente novamente ou pague com cartão.',
66
+ payAction: 'Pagar com Apple Pay',
67
+ },
68
+ googlePay: {
69
+ cannotComplete:
70
+ 'Não foi possível concluir o pagamento com o Google Pay. Tente novamente ou pague com cartão.',
71
+ buttonLocale: 'pt',
72
+ },
73
+ orPayWithCard: 'ou pague com cartão',
74
+ },
75
+ hosted: {
76
+ destinationNamed: (displayName) => `à página de pagamento da ${displayName}`,
77
+ destinationGeneric: 'à página de pagamento segura do provedor',
78
+ methodsChoice: (methods) => `, onde você escolhe pagar com ${methods}`,
79
+ pixAndCard: 'PIX ou cartão',
80
+ pixOnly: 'PIX',
81
+ cardOnly: 'cartão',
82
+ handoff: (destination, choice) => `Você será levado ${destination}${choice}.`,
83
+ afterwards:
84
+ 'Assim que o pagamento for concluído, você volta para cá e nós confirmamos o pedido.',
85
+ startAction: 'Seguir para o pagamento',
86
+ preparing: 'Preparando o pagamento',
87
+ },
88
+ transport: {
89
+ failed: 'Não foi possível concluir a operação. Tente novamente.',
90
+ invalidResponse: 'Resposta inválida do servidor.',
91
+ offline: 'Não foi possível conectar. Verifique sua conexão e tente novamente.',
92
+ },
93
+ validation: {
94
+ taxIdInvalid: 'CPF inválido.',
95
+ nameRequired: 'Informe seu nome.',
96
+ emailInvalid: 'E-mail inválido.',
97
+ phoneInvalid: 'Telefone inválido.',
98
+ required: 'Campo obrigatório.',
99
+ },
100
+ generatingPayment: 'Gerando pagamento…',
101
+ };
@@ -20,6 +20,8 @@ import type { BuyerVaultSession, VaultedCardDisplay } from "@12-apps/payments-ba
20
20
  import type { SavedCard } from "../../card";
21
21
  import { err, ok, type Result } from "../../result";
22
22
 
23
+ import type { CheckoutTransportCopy } from "./screens-copy";
24
+
23
25
  import type {
24
26
  ChargeCardInput,
25
27
  ChargeOutcome,
@@ -59,6 +61,16 @@ export const DEFAULT_CHECKOUT_BASE_URL = "/api/checkout";
59
61
 
60
62
  /** Where the `createPaymentFlowsBE` mount lives, and how to reach it. */
61
63
  export interface CheckoutTransport {
64
+ /**
65
+ * What the buyer reads when the WIRE failed and the server sent no sentence
66
+ * of its own (FUT-760) — required, and with no default.
67
+ *
68
+ * Three sentences, and only one of them is common: `offline`, which is what
69
+ * a buyer on a dropped connection sees. That is exactly why it may not be
70
+ * this package's Portuguese — it is the failure most likely to be the first
71
+ * thing an adopter's shopper ever reads from us.
72
+ */
73
+ copy: CheckoutTransportCopy;
62
74
  /**
63
75
  * Prefix for `/config`, `/status`, `/charge`, `/cards`, `/cards/begin`,
64
76
  * `/cards/complete`, `/refresh-key`.
@@ -73,6 +85,17 @@ export interface CheckoutTransport {
73
85
  headers?: () => HeadersInit | Promise<HeadersInit>;
74
86
  }
75
87
 
88
+ /**
89
+ * A transport whose WORDS some surrounding config already answers.
90
+ *
91
+ * `createPaymentFlows` takes one: its `copy` covers the whole checkout, so
92
+ * asking a host to repeat the three transport sentences beside `fetchImpl`
93
+ * would be the same question twice. Naming `copy` here still wins, for a mount
94
+ * that talks to a different surface in a different voice.
95
+ */
96
+ export type CheckoutTransportBinding = Omit<CheckoutTransport, "copy"> &
97
+ Partial<Pick<CheckoutTransport, "copy">>;
98
+
76
99
  /** The eight calls the buyer checkout makes, pre-bound to a {@link CheckoutTransport}. */
77
100
  export interface CheckoutClient {
78
101
  getConfig(tenantSlug: string): Promise<Result<CheckoutProviderConfig>>;
@@ -115,8 +138,8 @@ interface ApiEnvelope<T> {
115
138
  * The message is what the buyer reads; the code is what a surface uses to
116
139
  * decide how to PRESENT it, which a message cannot be parsed for.
117
140
  */
118
- function refused<T>(json: ApiEnvelope<T> | null): Result<T> {
119
- return err(json?.error ?? "Não foi possível concluir a operação. Tente novamente.", json?.code);
141
+ function refused<T>(json: ApiEnvelope<T> | null, copy: CheckoutTransportCopy): Result<T> {
142
+ return err(json?.error ?? copy.failed, json?.code);
120
143
  }
121
144
 
122
145
  /**
@@ -200,11 +223,14 @@ function completeVaultBody(input: CompleteVaultInput): string {
200
223
  /**
201
224
  * The checkout calls, bound to one transport.
202
225
  *
203
- * Passing no transport reproduces exactly what the free functions in
204
- * `client.ts` have always done: `/api/checkout/**` on the ambient `fetch`.
226
+ * `baseUrl` and `fetchImpl` still default to what the free functions in
227
+ * `client.ts` have always done `/api/checkout/**` on the ambient `fetch`.
228
+ * `copy` does NOT default, because there is no language this package could
229
+ * pick that would be right for the next host (FUT-760).
205
230
  */
206
- export function createCheckoutClient(transport: CheckoutTransport = {}): CheckoutClient {
231
+ export function createCheckoutClient(transport: CheckoutTransport): CheckoutClient {
207
232
  const baseUrl = transport.baseUrl ?? DEFAULT_CHECKOUT_BASE_URL;
233
+ const copy = transport.copy;
208
234
 
209
235
  /** Call a checkout route and normalize the response into a {@link Result}. */
210
236
  async function call<T>(route: string, init?: RequestInit): Promise<Result<T>> {
@@ -218,13 +244,13 @@ export function createCheckoutClient(transport: CheckoutTransport = {}): Checkou
218
244
  headers: { "Content-Type": "application/json", ...extra, ...init?.headers },
219
245
  });
220
246
  const json = (await res.json().catch(() => null)) as ApiEnvelope<T> | null;
221
- if (!res.ok) return refused(json);
247
+ if (!res.ok) return refused(json, copy);
222
248
  if (!json || json.data === undefined) {
223
- return err(json?.error ?? "Resposta inválida do servidor.");
249
+ return err(json?.error ?? copy.invalidResponse);
224
250
  }
225
251
  return ok(json.data);
226
252
  } catch {
227
- return err("Não foi possível conectar. Verifique sua conexão e tente novamente.");
253
+ return err(copy.offline);
228
254
  }
229
255
  }
230
256
 
@@ -28,6 +28,8 @@ import { useCheckoutNavigate, type CheckoutNavigate } from "./navigate-context";
28
28
  import type { CardChainLink } from "./method-capability";
29
29
  import type { BuyerInfo, CheckoutOrder, OrderStatus } from "./types";
30
30
  import { usePaymentPolling } from "./use-payment-polling";
31
+ import type { CardCopy } from "../../card/copy";
32
+ import { useCheckoutCopy } from "./copy-context";
31
33
 
32
34
  const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
33
35
 
@@ -190,6 +192,8 @@ async function resolveInstruments(input: {
190
192
  providerChain: readonly CardChainLink[];
191
193
  /** The bound key-refresh call (FUT-741) — the self-heal must hit OUR mount. */
192
194
  refreshKey: RefreshBrowserKey;
195
+ /** The words a failed mint reports with — the host's (FUT-760). */
196
+ copy: CardCopy;
193
197
  }): Promise<Result<CardInstruments>> {
194
198
  const { form } = input;
195
199
  if (!form.usingNewCard) return ok({ token: form.selection });
@@ -200,7 +204,7 @@ async function resolveInstruments(input: {
200
204
  input.onKeyRefreshed,
201
205
  form.saveCard,
202
206
  input.providerChain,
203
- undefined,
207
+ input.copy,
204
208
  input.refreshKey,
205
209
  );
206
210
  }
@@ -228,6 +232,7 @@ function useCardSubmit(
228
232
  const { publicKey, setPublicKey } = useCardPublicKey(order.orderId, providerConfig);
229
233
  const client = useCheckoutClientApi();
230
234
  const navigate = useCheckoutNavigate();
235
+ const cardCopy = useCheckoutCopy().card;
231
236
 
232
237
  const { status, error: pollError, timedOut: pollTimedOut } = usePaymentPolling(order.orderId, {
233
238
  enabled: submitted,
@@ -252,6 +257,7 @@ function useCardSubmit(
252
257
  onKeyRefreshed: setPublicKey,
253
258
  providerChain,
254
259
  refreshKey: client.refreshBrowserKey,
260
+ copy: cardCopy,
255
261
  });
256
262
  if (!resolved.ok) {
257
263
  setError(resolved.error);
@@ -315,11 +321,13 @@ export function useCardCheckout(
315
321
  const brand = detectBrand(onlyDigits(card.number));
316
322
  const usingNewCard = selection === NEW_CARD;
317
323
 
324
+ const cardCopy = useCheckoutCopy().card;
325
+ const fieldCopy = cardCopy.fields;
318
326
  const validate = (): CardFieldErrors => ({
319
- number: validateCardNumber(card.number),
320
- holder: validateHolder(card.holder),
321
- expiry: validateExpiry(card.expiry),
322
- cvv: validateCvv(card.cvv, brand),
327
+ number: validateCardNumber(card.number, fieldCopy),
328
+ holder: validateHolder(card.holder, fieldCopy),
329
+ expiry: validateExpiry(card.expiry, fieldCopy),
330
+ cvv: validateCvv(card.cvv, fieldCopy, brand),
323
331
  });
324
332
 
325
333
  const submit = useCardSubmit(
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState, type Dispatch, type SetStateAction } from "react";
2
2
 
3
- import { buyerFormComplete } from "./buyer-info-form";
3
+ import { buyerGateError } from "./buyer-gate";
4
+ import { useCheckoutCopy } from "./copy-context";
4
5
 
5
6
  import { rememberHostedOrder, takeHostedOrder } from "./hosted-return";
6
7
  import { useCheckoutNavigate, type CheckoutNavigate } from "./navigate-context";
@@ -88,51 +89,6 @@ function useCheckoutNav(
88
89
  return { back, editBuyer: taxIdOnFile ? openDados : undefined };
89
90
  }
90
91
 
91
- /** The message shown when a declared field is missing or malformed. */
92
- const FIELD_COMPLAINT: Record<string, string> = {
93
- taxId: "CPF inválido.",
94
- name: "Informe seu nome.",
95
- email: "E-mail inválido.",
96
- phone: "Telefone inválido.",
97
- };
98
-
99
- /** Which input to highlight for a declared key. */
100
- const FIELD_INPUT: Record<string, BuyerField> = {
101
- taxId: "cpf",
102
- name: "name",
103
- email: "email",
104
- phone: "phone",
105
- };
106
-
107
- /**
108
- * What the "Continuar" gate objects to, or undefined to advance.
109
- *
110
- * The fields come from the chain's own declaration (FUT-595) — absent, they
111
- * degrade to CPF-required, which is exactly what this gate has always demanded.
112
- *
113
- * A blank CPF is only an error when the store has NO CPF for this buyer. With
114
- * one on file the field starts empty by design (the client is never sent the
115
- * saved CPF), so demanding one here trapped a returning buyer who opened Dados
116
- * through "Alterar" and changed their mind: they could not reach Pagamento
117
- * again, and back only led out to the menu. Leaving it blank means "charge me
118
- * as before", which is exactly what the server's `resolveBuyerTaxId` does.
119
- */
120
- function buyerGateError(
121
- buyer: BuyerInfo,
122
- fields: readonly CheckoutCustomerField[],
123
- taxIdOnFile: boolean,
124
- ): { message: string; field: BuyerField } | undefined {
125
- const effective = taxIdOnFile
126
- ? fields.filter((field) => !(field.key === "taxId" && !buyer.taxId?.trim()))
127
- : fields;
128
- const offending = buyerFormComplete(buyer, effective);
129
- if (!offending) return undefined;
130
- return {
131
- message: FIELD_COMPLAINT[offending.key] ?? "Campo obrigatório.",
132
- field: FIELD_INPUT[offending.key] ?? "cpf",
133
- };
134
- }
135
-
136
92
  /**
137
93
  * Where checkout opens: on the outcome for a buyer returning from a hosted
138
94
  * provider, on Pagamento when their CPF is already on file, else on Dados.
@@ -319,6 +275,7 @@ export function useCheckoutController(
319
275
  tenantSlug?: string,
320
276
  ) {
321
277
  const { createOrder, saveBuyerContact, onExitToMenu, onPaid } = ports;
278
+ const validation = useCheckoutCopy().screens.validation;
322
279
  const navigate = useCheckoutNavigate();
323
280
  const resume = useHostedResume(tenantSlug);
324
281
  const [step, setStep] = useState<Step>(initialStep(Boolean(resume.order), taxIdOnFile));
@@ -341,7 +298,7 @@ export function useCheckoutController(
341
298
  }, [clearError]);
342
299
  const goToPayment = useCallback(() => {
343
300
  clearError();
344
- const complaint = buyerGateError(buyer, buyerFields, taxIdOnFile);
301
+ const complaint = buyerGateError(validation, buyer, buyerFields, taxIdOnFile);
345
302
  if (complaint) { failure.fail(complaint); return; }
346
303
  // Persist the buyer's details HERE, on "Continuar" — not when a payment is
347
304
  // raised. Everything after this step can fail (no provider configured, a
@@ -354,7 +311,7 @@ export function useCheckoutController(
354
311
  saveBuyerContact?.({ name: buyer.name, phone: buyer.phone, taxId: buyer.taxId });
355
312
  }
356
313
  setStep("payment");
357
- }, [buyer, buyerFields, saveProfile, taxIdOnFile, clearError, saveBuyerContact]);
314
+ }, [buyer, buyerFields, saveProfile, taxIdOnFile, clearError, saveBuyerContact, validation]);
358
315
  const startPayment = useCallback(async (chosen: PaymentMethod, override?: BuyerInfo) => {
359
316
  clearError();
360
317
  setCreating(true);
@@ -1,3 +1,5 @@
1
+ import type { CheckoutCopy } from "./copy-context";
2
+
1
3
  /**
2
4
  * Every string the legacy checkout views render — required props, with NO
3
5
  * defaults, deliberately (the payments extraction's own doctrine, FUT-760):
@@ -87,4 +89,15 @@ export interface CheckoutViewCopy {
87
89
  dados: DadosStepCopy;
88
90
  emptyCart: EmptyCartCopy;
89
91
  status: PaymentStatusCopy;
92
+ /**
93
+ * The words the screens BELOW these read — the card fields, the wallet
94
+ * panes, the buyer-details inputs (FUT-760).
95
+ *
96
+ * Carried here rather than as a second prop so a host still passes copy
97
+ * exactly once. `CheckoutFlow` mounts it as `CheckoutCopyProvider`, because
98
+ * threading it down through four intermediate components as props is how a
99
+ * copy port comes to exist, be required, and go unread — which is what
100
+ * happened to `unavailableWithRemedyTitle` before this port was finished.
101
+ */
102
+ screens: CheckoutCopy;
90
103
  }
@@ -3,6 +3,7 @@ import { useState, type JSX } from "react";
3
3
 
4
4
  import { ApplePayButton, applePaySupported } from "./apple-pay-button";
5
5
  import { CardView } from "./card-view";
6
+ import { useCheckoutCopy } from "./copy-context";
6
7
  import { GooglePayButton } from "./google-pay-button";
7
8
  import {
8
9
  applePayDeclared,
@@ -37,11 +38,12 @@ type WalletPaneProps = ProviderCheckoutScreenProps & {
37
38
  /** Post-submit confirmation, error > timeout > spinner — the card view's order. */
38
39
  function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
39
40
  const { Alert, LoadingState } = useCheckoutComponents();
41
+ const copy = useCheckoutCopy().screens.settling;
40
42
  if (wallet.pollError) {
41
43
  return (
42
44
  <Alert
43
45
  variant="danger"
44
- title="Não foi possível confirmar o pagamento"
46
+ title={copy.cannotConfirm}
45
47
  description={wallet.pollError}
46
48
  showIcon
47
49
  data-testid="wallet-poll-error"
@@ -52,8 +54,8 @@ function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
52
54
  return (
53
55
  <Alert
54
56
  variant="warning"
55
- title="O pagamento está demorando mais que o esperado"
56
- description="Você pode aguardar ou verificar seu pedido em instantes — não realize um novo pagamento."
57
+ title={copy.takingLonger}
58
+ description={copy.takingLongerHelp}
57
59
  showIcon
58
60
  data-testid="wallet-poll-timeout"
59
61
  />
@@ -63,7 +65,7 @@ function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
63
65
  <LoadingState
64
66
  variant="spinner"
65
67
  size="md"
66
- message="Processando pagamento…"
68
+ message={copy.processing}
67
69
  dataTestId="wallet-processing"
68
70
  />
69
71
  );
@@ -77,10 +79,11 @@ function WalletProcessing({ wallet }: { wallet: WalletCharge }): JSX.Element {
77
79
  */
78
80
  function WalletUnresolved({ message }: { message: string }): JSX.Element {
79
81
  const { Alert } = useCheckoutComponents();
82
+ const copy = useCheckoutCopy().screens.settling;
80
83
  return (
81
84
  <Alert
82
85
  variant="warning"
83
- title="Estamos confirmando seu pagamento"
86
+ title={copy.confirming}
84
87
  description={message}
85
88
  showIcon
86
89
  data-testid="wallet-unresolved"
@@ -105,6 +108,7 @@ function WalletButtons({
105
108
  onSheetError: (message: string) => void;
106
109
  }): JSX.Element | null {
107
110
  const { Text } = useCheckoutComponents();
111
+ const copy = useCheckoutCopy().screens.wallet;
108
112
  const [googleReady, setGoogleReady] = useState(false);
109
113
  const googlePay = googlePayConfig(props.config);
110
114
  const applePay = applePayDeclared(props.config) && applePaySupported();
@@ -131,7 +135,7 @@ function WalletButtons({
131
135
  {applePay || googleReady ? (
132
136
  <Divider>
133
137
  <Text variant="caption" size="xs" color="secondary" as="span">
134
- ou pague com cartão
138
+ {copy.orPayWithCard}
135
139
  </Text>
136
140
  </Divider>
137
141
  ) : null}
@@ -142,6 +146,7 @@ function WalletButtons({
142
146
  /** The CARD pane: wallet fast lane above, the card form below. */
143
147
  export function WalletCardPane(props: WalletPaneProps): JSX.Element {
144
148
  const { Alert } = useCheckoutComponents();
149
+ const copy = useCheckoutCopy().screens.settling;
145
150
  const { order, buyer, config, tenantSlug, onResolved, pollIntervalMs } = props;
146
151
  const wallet = useWalletCharge(order, buyer, onResolved, pollIntervalMs);
147
152
  // A sheet failure the wallet reported before any charge existed (pay.js
@@ -158,7 +163,7 @@ export function WalletCardPane(props: WalletPaneProps): JSX.Element {
158
163
  {wallet.error ? (
159
164
  <Alert
160
165
  variant="danger"
161
- title="Não foi possível pagar"
166
+ title={copy.cannotPay}
162
167
  description={wallet.error}
163
168
  showIcon
164
169
  data-testid="wallet-error"
@@ -167,7 +172,7 @@ export function WalletCardPane(props: WalletPaneProps): JSX.Element {
167
172
  {sheetError && !wallet.error ? (
168
173
  <Alert
169
174
  variant="danger"
170
- title="Não foi possível pagar"
175
+ title={copy.cannotPay}
171
176
  description={sheetError}
172
177
  showIcon
173
178
  data-testid="wallet-sheet-error"