@12-apps/payments-frontend 3.9.0 → 3.11.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 (73) 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 +117 -0
  5. package/src/card/cpf.ts +12 -5
  6. package/src/card/fields.tsx +17 -12
  7. package/src/card/format.ts +33 -17
  8. package/src/card/index.ts +3 -1
  9. package/src/card/pt-BR.ts +52 -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 +24 -19
  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/ProviderSetupGuide.tsx +3 -1
  26. package/src/components/ProviderStatusBar.tsx +24 -24
  27. package/src/components/SetupGuideSection.tsx +15 -6
  28. package/src/components/checkout/apple-pay-button.tsx +19 -9
  29. package/src/components/checkout/buyer-fields.ts +5 -3
  30. package/src/components/checkout/buyer-gate.ts +68 -0
  31. package/src/components/checkout/buyer-info-form.tsx +14 -15
  32. package/src/components/checkout/card-instruments.ts +31 -12
  33. package/src/components/checkout/card-view.tsx +11 -7
  34. package/src/components/checkout/checkout-flow.tsx +4 -1
  35. package/src/components/checkout/checkout-steps.tsx +3 -1
  36. package/src/components/checkout/client-context.tsx +28 -16
  37. package/src/components/checkout/client.ts +43 -16
  38. package/src/components/checkout/copy-context.tsx +83 -0
  39. package/src/components/checkout/google-pay-button.tsx +19 -7
  40. package/src/components/checkout/method-picker.tsx +34 -10
  41. package/src/components/checkout/payer-summary.tsx +8 -6
  42. package/src/components/checkout/payment-error-panel.tsx +8 -5
  43. package/src/components/checkout/pix-view.tsx +16 -9
  44. package/src/components/checkout/providers/hosted-link.tsx +26 -16
  45. package/src/components/checkout/pt-BR.ts +27 -0
  46. package/src/components/checkout/screens-copy.ts +201 -0
  47. package/src/components/checkout/screens-pt-BR.ts +101 -0
  48. package/src/components/checkout/transport.ts +34 -8
  49. package/src/components/checkout/use-card-checkout.ts +13 -5
  50. package/src/components/checkout/use-checkout-controller.ts +5 -48
  51. package/src/components/checkout/view-copy.ts +13 -0
  52. package/src/components/checkout/wallet-pane.tsx +13 -8
  53. package/src/components/checkout-payment-copy.ts +88 -0
  54. package/src/components/checkout-payment-pt-BR.ts +50 -0
  55. package/src/components/checkout-payment-types.ts +25 -0
  56. package/src/components/checkout-payment-views.tsx +225 -0
  57. package/src/components/connection-state.ts +22 -10
  58. package/src/components/credential-rules.ts +8 -3
  59. package/src/components/platform/ConnectApplicationPanel.tsx +6 -2
  60. package/src/components/platform/ConnectEnvironmentCard.tsx +19 -15
  61. package/src/components/platform/HomologacaoGuideCard.tsx +23 -19
  62. package/src/components/platform/HomologacaoOutcomeCard.tsx +24 -16
  63. package/src/components/platform/PlatformHomologacao.tsx +12 -8
  64. package/src/components/settings-copy-context.tsx +50 -0
  65. package/src/components/settings-copy.ts +312 -0
  66. package/src/components/settings-pt-BR.ts +172 -0
  67. package/src/flows/copy.ts +12 -0
  68. package/src/flows/create-payment-flows.tsx +8 -1
  69. package/src/flows/runtime.tsx +12 -4
  70. package/src/flows/screens-vault.tsx +10 -10
  71. package/src/flows/types.ts +5 -2
  72. package/src/flows/use-add-card.ts +10 -5
  73. package/src/index.ts +63 -0
@@ -26,6 +26,7 @@ import { err, ok, type Result } from "../result";
26
26
  import { detectBrand, onlyDigits } from "./format";
27
27
  import { tokenizeWithStripe } from "./stripe-token";
28
28
  import type { CardDetails, CardToken } from "./types";
29
+ import type { CardCopy } from "./copy";
29
30
 
30
31
  /** Test PAN that always declines (mirrors common sandbox decline cards). */
31
32
  const DECLINE_PAN = "4000000000000002";
@@ -80,11 +81,11 @@ function ensurePagBankSdk(): Promise<boolean> {
80
81
  }
81
82
 
82
83
  /** Local card-field validation. Returns an error message, or `null` when valid. */
