@12-apps/payments-frontend 3.8.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
@@ -1,6 +1,7 @@
1
1
  import { Box } from "@mui/material";
2
2
  import { useEffect, useRef, useState, type JSX } from "react";
3
3
 
4
+ import { useCheckoutCopy } from "./copy-context";
4
5
  import type { CheckoutOrder } from "./types";
5
6
 
6
7
  /**
@@ -219,11 +220,13 @@ export function GooglePayButton({
219
220
  onReady,
220
221
  }: GooglePayButtonProps): JSX.Element | null {
221
222
  const client = useGooglePayClient(api, environment);
223
+ const copy = useCheckoutCopy().screens.wallet.googlePay;
222
224
  const container = useRef<HTMLDivElement | null>(null);
223
225
  // The latest handlers/order, so the Google-rendered button — mounted once —
224
- // never closes over a stale charge target.
225
- const current = useRef({ order, params, onKey, onError, onReady });
226
- current.current = { order, params, onKey, onError, onReady };
226
+ // never closes over a stale charge target. The words ride along for the same
227
+ // reason: the sheet's failure is reported from inside that one-time closure.
228
+ const current = useRef({ order, params, onKey, onError, onReady, copy });
229
+ current.current = { order, params, onKey, onError, onReady, copy };
227
230
 
228
231
  useEffect(() => {
229
232
  if (client) current.current.onReady?.();
@@ -234,23 +237,32 @@ export function GooglePayButton({
234
237
  if (!client || !mount) return undefined;
235
238
  const button = client.createButton({
236
239
  onClick: () => {
237
- const { order: forOrder, params: forParams, onKey: emit, onError: fail } = current.current;
240
+ const {
241
+ order: forOrder,
242
+ params: forParams,
243
+ onKey: emit,
244
+ onError: fail,
245
+ copy: words,
246
+ } = current.current;
238
247
  client
239
248
  .loadPaymentData(paymentDataRequest(forParams, forOrder))
240
249
  .then((data) => emit(data.paymentMethodData.tokenizationData.token))
241
250
  .catch((error: unknown) => {
242
251
  if (sheetDismissed(error)) return;
243
- fail("Não foi possível concluir o pagamento com o Google Pay. Tente novamente ou pague com cartão.");
252
+ fail(words.cannotComplete);
244
253
  });
245
254
  },
246
255
  buttonSizeMode: "fill",
247
- buttonLocale: "pt",
256
+ // Google draws this button's own label, so the language it draws it in
257
+ // is the host's say (FUT-760) — a translated checkout with a Portuguese
258
+ // pay button is two languages on one screen.
259
+ buttonLocale: copy.buttonLocale,
248
260
  });
249
261
  mount.replaceChildren(button);
250
262
  return () => {
251
263
  mount.replaceChildren();
252
264
  };
253
- }, [client]);
265
+ }, [client, copy.buttonLocale]);
254
266
 
255
267
  if (!client) return null;
256
268
  return (
@@ -1,7 +1,9 @@
1
1
  import { Box } from "@mui/material";
2
2
  import type { JSX } from "react";
3
3
 
4
+ import { useCheckoutCopy } from "./copy-context";
4
5
  import { CreditCardIcon, PixIcon } from "./icons";
6
+ import type { MethodPickerCopy } from "./screens-copy";
5
7
  import type { PaymentMethod } from "./types";
6
8
  import { useCheckoutComponents } from "./ui";
7
9
 
@@ -12,10 +14,25 @@ interface MethodOption {
12
14
  icon: JSX.Element;
13
15
  }
14
16
 
15
- const OPTIONS: MethodOption[] = [
16
- { value: "PIX", label: "PIX", description: "Aprovação imediata", icon: <PixIcon /> },
17
- { value: "CARD", label: "Cartão", description: "Crédito à vista", icon: <CreditCardIcon /> },
18
- ];
17
+ /**
18
+ * The two tiles, named by the HOST (FUT-760).
19
+ *
20
+ * Built per render rather than as a module constant, because the words are no
21
+ * longer this package's: a frozen `OPTIONS` array could only hold the origin
22
+ * host's Portuguese. The order and the icons are still ours — that is layout,
23
+ * not language.
24
+ */
25
+ function methodOptions(copy: MethodPickerCopy): MethodOption[] {
26
+ return [
27
+ { value: "PIX", label: copy.pixLabel, description: copy.pixDescription, icon: <PixIcon /> },
28
+ {
29
+ value: "CARD",
30
+ label: copy.cardLabel,
31
+ description: copy.cardDescription,
32
+ icon: <CreditCardIcon />,
33
+ },
34
+ ];
35
+ }
19
36
 