83
- function validateCardInput(card: CardDetails): string | null {
84
- if (!passesLuhn(onlyDigits(card.number))) return "Número de cartão inválido.";
85
- if (card.holder.trim().length < 2) return "Informe o nome impresso no cartão.";
86
- if (!expiryIsFuture(card.expiry)) return "Validade inválida ou expirada.";
87
- if (!/^\d{3,4}$/.test(card.cvv.trim())) return "CVV inválido.";
84
+ function validateCardInput(card: CardDetails, copy: CardCopy): string | null {
85
+ if (!passesLuhn(onlyDigits(card.number))) return copy.fields.numberInvalid;
86
+ if (card.holder.trim().length < 2) return copy.fields.holderRequired;
87
+ if (!expiryIsFuture(card.expiry)) return copy.fields.expiryInvalid;
88
+ if (!/^\d{3,4}$/.test(card.cvv.trim())) return copy.fields.cvvInvalid;
88
89
  return null;
89
90
  }
90
91
 
@@ -98,12 +99,13 @@ function encryptWithSdk(
98
99
  publicKey: string,
99
100
  brand: string,
100
101
  last4: string,
102
+ copy: CardCopy,
101
103
  ): Result<CardToken> {
102
104
  if (typeof window === "undefined" || !window.PagSeguro?.encryptCard) {
103
- return err("Não foi possível carregar o meio de pagamento. Recarregue a página.");
105
+ return err(copy.tokenize.sdkUnavailable);
104
106
  }
105
107
  const match = /^(\d{2})\/(\d{2})$/.exec(card.expiry.trim());
106
- if (!match) return err("Validade inválida ou expirada.");
108
+ if (!match) return err(copy.fields.expiryInvalid);
107
109
  const encrypted = window.PagSeguro.encryptCard({
108
110
  publicKey,
109
111
  holder: card.holder.trim(),
@@ -113,7 +115,7 @@ function encryptWithSdk(
113
115
  securityCode: card.cvv.trim(),
114
116
  });
115
117
  if (encrypted.hasErrors || !encrypted.encryptedCard) {
116
- return err("Não foi possível processar o cartão. Verifique os dados e tente novamente.");
118
+ return err(copy.tokenize.cardNotProcessed);
117
119
  }
118
120
  return ok({ token: encrypted.encryptedCard, brand, last4 });
119
121
  }
@@ -157,11 +159,12 @@ async function tokenizeWithPagarme(
157
159
  publicKey: string,
158
160
  brand: string,
159
161
  last4: string,
162
+ copy: CardCopy,
160
163
  /** Deadline for the round trip; an abort reads as "could not contact". */
161
164
  signal?: AbortSignal,
162
165
  ): Promise<Result<CardToken>> {
163
166
  const match = /^(\d{2})\/(\d{2})$/.exec(card.expiry.trim());
164
- if (!match) return err("Validade inválida ou expirada.");
167
+ if (!match) return err(copy.fields.expiryInvalid);
165
168
 
166
169
  let response: Response;
167
170
  try {
@@ -181,7 +184,7 @@ async function tokenizeWithPagarme(
181
184
  }),
182
185
  });
183
186
  } catch {
184
- return err("Não foi possível contatar o provedor do cartão. Verifique sua conexão.");
187
+ return err(copy.tokenize.providerUnreachable);
185
188
  }
186
189
 
187
190
  const body = (await response.json().catch(() => null)) as { id?: unknown } | null;
@@ -189,8 +192,7 @@ async function tokenizeWithPagarme(
189
192
  // Carried verbatim: a rejected tokenization is the provider's answer, and
190
193
  // the whole point of the activation screen is that it reaches a human.
191
194
  return err(
192
- `O provedor recusou os dados do cartão (HTTP ${response.status}). ` +
193
- `Resposta: ${JSON.stringify(body).slice(0, 300)}`,
195
+ copy.tokenize.providerRefused(response.status, JSON.stringify(body).slice(0, 300)),
194
196
  );
195
197
  }
196
198
  return ok({ token: body.id, brand, last4 });
@@ -205,6 +207,7 @@ async function tokenizeWithPagarme(
205
207
  export async function tokenizeCard(
206
208
  card: CardDetails,
207
209
  publicKey: string | null | undefined,
210
+ copy: CardCopy,
208
211
  tokenizer: CardTokenizer = "pagbank-sdk",
209
212
  /**
210
213
  * Bounds the network schemes (Pagar.me / Stripe). The PagBank one encrypts
@@ -212,31 +215,24 @@ export async function tokenizeCard(
212
215
  */
213
216
  signal?: AbortSignal,
214
217
  ): Promise<Result<CardToken>> {
215
- const validationError = validateCardInput(card);
218
+ const validationError = validateCardInput(card, copy);
216
219
  if (validationError) return err(validationError);
217
220
 
218
- if (!publicKey) {
219
- return err(
220
- "A chave pública do cartão não está disponível para esta loja. " +
221
- "Reconecte o provedor e tente novamente.",
222
- );
223
- }
221
+ if (!publicKey) return err(copy.tokenize.noPublicKey);
224
222
 
225
223
  const pan = onlyDigits(card.number);
226
224
  const brand = detectBrand(pan);
227
225
  const last4 = pan.slice(-4);
228
226
 
229
227
  if (tokenizer === "pagarme-token") {
230
- return tokenizeWithPagarme(card, pan, publicKey, brand, last4, signal);
228
+ return tokenizeWithPagarme(card, pan, publicKey, brand, last4, copy, signal);
231
229
  }
232
230
  if (tokenizer === "stripe-pm") {
233
- return tokenizeWithStripe(card, pan, publicKey, brand, last4, signal);
231
+ return tokenizeWithStripe(card, pan, publicKey, brand, last4, copy, signal);
234
232
  }
235
233
 
236
- if (!(await ensurePagBankSdk())) {
237
- return err("Não foi possível carregar o meio de pagamento. Recarregue a página.");
238
- }
239
- return encryptWithSdk(card, pan, publicKey, brand, last4);
234
+ if (!(await ensurePagBankSdk())) return err(copy.tokenize.sdkUnavailable);
235
+ return encryptWithSdk(card, pan, publicKey, brand, last4, copy);
240
236
  }
241
237
 
242
238
  /**
@@ -259,10 +255,6 @@ export interface CardTokenizationConfig {
259
255
  mockTokenization: boolean;
260
256
  }
261
257
 
262
- /** The clear bail (FUT-697): said BEFORE any charge, naming the remedy. */
263
- const CARD_PATH_UNAVAILABLE =
264
- "O pagamento com cartão está indisponível nesta loja no momento. Recarregue a página e tente de novo, escolha outro método de pagamento ou combine diretamente com a loja.";
265
-
266
258
  /**
267
259
  * Tokenize for the buyer checkout, speaking the ACTIVE provider's protocol.
268
260
  *
@@ -282,6 +274,7 @@ const CARD_PATH_UNAVAILABLE =
282
274
  export async function tokenizeForCheckout(
283
275
  card: CardDetails,
284
276
  config: CardTokenizationConfig,
277
+ copy: CardCopy,
285
278
  /**
286
279
  * Optional deadline for a provider that mints over the network. The chain
287
280
  * path passes one so a backup acquirer nobody can reach cannot hold the
@@ -290,11 +283,14 @@ export async function tokenizeForCheckout(
290
283
  signal?: AbortSignal,
291
284
  ): Promise<Result<CardToken>> {
292
285
  const scheme = config.provider ? tokenizerFor(config.provider) : null;
293
- if (scheme && config.publicKey) return tokenizeCard(card, config.publicKey, scheme, signal);
286
+ if (scheme && config.publicKey) {
287
+ return tokenizeCard(card, config.publicKey, copy, scheme, signal);
288
+ }
294
289
 
295
- if (!config.mockTokenization) return err(CARD_PATH_UNAVAILABLE);
290
+ // The clear bail (FUT-697): said BEFORE any charge, naming the remedy.
291
+ if (!config.mockTokenization) return err(copy.tokenize.cardUnavailable);
296
292
 
297
- const validationError = validateCardInput(card);
293
+ const validationError = validateCardInput(card, copy);
298
294
  if (validationError) return err(validationError);
299
295
 
300
296
  const pan = onlyDigits(card.number);
@@ -1,17 +1,6 @@
1
1
  'use client';
2
2
 
3
- import {
4
- Alert,
5
- Box,
6
- Button,
7
- CircularProgress,
8
- FormControlLabel,
9
- Radio,
10
- RadioGroup,
11
- Stack,
12
- TextField,
13
- Typography,
14
- } from '@mui/material';
3
+ import { Alert, Button, CircularProgress, Stack, Typography } from '@mui/material';
15
4
  import { useCallback, useEffect, useState } from 'react';
16
5
 
17
6
  import type { ClientChargeView, CustomerInfo, Money } from '@12-apps/payments-backend';
@@ -19,6 +8,12 @@ import type { ClientChargeView, CustomerInfo, Money } from '@12-apps/payments-ba
19
8
  import type { ClientPaymentsConfig, PaymentsClient } from '../client';
20
9
  import { useChargeStatus, useCreateCharge, usePaymentsClient } from '../context';
21
10
 
11
+ import type { CheckoutPaymentCopy, LegacyRefusalCopy } from './checkout-payment-copy';
12
+ import type { CardFormValues, SavedCardOption } from './checkout-payment-types';
13
+ import { formatAmount, MethodChooser, PixPanel } from './checkout-payment-views';
14
+
15
+ export type { CardFormValues, SavedCardOption };
16
+
22
17
  /**
23
18
  * Plug-and-play checkout payment step — the reusable equivalent of the
24
19
  * buyer-facing payment screen. Renders the right flow for the merchant's
@@ -36,22 +31,6 @@ import { useChargeStatus, useCreateCharge, usePaymentsClient } from '../context'
36
31
  * order creation, and what happens on `onPaid` — this component owns only
37
32
  * the payment interaction.
38
33
  */
39
- export interface CardFormValues {
40
- number: string;
41
- holder: string;
42
- expiry: string;
43
- cvv: string;
44
- }
45
-
46
- /** A provider-vaulted card the buyer may reuse ("Mastercard •••• 7599"). */
47
- export interface SavedCardOption {
48
- savedCardToken: string;
49
- brand: string;
50
- last4: string;
51
- /** e.g. "02/2034" — shown as "Validade 02/2034". */
52
- expiry?: string;
53
- }
54
-
55
34
  export interface CheckoutPaymentProps {
56
35
  /**
57
36
  * Opaque host-side handle for what is being paid (cart/order id). Sent as
@@ -72,13 +51,11 @@ export interface CheckoutPaymentProps {
72
51
  savedCards?: SavedCardOption[];
73
52
  onPaid: (charge: ClientChargeView) => void;
74
53
  onFailed?: (charge: ClientChargeView) => void;
75
- }
76
-
77
- function formatBRL(amount: Money): string {
78
- return (amount.amountCents / 100).toLocaleString('pt-BR', {
79
- style: 'currency',
80
- currency: amount.currency,
81
- });
54
+ /**
55
+ * Every sentence this step renders — the HOST's, required and with no
56
+ * default (FUT-760). A pt-BR host passes `PT_BR_CHECKOUT_PAYMENT_COPY`.
57
+ */
58
+ copy: CheckoutPaymentCopy;
82
59
  }
83
60
 
84
61
  function firstError(...messages: (string | null | undefined)[]): string | null {
@@ -118,158 +95,6 @@ function useChargeOutcome(
118
95
  }, [current, settled, onPaid, onFailed]);
119
96
  }