20
37
  /** Text/icon color for a tile's state — one place, so the ternaries stay flat. */
21
38
  function tileColor(unavailable: boolean, selected: boolean, muted: string): string {
@@ -36,6 +53,7 @@ function MethodTile({
36
53
  onSelect: () => void;
37
54
  }): JSX.Element {
38
55
  const { Text } = useCheckoutComponents();
56
+ const copy = useCheckoutCopy().screens.method;
39
57
  const cursor = unavailable ? "not-allowed" : "pointer";
40
58
  return (
41
59
  <Box
@@ -79,7 +97,7 @@ function MethodTile({
79
97
  {option.label}
80
98
  </Text>
81
99
  <Text variant="caption" size="xs" color="secondary" as="p">
82
- {unavailable ? "Indisponível nesta loja" : option.description}
100
+ {unavailable ? copy.unavailableHere : option.description}
83
101
  </Text>
84
102
  </Box>
85
103
  </Box>
@@ -104,8 +122,13 @@ function MethodTile({
104
122
  * leave an unexplained one-option "choice". The caller preselects the sole
105
123
  * remaining method, so no extra tap is demanded either.
106
124
  */
107
- function visibleOptions(offered: PaymentMethod[] | null): MethodOption[] {
108
- return OPTIONS.filter((option) => offered === null || offered.includes(option.value));
125
+ function visibleOptions(
126
+ copy: MethodPickerCopy,
127
+ offered: PaymentMethod[] | null,
128
+ ): MethodOption[] {
129
+ return methodOptions(copy).filter(
130
+ (option) => offered === null || offered.includes(option.value),
131
+ );
109
132
  }
110
133
 
111
134
  export function MethodPicker({
@@ -123,15 +146,16 @@ export function MethodPicker({
123
146
  offered?: PaymentMethod[] | null;
124
147
  }): JSX.Element {
125
148
  const { Text } = useCheckoutComponents();
126
- const options = visibleOptions(offered);
149
+ const copy = useCheckoutCopy().screens.method;
150
+ const options = visibleOptions(copy, offered);
127
151
  return (
128
152
  <Box>
129
153
  <Text variant="body" size="sm" weight="bold" as="p" style={{ marginBottom: 8 }}>
130
- Forma de pagamento
154
+ {copy.groupLabel}
131
155
  </Text>
132
156
  <Box
133
157
  role="radiogroup"
134
- aria-label="Forma de pagamento"
158
+ aria-label={copy.groupLabel}
135
159
  data-testid="checkout-method"
136
160
  sx={{ display: "flex", gap: 1 }}
137
161
  >
@@ -3,13 +3,14 @@ import type { JSX } from "react";
3
3
 
4
4
  import { formatCpf } from "../../card";
5
5
 
6
+ import { useCheckoutCopy } from "./copy-context";
6
7
  import { PersonOutlineIcon } from "./icons";
8
+ import type { PayerSummaryCopy } from "./screens-copy";
7
9
  import { useCheckoutComponents } from "./ui";
8
10
 
9
-
10
11
  /** What the CPF line reads, given whether the buyer typed one this time. */
11
- function cpfLabel(taxId: string | undefined): string {
12
- return taxId?.trim() ? `CPF ${formatCpf(taxId)}` : "CPF já cadastrado";
12
+ function cpfLabel(copy: PayerSummaryCopy, taxId: string | undefined): string {
13
+ return taxId?.trim() ? copy.taxId(formatCpf(taxId)) : copy.taxIdAlreadyKnown;
13
14
  }
14
15
 
15
16
  /**
@@ -42,6 +43,7 @@ export function PayerSummary({
42
43
  onEdit?: () => void;
43
44
  }): JSX.Element | null {
44
45
  const { Button, Text } = useCheckoutComponents();
46
+ const copy = useCheckoutCopy().screens.payer;
45
47
  if (!onEdit) return null;
46
48
 
47
49
  return (
@@ -61,10 +63,10 @@ export function PayerSummary({
61
63
  <PersonOutlineIcon sx={{ fontSize: 20, color: "text.secondary", flex: "0 0 auto" }} />
62
64
  <Box sx={{ flex: 1, minWidth: 0 }}>
63
65
  <Text variant="body" size="sm" as="p" data-testid="checkout-payer-name">
64
- {name ? `Pagando como ${name}` : "Pagando com os seus dados salvos"}
66
+ {name ? copy.payingAs(name) : copy.payingWithSavedDetails}
65
67
  </Text>
66
68
  <Text variant="caption" size="xs" color="secondary" as="p" data-testid="checkout-payer-cpf">
67
- {cpfLabel(taxId)}
69
+ {cpfLabel(copy, taxId)}
68
70
  </Text>
69
71
  </Box>
70
72
  <Button
@@ -74,7 +76,7 @@ export function PayerSummary({
74
76
  onClick={onEdit}
75
77
  dataTestId="checkout-payer-edit"
76
78
  >
77
- Alterar
79
+ {copy.changeAction}
78
80
  </Button>
79
81
  </Box>
80
82
  );
@@ -1,6 +1,7 @@
1
1
  import { Box } from "@mui/material";
2
2
  import { useState, type JSX } from "react";
3
3
 
4
+ import { useCheckoutCopy } from "./copy-context";
4
5
  import { UNRESOLVED_CODE } from "./failure-codes";
5
6
  import { useCheckoutComponents } from "./ui";
6
7
 
@@ -35,13 +36,14 @@ export function PaymentErrorPanel({
35
36
  onRetry: () => void;
36
37
  }): JSX.Element {
37
38
  const { Alert } = useCheckoutComponents();
39
+ const copy = useCheckoutCopy().screens.error;
38
40
  const unresolved = code === UNRESOLVED_CODE;
39
41
 
40
42
  return (
41
43
  <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
42
44
  <Alert
43
45
  variant={unresolved ? "warning" : "danger"}
44
- title={unresolved ? "Estamos confirmando seu pagamento" : "Não foi possível continuar"}
46
+ title={unresolved ? copy.confirming : copy.cannotContinue}
45
47
  description={message}
46
48
  showIcon
47
49
  data-testid={unresolved ? "checkout-unresolved" : "checkout-error"}
@@ -62,13 +64,14 @@ function RetryAffordance({
62
64
  onRetry: () => void;
63
65
  }): JSX.Element {
64
66
  const { Button, Input } = useCheckoutComponents();
67
+ const copy = useCheckoutCopy().screens.error;
65
68
  const [altEmail, setAltEmail] = useState("");
66
69
 
67
70
  if (!emailFlagged) {
68
71
  return (
69
72
  <Box>
70
73
  <Button variant="solid" color="primary" size="md" onClick={onRetry} dataTestId="checkout-retry-payment">
71
- Tentar novamente
74
+ {copy.retryAction}
72
75
  </Button>
73
76
  </Box>
74
77
  );
@@ -76,20 +79,20 @@ function RetryAffordance({
76
79
  return (
77
80
  <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
78
81
  <Input
79
- label="E-mail para o pagamento"
82
+ label={copy.emailLabel}
80
83
  type="email"
81
84
  variant="outlined"
82
85
  size="md"
83
86
  fullWidth
84
87
  autoComplete="email"
85
- placeholder="use um e-mail diferente do da loja"
88
+ placeholder={copy.emailMustDifferHint}
86
89
  value={altEmail}
87
90
  onChange={(event) => setAltEmail(event.target.value)}
88
91
  data-testid="checkout-alt-email"
89
92
  />
90
93
  <Box>
91
94
  <Button variant="solid" color="primary" size="md" disabled={!altEmail.trim()} onClick={() => onUseEmail(altEmail.trim())} dataTestId="checkout-use-alt-email">
92
- Usar este e-mail e continuar
95
+ {copy.useEmailAction}
93
96
  </Button>
94
97
  </Box>
95
98
  </Box>
@@ -2,6 +2,7 @@ import { Box } from "@mui/material";
2
2
  import { useEffect, useState, type JSX } from "react";
3
3
  import QRCode from "react-qr-code";
4
4
 
5
+ import { useCheckoutCopy } from "./copy-context";
5
6
  import { ContentCopyIcon } from "./icons";
6
7
  import type { CheckoutOrder, OrderStatus, PixCharge } from "./types";
7
8
  import { useCheckoutComponents } from "./ui";
@@ -17,6 +18,7 @@ import { usePaymentPolling } from "./use-payment-polling";
17
18
  /** The copyable "copia e cola" strip with its copy button. */
18
19
  function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
19
20
  const { Button, Text } = useCheckoutComponents();
21
+ const copy = useCheckoutCopy().screens.pix;
20
22
  const [copied, setCopied] = useState(false);
21
23
 
22
24
  const copyCode = async (): Promise<void> => {
@@ -63,7 +65,7 @@ function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
63
65
  }}
64
66
  dataTestId="pix-copy"
65
67
  >
66
- {copied ? "Copiado!" : "Copiar"}
68
+ {copied ? copy.copiedAction : copy.copyAction}
67
69
  </Button>
68
70
  </Box>
69
71
  );
@@ -72,11 +74,12 @@ function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
72
74
  /** The live footer: poll error, or the pulsing "awaiting payment" indicator. */
73
75
  function PixPollFooter({ error }: { error: string | null }): JSX.Element {
74
76
  const { Alert, Text } = useCheckoutComponents();
77
+ const { pix, settling } = useCheckoutCopy().screens;
75
78
  if (error) {
76
79
  return (
77
80
  <Alert
78
81
  variant="danger"
79
- title="Não foi possível confirmar o pagamento"
82
+ title={settling.cannotConfirm}
80
83
  description={error}
81
84
  showIcon
82
85
  data-testid="pix-poll-error"
@@ -87,7 +90,7 @@ function PixPollFooter({ error }: { error: string | null }): JSX.Element {
87
90
  <Box sx={{ display: "flex", alignItems: "center", gap: 1, color: "text.secondary" }}>
88
91
  <LoadingDot />
89
92
  <Text variant="caption" size="xs" color="secondary" as="span" data-testid="pix-awaiting">
90
- Aguardando pagamento…
93
+ {pix.awaiting}
91
94
  </Text>
92
95
  </Box>
93
96
  );
@@ -103,6 +106,7 @@ export function PixView({
103
106
  pollIntervalMs?: number;
104
107
  }): JSX.Element {
105
108
  const { Text } = useCheckoutComponents();
109
+ const copy = useCheckoutCopy().screens.pix;
106
110
  const { status, error } = usePaymentPolling(order.orderId, { intervalMs: pollIntervalMs });
107
111
 
108
112
  // Bubble a terminal status up once, so the parent can advance to the status step.
@@ -116,12 +120,15 @@ export function PixView({
116
120
  if (!pix) {
117
121
  return (
118
122
  <Text variant="body" size="sm" color="danger" as="p" data-testid="pix-missing">
119
- Não foi possível gerar o código PIX.
123
+ {copy.chargeMissing}
120
124
  </Text>
121
125
  );
122
126
  }
123
127
 
124
- const validUntil = new Date(pix.expiresAt).toLocaleTimeString("pt-BR", {
128
+ // The locale is the HOST's (FUT-760): it decides what a buyer reads off the
129
+ // clock, so it travels with the sentence it feeds rather than being frozen
130
+ // to the origin host's here.
131
+ const validUntil = new Date(pix.expiresAt).toLocaleTimeString(copy.expiryLocale, {
125
132
  hour: "2-digit",
126
133
  minute: "2-digit",
127
134
  });
@@ -132,16 +139,16 @@ export function PixView({
132
139
  sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, textAlign: "center" }}
133
140
  >
134
141
  <Text variant="heading" size="md" weight="bold" as="h2">
135
- Pague com PIX
142
+ {copy.heading}
136
143
  </Text>
137
144
  <Text variant="body" size="sm" color="secondary" as="p">
138
- Escaneie o QR code no app do seu banco ou copie o código. Total {order.totalLabel}.
145
+ {copy.instructions(order.totalLabel)}
139
146
  </Text>
140
147
 
141
148
  <Box
142
149
  data-testid="pix-qr"
143
150
  role="img"
144
- aria-label="QR Code PIX para pagamento"
151
+ aria-label={copy.qrAlt}
145
152
  sx={{ bgcolor: "background.paper", p: 2, borderRadius: 2, border: "1px solid", borderColor: "divider" }}
146
153
  >
147
154
  <QRCode value={pix.copyPaste} size={200} />
@@ -150,7 +157,7 @@ export function PixView({
150
157
  <PixCodeBox pix={pix} />
151
158
 
152
159
  <Text variant="caption" size="xs" color="secondary" as="p" data-testid="pix-expiry">
153
- Válido até {validUntil}. A confirmação é automática.
160
+ {copy.validUntil(validUntil)}
154
161
  </Text>
155
162
 
156
163
  <PixPollFooter error={error} />
@@ -32,7 +32,9 @@
32
32
  import { Box } from "@mui/material";
33
33
  import type { JSX } from "react";
34
34
 
35
+ import { useCheckoutCopy } from "../copy-context";
35
36
  import { offeredMethods } from "../method-capability";
37
+ import type { HostedHandoverCopy } from "../screens-copy";
36
38
  import type { CheckoutProviderConfig } from "../types";
37
39
  import { useCheckoutComponents } from "../ui";
38
40
 
@@ -47,9 +49,9 @@ import type { ProviderCheckoutScreenProps } from "./types";
47
49
  * true sentence rather than our internal id ("infinitepay") dressed up as a
48
50
  * brand.
49
51
  */
50
- function destinationLabel(config: CheckoutProviderConfig | null): string {
52
+ function destinationLabel(copy: HostedHandoverCopy, config: CheckoutProviderConfig | null): string {
51
53
  const name = config?.chain?.[0]?.displayName?.trim();
52
- return name ? `à página de pagamento da ${name}` : "à página de pagamento segura do provedor";
54
+ return name ? copy.destinationNamed(name) : copy.destinationGeneric;
53
55
  }
54
56
 
55
57
  /**
@@ -60,21 +62,27 @@ function destinationLabel(config: CheckoutProviderConfig | null): string {
60
62
  * keeps this honest for a provider that takes only one of the two: a hosted
61
63
  * PIX-only store must not promise a card.
62
64
  */
63
- function methodsPhrase(config: CheckoutProviderConfig | null): string | null {
65
+ function methodsPhrase(
66
+ copy: HostedHandoverCopy,
67
+ config: CheckoutProviderConfig | null,
68
+ ): string | null {
64
69
  const offered = offeredMethods(config);
65
70
  const pix = offered === null || offered.includes("PIX");
66
71
  const card = offered === null || offered.includes("CARD");
67
- if (pix && card) return "PIX ou cartão";
68
- if (pix) return "PIX";
69
- if (card) return "cartão";
72
+ if (pix && card) return copy.pixAndCard;
73
+ if (pix) return copy.pixOnly;
74
+ if (card) return copy.cardOnly;
70
75
  return null;
71
76
  }
72
77
 
73
78
  /** The full "where you are going and what happens there" sentence. */
74
- function handoffMessage(config: CheckoutProviderConfig | null): string {
75
- const methods = methodsPhrase(config);
76
- const choice = methods ? `, onde você escolhe pagar com ${methods}` : "";
77
- return `Você será levado ${destinationLabel(config)}${choice}.`;
79
+ function handoffMessage(
80
+ copy: HostedHandoverCopy,
81
+ config: CheckoutProviderConfig | null,
82
+ ): string {
83
+ const methods = methodsPhrase(copy, config);
84
+ const choice = methods ? copy.methodsChoice(methods) : "";
85
+ return copy.handoff(destinationLabel(copy, config), choice);
78
86
  }
79
87
 
80
88
  /** The invitation: what happens next, and the one button that starts it. */
@@ -86,16 +94,17 @@ function HandOffInvite({
86
94
  onStart: () => void;
87
95
  }): JSX.Element {
88
96
  const { Button, Text } = useCheckoutComponents();
97
+ const copy = useCheckoutCopy().screens.hosted;
89
98
  return (
90
99
  <Box
91
100
  data-testid="checkout-handoff-invite"
92
101
  sx={{ display: "flex", flexDirection: "column", gap: 2 }}
93
102
  >
94
103
  <Text variant="body" size="md" as="p">
95
- {handoffMessage(config)}
104
+ {handoffMessage(copy, config)}
96
105
  </Text>
97
106
  <Text variant="caption" size="sm" as="p" color="secondary">
98
- Assim que o pagamento for concluído, você volta para cá e nós confirmamos o pedido.
107
+ {copy.afterwards}
99
108
  </Text>
100
109
  <Button
101
110
  variant="solid"
@@ -105,7 +114,7 @@ function HandOffInvite({
105
114
  onClick={onStart}
106
115
  dataTestId="checkout-handoff-start"
107
116
  >
108
- Seguir para o pagamento
117
+ {copy.startAction}
109
118
  </Button>
110
119
  </Box>
111
120
  );
@@ -121,17 +130,18 @@ function HandOffInvite({
121
130
  */
122
131
  function HandOffPending({ config }: { config: CheckoutProviderConfig | null }): JSX.Element {
123
132
  const { Text, LoadingState } = useCheckoutComponents();
133
+ const copy = useCheckoutCopy().screens.hosted;
124
134
  return (
125
135
  <Box
126
136
  data-testid="checkout-handoff-pending"
127
137
  sx={{ display: "flex", flexDirection: "column", gap: 2, alignItems: "center", py: 4 }}
128
138
  >
129
- <LoadingState variant="spinner" message="Preparando o pagamento" size="md" />
139
+ <LoadingState variant="spinner" message={copy.preparing} size="md" />
130
140
  <Text variant="body" size="md" as="p">
131
- {handoffMessage(config)}
141
+ {handoffMessage(copy, config)}
132
142
  </Text>
133
143
  <Text variant="caption" size="sm" as="p" color="secondary">
134
- Assim que o pagamento for concluído, você volta para cá e nós confirmamos o pedido.
144
+ {copy.afterwards}
135
145
  </Text>
136
146
  </Box>
137
147
  );
@@ -1,3 +1,6 @@
1
+ import { PT_BR_CARD_COPY } from "../../card/pt-BR";
2
+ import { PT_BR_CHECKOUT_SCREENS_COPY } from "./screens-pt-BR";
3
+ import type { CheckoutCopy } from "./copy-context";
1
4
  import type { CheckoutViewCopy, PaymentStatusCopy } from "./view-copy";
2
5
 
3
6
  /**
@@ -39,7 +42,31 @@ export const PT_BR_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
39
42
  receiptEmailLabel: "Comprovante enviado para",
40
43
  };
41
44
 
45
+ /**
46
+ * The words the checkout's deeper screens read from context — the card form,
47
+ * its tokenizers, and the buyer-details step's own fields.
48
+ *
49
+ * Same rule as everything else in this file: a NAMED pack a host passes by
50
+ * hand at the mount, never a default the package reaches for.
51
+ */
52
+ export const PT_BR_CHECKOUT_COPY: CheckoutCopy = {
53
+ card: PT_BR_CARD_COPY,
54
+ screens: PT_BR_CHECKOUT_SCREENS_COPY,
55
+ buyer: {
56
+ emailInvalid: "E-mail inválido.",
57
+ emailRequired: "E-mail obrigatório.",
58
+ nameRequired: "Nome obrigatório.",
59
+ phoneRequired: "Telefone obrigatório.",
60
+ fieldsHint: (names) =>
61
+ names.length === 0
62
+ ? "Nome, e-mail e telefone são opcionais — usados apenas para o comprovante."
63
+ : `Informe seu ${names.join(", ")} (${names.length === 1 ? "obrigatório" : "obrigatórios"} ` +
64
+ "para o pagamento). Os demais campos são opcionais — usados apenas para o comprovante.",
65
+ },
66
+ };
67
+
42
68
  export const PT_BR_CHECKOUT_VIEW_COPY: CheckoutViewCopy = {
69
+ screens: PT_BR_CHECKOUT_COPY,
43
70
  steps: {
44
71
  dados: "Dados",
45
72
  payment: "Pagamento",