120
97
 
121
- function PixPanel({ charge }: { charge: ClientChargeView }) {
122
- const [copied, setCopied] = useState(false);
123
- if (!charge.pix) return null;
124
- return (
125
- <Stack spacing={1} alignItems="flex-start">
126
- {charge.pix.qrImageUrl ? (
127
- <Box component="img" src={charge.pix.qrImageUrl} alt="QR Code PIX" sx={{ width: 220 }} />
128
- ) : null}
129
- <TextField fullWidth multiline size="small" label="PIX copia e cola" value={charge.pix.qrText} />
130
- <Button
131
- size="small"
132
- onClick={() => {
133
- void navigator.clipboard.writeText(charge.pix?.qrText ?? '').then(() => setCopied(true));
134
- }}
135
- >
136
- {copied ? 'Copiado!' : 'Copiar código'}
137
- </Button>
138
- <Stack direction="row" spacing={1} alignItems="center">
139
- <CircularProgress size={16} />
140
- <Typography variant="body2">Aguardando pagamento…</Typography>
141
- </Stack>
142
- </Stack>
143
- );
144
- }
145
-
146
- function CardPanel({ disabled, onSubmit }: { disabled: boolean; onSubmit: (values: CardFormValues) => void }) {
147
- const [values, setValues] = useState<CardFormValues>({ number: '', holder: '', expiry: '', cvv: '' });
148
- const field = (key: keyof CardFormValues, label: string, width?: number) => (
149
- <TextField
150
- size="small"
151
- label={label}
152
- value={values[key]}
153
- sx={width ? { width } : undefined}
154
- onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
155
- />
156
- );
157
- return (
158
- <Stack spacing={2}>
159
- {field('number', 'Número do cartão')}
160
- {field('holder', 'Nome impresso no cartão')}
161
- <Stack direction="row" spacing={2}>
162
- {field('expiry', 'Validade (MM/AA)', 140)}
163
- {field('cvv', 'CVV', 100)}
164
- </Stack>
165
- <Button variant="contained" disabled={disabled} onClick={() => onSubmit(values)}>
166
- Pagar com cartão
167
- </Button>
168
- </Stack>
169
- );
170
- }
171
-
172
- interface CardSectionProps {
173
- amount: Money;
174
- savedCards: SavedCardOption[];
175
- disabled: boolean;
176
- onNewCard: (values: CardFormValues) => void;
177
- onSavedCard: (card: SavedCardOption) => void;
178
- }
179
-
180
- /** "Pague com cartão": saved cards as radios + "Novo cartão" fallback. */
181
- function CardSection({ amount, savedCards, disabled, onNewCard, onSavedCard }: CardSectionProps) {
182
- const [selected, setSelected] = useState(savedCards[0]?.savedCardToken ?? 'new');
183
- const chosen = savedCards.find((c) => c.savedCardToken === selected);
184
- if (savedCards.length === 0) return <CardPanel disabled={disabled} onSubmit={onNewCard} />;
185
- return (
186
- <Stack spacing={2}>
187
- <Typography variant="subtitle2">Pague com cartão</Typography>
188
- <RadioGroup value={selected} onChange={(_, v) => setSelected(v)}>
189
- {savedCards.map((card) => (
190
- <FormControlLabel
191
- key={card.savedCardToken}
192
- value={card.savedCardToken}
193
- control={<Radio />}
194
- label={`${card.brand} •••• ${card.last4}${card.expiry ? ` — Validade ${card.expiry}` : ''}`}
195
- />
196
- ))}
197
- <FormControlLabel value="new" control={<Radio />} label="Novo cartão — inserir outro cartão" />
198
- </RadioGroup>
199
- {chosen ? (
200
- <Button variant="contained" disabled={disabled} onClick={() => onSavedCard(chosen)}>
201
- Pagar {formatBRL(amount)}
202
- </Button>
203
- ) : (
204
- <CardPanel disabled={disabled} onSubmit={onNewCard} />
205
- )}
206
- </Stack>
207
- );
208
- }
209
-
210
- interface MethodChooserProps {
211
- config: ClientPaymentsConfig;
212
- amount: Money;
213
- savedCards: SavedCardOption[];
214
- loading: boolean;
215
- startPix: () => void;
216
- startCard: (values: CardFormValues) => void;
217
- startSavedCard: (card: SavedCardOption) => void;
218
- }
219
-
220
- /** One "Forma de pagamento" option card (PIX / Cartão). */
221
- function MethodCard(props: { title: string; subtitle: string; selected: boolean; onClick: () => void }) {
222
- return (
223
- <Button
224
- variant="outlined"
225
- onClick={props.onClick}
226
- sx={{ flex: 1, justifyContent: 'flex-start', textAlign: 'left', borderWidth: props.selected ? 2 : 1 }}
227
- >
228
- <Stack alignItems="flex-start">
229
- <Typography variant="subtitle2">{props.title}</Typography>
230
- <Typography variant="caption" color="text.secondary">
231
- {props.subtitle}
232
- </Typography>
233
- </Stack>
234
- </Button>
235
- );
236
- }
237
-
238
- /** Pre-charge phase: "Forma de pagamento" method cards, then the flow. */
239
- function MethodChooser(props: MethodChooserProps) {
240
- const { config, amount, savedCards, loading, startPix, startCard, startSavedCard } = props;
241
- const [method, setMethod] = useState<'PIX' | 'CARD'>('PIX');
242
- if (config.tokenization === 'REDIRECT') {
243
- return (
244
- <Button variant="contained" disabled={loading} onClick={startPix}>
245
- Continuar para o pagamento
246
- </Button>
247
- );
248
- }
249
- return (
250
- <>
251
- <Typography variant="subtitle2">Forma de pagamento</Typography>
252
- <Stack direction="row" spacing={2}>
253
- <MethodCard title="PIX" subtitle="Aprovação imediata" selected={method === 'PIX'} onClick={() => setMethod('PIX')} />
254
- <MethodCard title="Cartão" subtitle="Crédito à vista" selected={method === 'CARD'} onClick={() => setMethod('CARD')} />
255
- </Stack>
256
- {method === 'PIX' ? (
257
- <Button variant="contained" disabled={loading} onClick={startPix}>
258
- Gerar QR Code PIX
259
- </Button>
260
- ) : (
261
- <CardSection
262
- amount={amount}
263
- savedCards={savedCards}
264
- disabled={loading}
265
- onNewCard={startCard}
266
- onSavedCard={startSavedCard}
267
- />
268
- )}
269
- </>
270
- );
271
- }
272
-
273
98
  /** Poll target: only charges that can still change server-side. */
274
99
  function pollRefOf(charge: ClientChargeView | null) {
275
100
  if (!charge || isFinalOnCreate(charge)) return null;
@@ -279,36 +104,46 @@ function pollRefOf(charge: ClientChargeView | null) {
279
104
  interface CheckoutGateProps {
280
105
  errorMessage: string | null;
281
106
  config: ClientPaymentsConfig | null | undefined;
107
+ copy: LegacyRefusalCopy;
282
108
  }
283
109
 
284
110
  /** Error / loading / payments-disabled gates ahead of the payment UI. */
285
- function checkoutGate({ errorMessage, config }: CheckoutGateProps) {
111
+ function checkoutGate({ errorMessage, config, copy }: CheckoutGateProps) {
286
112
  if (errorMessage) return <Alert severity="error">{errorMessage}</Alert>;
287
113
  if (config === undefined) return <CircularProgress data-testid="checkout-payment-loading" />;
288
- if (config === null) {
289
- return <Alert severity="warning">Esta loja ainda não aceita pagamentos online.</Alert>;
290
- }
114
+ if (config === null) return <Alert severity="warning">{copy.paymentsOff}</Alert>;
291
115
  return null;
292
116
  }
293
117
 
294
118
  /** In-flight phase: the created charge decides what the buyer sees. */
295
- function ChargePhase({ charge, amount }: { charge: ClientChargeView; amount: Money }) {
119
+ function ChargePhase({
120
+ charge,
121
+ amount,
122
+ copy,
123
+ }: {
124
+ charge: ClientChargeView;
125
+ amount: Money;
126
+ copy: CheckoutPaymentCopy;
127
+ }) {
296
128
  if (charge.hostedCheckoutUrl && charge.status === 'PENDING') {
297
129
  return (
298
130
  <Stack spacing={2}>
299
- <Typography>Você será direcionado para concluir o pagamento com segurança.</Typography>
131
+ <Typography>{copy.refusal.redirectNotice}</Typography>
300
132
  <Button variant="contained" href={charge.hostedCheckoutUrl}>
301
- Pagar {formatBRL(amount)}
133
+ {copy.money.payAction(formatAmount(amount, copy.money))}
302
134
  </Button>
303
135
  </Stack>
304
136
  );
305
137
  }
306
- if (charge.method === 'PIX' && charge.status === 'PENDING') return <PixPanel charge={charge} />;
138
+ if (charge.method === 'PIX' && charge.status === 'PENDING') {
139
+ return <PixPanel charge={charge} copy={copy.pix} />;
140
+ }
307
141
  return null;
308
142
  }
309
143
 
310
144
  export function CheckoutPayment(props: CheckoutPaymentProps) {
311
- const { reference, amount, customer, tokenizeCard, savedCards = [], onPaid, onFailed } = props;
145
+ const { reference, amount, customer, tokenizeCard, savedCards = [], onPaid, onFailed, copy } =
146
+ props;
312
147
  const client = usePaymentsClient();
313
148
  const { config, error: configError } = useClientConfig(client);
314
149
  const [tokenizeError, setTokenizeError] = useState<string | null>(null);
@@ -325,7 +160,7 @@ export function CheckoutPayment(props: CheckoutPaymentProps) {
325
160
  const startCard = useCallback(
326
161
  async (values: CardFormValues) => {
327
162
  if (!config || !tokenizeCard) {
328
- setTokenizeError('Pagamento com cartão indisponível.');
163
+ setTokenizeError(copy.refusal.cardUnavailable);
329
164
  return;
330
165
  }
331
166
  try {
@@ -340,7 +175,7 @@ export function CheckoutPayment(props: CheckoutPaymentProps) {
340
175
  setTokenizeError(err instanceof Error ? err.message : String(err));
341
176
  }
342
177
  },
343
- [config, tokenizeCard, create, reference, customer],
178
+ [config, tokenizeCard, create, reference, customer, copy],
344
179
  );
345
180
 
346
181
  const startSavedCard = useCallback(
@@ -355,14 +190,14 @@ export function CheckoutPayment(props: CheckoutPaymentProps) {
355
190
  );
356
191
 
357
192
  const errorMessage = firstError(configError, tokenizeError, createError && createError.message);
358
- const gate = checkoutGate({ errorMessage, config });
193
+ const gate = checkoutGate({ errorMessage, config, copy: copy.refusal });
359
194
  if (gate || config === undefined || config === null) return gate;
360
195
 
361
196
  return (
362
197
  <Stack spacing={2}>
363
- <Typography variant="h6">Total: {formatBRL(amount)}</Typography>
198
+ <Typography variant="h6">{copy.money.totalLabel(formatAmount(amount, copy.money))}</Typography>
364
199
  {current ? (
365
- <ChargePhase charge={current} amount={amount} />
200
+ <ChargePhase charge={current} amount={amount} copy={copy} />
366
201
  ) : (
367
202
  <MethodChooser
368
203
  config={config}
@@ -372,6 +207,7 @@ export function CheckoutPayment(props: CheckoutPaymentProps) {
372
207
  startPix={startPix}
373
208
  startCard={(values) => void startCard(values)}
374
209
  startSavedCard={startSavedCard}
210
+ copy={copy}
375
211
  />
376
212
  )}
377
213
  </Stack>
@@ -12,6 +12,8 @@ import {
12
12
 
13
13
  import type { CredentialFieldSpec } from '@12-apps/payments-backend';
14
14
 
15
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
16
+
15
17
  /**
16
18
  * The last look at the value that decides who gets paid.
17
19
  *
@@ -45,6 +47,7 @@ export function ConfirmCredentialSave({
45
47
  onCancel: () => void;
46
48
  onConfirm: () => void;
47
49
  }) {
50
+ const copy = usePaymentsSettingsCopy().confirmSave;
48
51
  return (
49
52
  <Dialog
50
53
  open={pending !== null}
@@ -54,13 +57,10 @@ export function ConfirmCredentialSave({
54
57
  data-testid="payments-confirm-credential"
55
58
  aria-labelledby="payments-confirm-credential-title"
56
59
  >
57
- <DialogTitle id="payments-confirm-credential-title">
58
- Confirme para onde vai o dinheiro
59
- </DialogTitle>
60
+ <DialogTitle id="payments-confirm-credential-title">{copy.title}</DialogTitle>
60
61
  <DialogContent>
61
62
  <Typography variant="body2" color="text.secondary">
62
- Todo pagamento recebido por esta loja será depositado na conta desta{' '}
63
- {pending?.spec.label ?? 'credencial'}:
63
+ {copy.body(pending?.spec.label ?? copy.fieldFallback)}
64
64
  </Typography>
65
65
  <Box
66
66
  data-testid="payments-confirm-credential-value"
@@ -79,9 +79,7 @@ export function ConfirmCredentialSave({
79
79
  >
80
80
  {pending?.value}
81
81
  </Box>
82
- <Typography variant="caption" color="text.secondary">
83
- Um valor errado envia os pagamentos desta loja para outra pessoa, e não há como reverter.
84
- </Typography>
82
+ <Typography variant="caption" color="text.secondary">{copy.warning}</Typography>
85
83
  </DialogContent>
86
84
  <DialogActions>
87
85
  {/*
@@ -90,7 +88,7 @@ export function ConfirmCredentialSave({
90
88
  go back and check than to press on.
91
89
  */}
92
90
  <Button onClick={onCancel} disabled={busy} data-testid="payments-confirm-credential-cancel">
93
- Voltar e revisar
91
+ {copy.cancelAction}
94
92
  </Button>
95
93
  <Button
96
94
  variant="contained"
@@ -98,7 +96,7 @@ export function ConfirmCredentialSave({
98
96
  disabled={busy}
99
97
  data-testid="payments-confirm-credential-confirm"
100
98
  >
101
- É essa, salvar
99
+ {copy.confirmAction}
102
100
  </Button>
103
101
  </DialogActions>
104
102
  </Dialog>
@@ -17,6 +17,7 @@ import type { ReactNode } from 'react';
17
17
  import type { ConnectedOAuthAccount, PaymentEnvironment } from '@12-apps/payments-backend';
18
18
 
19
19
  import { BTN_PRIMARY_SX, BTN_SECONDARY_SX, LINKISH_SX, T } from './panel-tokens';
20
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
20
21
 
21
22
  /**
22
23
  * The connect card's own pieces: what a connection IS, what authorizing
@@ -46,23 +47,25 @@ export function ConnectionFacts({
46
47
  account: ConnectedOAuthAccount | null;
47
48
  displayName: string;
48
49
  }) {
50
+ const copy = usePaymentsSettingsCopy().card;
51
+ const env = usePaymentsSettingsCopy().environment;
49
52
  // A fact's testid names its VALUE, not its row: the environment is asserted
50
53
  // with an exact `toHaveText`, so hanging the id on the grid — or even on the
51
54
  // row, which also carries the "AMBIENTE" label — makes that assertion read
52
55
  // the whole card. Every consumer of this id wants the one string.
53
56
  const facts: { key: string; value: string; testId?: string }[] = [
54
- { key: 'Conta', value: account?.accountLabel ?? account?.accountId ?? `Conta ${displayName}` },
55
- { key: 'Conexão', value: `Autorizada no ${displayName}` },
57
+ { key: copy.accountLabel, value: account?.accountLabel ?? account?.accountId ?? copy.accountHeading(displayName) },
58
+ { key: copy.connectionLabel, value: copy.authorizedAt(displayName) },
56
59
  {
57
- key: 'Ambiente',
58
- value: environment === 'PRODUCTION' ? 'Produção' : 'Sandbox (testes)',
60
+ key: copy.environmentLabel,
61
+ value: environment === 'PRODUCTION' ? env.production : copy.sandboxWithNote,
59
62
  testId: 'payments-connected-environment',
60
63
  },
61
64
  ];
62
65
  if (account?.connectedAt) {
63
66
  facts.push({
64
- key: 'Conectada em',
65
- value: new Date(account.connectedAt).toLocaleString('pt-BR'),
67
+ key: copy.connectedAtLabel,
68
+ value: new Date(account.connectedAt).toLocaleString(),
66
69
  });
67
70
  }
68
71
  return (
@@ -108,10 +111,11 @@ export function ConnectionFacts({
108
111
  * you end up back here.
109
112
  */
110
113
  export function ConnectSteps({ displayName }: { displayName: string }) {
114
+ const copy = usePaymentsSettingsCopy().card;
111
115
  const steps = [
112
- `Entre na sua conta ${displayName} — dá para criar uma na hora, se ainda não tiver.`,
113
- 'Autorize o acesso na tela do provedor.',
114
- 'Você volta para cá com a conta conectada.',
116
+ copy.steps.signIn(displayName),
117
+ copy.steps.authorize,
118
+ copy.steps.comeBack,
115
119
  ];
116
120
  return (
117
121
  <Stack component="ol" spacing={1.1} sx={{ listStyle: 'none', m: 0, p: 0 }}>
@@ -159,32 +163,33 @@ export function DisconnectDialog(props: {
159
163
  /** The softer path: keep the connection, stop taking orders through it. */
160
164
  onPauseInstead?: () => void;
161
165
  }) {
166
+ const copy = usePaymentsSettingsCopy().card;
162
167
  const { displayName, receiving } = props;
163
168
  return (
164
169
  <Dialog open={props.open} onClose={props.onCancel} data-testid="payments-disconnect-confirm">
165
170
  <DialogTitle sx={{ fontSize: '16.5px', fontWeight: 700, letterSpacing: '-.01em', pb: '8px' }}>
166
- Remover a conexão com {displayName}?
171
+ {copy.removeQuestion(displayName)}
167
172
  </DialogTitle>
168
173
  <DialogContent sx={{ pb: '4px' }}>
169
174
  <DialogContentText sx={{ fontSize: '13px', color: T.ink2, lineHeight: 1.55 }}>
170
175
  {receiving
171
- ? 'A loja para de receber na hora e fica sem provedor ativo — pedidos novos não conseguem ser pagos até você conectar outro.'
172
- : 'Esta conexão sai da loja. Você pode conectar de novo depois.'}
176
+ ? copy.removeConsequenceLive
177
+ : copy.removeConsequenceIdle}
173
178
  </DialogContentText>
174
179
  {/* The consequences, itemised. A single paragraph makes the reversible
175
180
  facts and the irreversible one weigh the same, and the one that
176
181
  costs money is the one an owner skims past. */}
177
182
  <Stack component="ul" spacing={1} sx={{ listStyle: 'none', m: 0, mt: '14px', p: 0 }}>
178
183
  {receiving ? (
179
- <Consequence tone="bad">Pedidos novos deixam de ser cobrados imediatamente.</Consequence>
184
+ <Consequence tone="bad">{copy.removeStopsChargingNow}</Consequence>
180
185
  ) : null}
181
186
  <Consequence>
182
- {`A autorização é revogada no ${displayName}. Sua conta e seu histórico continuam lá, intactos.`}
187
+ {copy.removeRevokes(displayName)}
183
188
  </Consequence>
184
189
  <Consequence>
185
- {`Pagamentos já aprovados e estornos em andamento seguem normalmente pelo ${displayName}.`}
190
+ {copy.removeKeepsSettled(displayName)}
186
191
  </Consequence>
187
- <Consequence>Para reconectar, os passos recomeçam do zero.</Consequence>
192
+ <Consequence>{copy.removeRestartsSetup}</Consequence>
188
193
  </Stack>
189
194
  </DialogContent>
190
195
  <DialogActions sx={{ display: 'flex', flexDirection: 'column', gap: '8px', p: '18px 22px 20px' }}>
@@ -199,7 +204,7 @@ export function DisconnectDialog(props: {
199
204
  sx={BTN_SECONDARY_SX}
200
205
  data-testid="payments-pause-instead"
201
206
  >
202
- Só pausar o recebimento
207
+ {copy.pauseInstead}
203
208
  </Button>
204
209
  ) : null}
205
210
  <Button
@@ -211,10 +216,10 @@ export function DisconnectDialog(props: {
211
216
  data-testid="payments-disconnect-confirm-action"
212
217
  sx={{ ...BTN_PRIMARY_SX, background: T.bad, '&:hover': { background: '#a51f1f' } }}
213
218
  >
214
- {props.busy ? <CircularProgress size={18} sx={{ color: '#fff' }} /> : 'Remover conexão'}
219
+ {props.busy ? <CircularProgress size={18} sx={{ color: '#fff' }} /> : copy.removeAction}
215
220
  </Button>
216
221
  <Button fullWidth onClick={props.onCancel} disabled={props.busy} sx={LINKISH_SX}>
217
- Cancelar
222
+ {copy.cancel}
218
223
  </Button>
219
224
  </DialogActions>
220
225
  </Dialog>