@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
@@ -22,6 +22,8 @@ import type { PaymentsSettingsClient } from '../client';
22
22
  import { ConnectionFacts, ConnectSteps, DisconnectDialog } from './ConnectionCard';
23
23
  import { expiryProximity, isConnected } from './connection-state';
24
24
  import { BAR_MSG_SX, BAR_SX, BTN_PRIMARY_SX, BTN_QUIET_DANGER_SX, T } from './panel-tokens';
25
+ import type { OAuthConnectionCopy } from './settings-copy';
26
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
25
27
 
26
28
  /**
27
29
  * The `authMode: 'oauth'` half of the settings page: a provider whose
@@ -56,12 +58,13 @@ export interface ProviderConnectionProps {
56
58
  * gray line read the same on the eve of an outage as a year out.
57
59
  */
58
60
  function ExpiryNote(props: { expiresAt: string }) {
61
+ const copy = usePaymentsSettingsCopy().oauth;
59
62
  const when = new Date(props.expiresAt).toLocaleString('pt-BR');
60
63
  const proximity = expiryProximity(props.expiresAt);
61
64
  if (proximity === 'SAFE' || proximity === null) {
62
65
  return (
63
66
  <Typography variant="caption" color="text.secondary">
64
- {`Autorização válida até ${when}`}
67
+ {copy.validUntil(when)}
65
68
  </Typography>
66
69
  );
67
70
  }
@@ -73,8 +76,8 @@ function ExpiryNote(props: { expiresAt: string }) {
73
76
  data-testid="payments-expiry-warning"
74
77
  >
75
78
  {proximity === 'PAST'
76
- ? `A autorização expirou em ${when}. Reconecte para voltar a receber pagamentos.`
77
- : `A autorização expira em ${when}. Se o aviso continuar, reconecte a conta.`}
79
+ ? copy.expiredAt(when)
80
+ : copy.expiresAt(when)}
78
81
  </Typography>
79
82
  );
80
83
  }
@@ -96,23 +99,19 @@ function ExpiryNote(props: { expiresAt: string }) {
96
99
  * up confidently misdescribing an outage.
97
100
  */
98
101
  function connectFailure(
102
+ copy: OAuthConnectionCopy,
99
103
  message: string,
100
104
  displayName: string,
101
105
  ): { severity: 'error' | 'warning'; text: string } {
102
106
  if (/no platform oauth application credentials/i.test(message)) {
103
- return {
104
- severity: 'warning',
105
- text:
106
- `A conexão automática com ${displayName} não está disponível nesta instalação — ` +
107
- 'o aplicativo de autorização não foi cadastrado. Para conectar agora, abra ' +
108
- '“Prefiro informar as credenciais manualmente” abaixo e cole as suas próprias chaves.',
109
- };
107
+ return { severity: 'warning', text: copy.notAvailableHere(displayName) };
110
108
  }
111
109
  return { severity: 'error', text: message };
112
110
  }
113
111
 
114
112
  function ConnectError({ message, displayName }: { message: string; displayName: string }) {
115
- const { severity, text } = connectFailure(message, displayName);
113
+ const copy = usePaymentsSettingsCopy().oauth;
114
+ const { severity, text } = connectFailure(copy, message, displayName);
116
115
  return (
117
116
  <Alert severity={severity} data-testid="payments-connect-failure">
118
117
  {text}
@@ -120,21 +119,28 @@ function ConnectError({ message, displayName }: { message: string; displayName:
120
119
  );
121
120
  }
122
121
 
123
- function connectLabel(displayName: string, connected: boolean, busy: string | null) {
122
+ function connectLabel(
123
+ copy: OAuthConnectionCopy,
124
+ displayName: string,
125
+ connected: boolean,
126
+ busy: string | null,
127
+ ) {
124
128
  if (busy === 'connect') return <CircularProgress size={18} />;
125
- return connected ? 'Reconectar' : `Conectar com ${displayName}`;
129
+ return connected ? copy.reconnectAction : copy.connectAction(displayName);
126
130
  }
127
131
 
128
132
  /**
129
133
  * pt-BR labels for the scopes a store owner actually sees. Unknown scopes fall
130
134
  * back to the provider's own spelling — wrong words are worse than raw ones.
131
135
  */
132
- const SCOPE_LABELS: Record<string, string> = {
133
- 'payments.read': 'Consultar pagamentos',
134
- 'payments.create': 'Criar cobranças',
135
- 'payments.refund': 'Estornar pagamentos',
136
- 'accounts.read': 'Consultar dados da conta',
137
- };
136
+ function scopeLabels(copy: OAuthConnectionCopy): Record<string, string> {
137
+ return {
138
+ 'payments.read': copy.scopes.read,
139
+ 'payments.create': copy.scopes.create,
140
+ 'payments.refund': copy.scopes.refund,
141
+ 'accounts.read': copy.scopes.account,
142
+ };
143
+ }
138
144
 
139
145
  /**
140
146
  * WHICH account is connected (FUT-300): identity, granted scopes and the
@@ -143,6 +149,7 @@ const SCOPE_LABELS: Record<string, string> = {
143
149
  * charges into was to disconnect and connect again.
144
150
  */
145
151
  function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
152
+ const copy = usePaymentsSettingsCopy().oauth;
146
153
  const { account } = props;
147
154
  const identity = account.accountLabel ?? account.accountId;
148
155
  return (
@@ -154,7 +161,7 @@ function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
154
161
  ) : null}
155
162
  {account.connectedAt ? (
156
163
  <Typography variant="caption" color="text.secondary">
157
- {`Conectada em ${new Date(account.connectedAt).toLocaleString('pt-BR')}`}
164
+ {copy.connectedAt(new Date(account.connectedAt).toLocaleString())}
158
165
  </Typography>
159
166
  ) : null}
160
167
  {account.grantedScopes.length > 0 ? (
@@ -164,7 +171,7 @@ function ConnectedAccountDetails(props: { account: ConnectedOAuthAccount }) {
164
171
  key={scope}
165
172
  size="small"
166
173
  variant="outlined"
167
- label={SCOPE_LABELS[scope] ?? scope}
174
+ label={scopeLabels(copy)[scope] ?? scope}
168
175
  />
169
176
  ))}
170
177
  </Stack>
@@ -184,6 +191,7 @@ function ConnectionSummary(props: {
184
191
  environment: PaymentEnvironment;
185
192
  connectedAccount: ConnectedOAuthAccount | null;
186
193
  }) {
194
+ const copy = usePaymentsSettingsCopy().oauth;
187
195
  return (
188
196
  <>
189
197
  {/*
@@ -215,8 +223,8 @@ function ConnectionSummary(props: {
215
223
  */}
216
224
  <Typography sx={{ fontSize: '13px', color: T.ink2, lineHeight: 1.6 }}>
217
225
  {props.connected
218
- ? 'Sua conta está conectada. As cobranças são criadas em seu nome — nenhuma chave precisa ser copiada.'
219
- : `Conecte sua conta ${props.displayName} autorizando o acesso no site do provedor. Nenhuma chave precisa ser copiada.`}
226
+ ? copy.connectedExplainer
227
+ : copy.invitation(props.displayName)}
220
228
  </Typography>
221
229
  {props.connected ? null : <ConnectSteps displayName={props.displayName} />}
222
230
  {props.connected && props.connectedAccount ? (
@@ -224,7 +232,7 @@ function ConnectionSummary(props: {
224
232
  ) : null}
225
233
  {props.status === 'RECONNECT_REQUIRED' ? (
226
234
  <Alert severity="warning">
227
- A autorização expirou ou foi revogada. Reconecte para voltar a receber pagamentos.
235
+ {copy.revoked}
228
236
  </Alert>
229
237
  ) : null}
230
238
  {props.expiresAt ? <ExpiryNote expiresAt={props.expiresAt} /> : null}
@@ -249,13 +257,14 @@ function ConnectionActions(props: {
249
257
  onConnect: () => void;
250
258
  onDisconnect: () => void;
251
259
  }) {
260
+ const copy = usePaymentsSettingsCopy().oauth;
252
261
  const { displayName, connected, busy, onConnect, onDisconnect } = props;
253
262
  return (
254
263
  <Box sx={BAR_SX} data-testid="payments-connection-bar">
255
264
  <Typography sx={BAR_MSG_SX}>
256
265
  {connected
257
- ? `Conta ${displayName} conectada. Revogue quando quiser, aqui ou no painel do provedor.`
258
- : 'Você sai para o provedor e volta para cá — leva menos de um minuto.'}
266
+ ? copy.connectedNote(displayName)
267
+ : copy.roundTripNote}
259
268
  </Typography>
260
269
  {connected ? (
261
270
  <Button
@@ -264,7 +273,7 @@ function ConnectionActions(props: {
264
273
  sx={BTN_QUIET_DANGER_SX}
265
274
  data-testid="payments-disconnect"
266
275
  >
267
- {busy === 'disconnect' ? <CircularProgress size={18} /> : 'Remover conexão'}
276
+ {busy === 'disconnect' ? <CircularProgress size={18} /> : copy.removeAction}
268
277
  </Button>
269
278
  ) : null}
270
279
  <Button
@@ -274,7 +283,7 @@ function ConnectionActions(props: {
274
283
  onClick={onConnect}
275
284
  sx={BTN_PRIMARY_SX}
276
285
  >
277
- {connectLabel(displayName, connected, busy)}
286
+ {connectLabel(copy, displayName, connected, busy)}
278
287
  </Button>
279
288
  </Box>
280
289
  );
@@ -7,6 +7,7 @@ import type { MaskedProviderConfig, MerchantSettingsView, ProviderDescriptor } f
7
7
  import type { PaymentsSettingsClient } from '../client';
8
8
  import { connectionBadge } from './connection-state';
9
9
  import { ProviderPriorityList, chainOf } from './ProviderPriorityList';
10
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
10
11
 
11
12
  /**
12
13
  * Provider picker — one card per provider, click to open it.
@@ -26,6 +27,7 @@ function ProviderPicker({
26
27
  activeName: string | null;
27
28
  onSelect: (name: string) => void;
28
29
  }) {
30
+ const copy = usePaymentsSettingsCopy().listBadge;
29
31
  return (
30
32
  <Box
31
33
  data-testid="payments-provider-picker"
@@ -38,7 +40,7 @@ function ProviderPicker({
38
40
  >
39
41
  {providers.map((provider) => {
40
42
  const config = configs.find((c) => c.provider === provider.name) ?? null;
41
- const badge = connectionBadge(config);
43
+ const badge = connectionBadge(copy, config);
42
44
  const selected = provider.name === activeName;
43
45
  return (
44
46
  <Box
@@ -93,6 +95,7 @@ export function ProviderList({
93
95
  reload: () => Promise<void>;
94
96
  onSelect: (name: string) => void;
95
97
  }) {
98
+ const copy = usePaymentsSettingsCopy().providerList;
96
99
  return (
97
100
  <Box data-testid="payments-provider-settings">
98
101
  {/*
@@ -111,9 +114,9 @@ export function ProviderList({
111
114
  />
112
115
  </Box>
113
116
  ) : null}
114
- <Typography variant="subtitle1">Escolha o provedor de pagamento</Typography>
117
+ <Typography variant="subtitle1">{copy.heading}</Typography>
115
118
  <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
116
- Selecione onde sua loja recebe — a configuração é feita sob medida para ele.
119
+ {copy.subheading}
117
120
  </Typography>
118
121
  <ProviderPicker
119
122
  providers={view.providers}
@@ -15,6 +15,8 @@ import { useCallback, useState } from 'react';
15
15
  import type { MerchantSettingsView, ProviderDescriptor } from '@12-apps/payments-backend';
16
16
 
17
17
  import type { PaymentsSettingsClient } from '../client';
18
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
19
+ import { richText } from './rich-text';
18
20
 
19
21
  /**
20
22
  * The store's failover chain, ordered.
@@ -92,6 +94,7 @@ function PriorityRow({
92
94
  onDragEnd,
93
95
  onMove,
94
96
  }: PriorityRowProps) {
97
+ const copy = usePaymentsSettingsCopy().priority;
95
98
  return (
96
99
  <Paper
97
100
  component="li"
@@ -120,12 +123,12 @@ function PriorityRow({
120
123
  <Typography sx={{ flexGrow: 1 }}>{label}</Typography>
121
124
  {index === 0 ? (
122
125
  <Typography variant="caption" color="primary" data-testid="payments-priority-first">
123
- primeiro
126
+ {copy.firstInChain}
124
127
  </Typography>
125
128
  ) : null}
126
129
  <IconButton
127
130
  size="small"
128
- aria-label={`Mover ${label} para cima`}
131
+ aria-label={copy.moveUp(label)}
129
132
  disabled={index === 0 || saving}
130
133
  onClick={() => onMove(index, index - 1)}
131
134
  >
@@ -133,7 +136,7 @@ function PriorityRow({
133
136
  </IconButton>
134
137
  <IconButton
135
138
  size="small"
136
- aria-label={`Mover ${label} para baixo`}
139
+ aria-label={copy.moveDown(label)}
137
140
  disabled={index === total - 1 || saving}
138
141
  onClick={() => onMove(index, index + 1)}
139
142
  >
@@ -154,6 +157,7 @@ function useChainReorder(
154
157
  client: PaymentsSettingsClient,
155
158
  onReordered?: (next: MerchantSettingsView) => void,
156
159
  ) {
160
+ const copy = usePaymentsSettingsCopy().priority;
157
161
  const [chain, setChain] = useState<string[]>(() => chainOf(view));
158
162
  const [error, setError] = useState<string | null>(null);
159
163
  const [saving, setSaving] = useState(false);
@@ -174,7 +178,7 @@ function useChainReorder(
174
178
  })
175
179
  .catch((err: unknown) => {
176
180
  setChain(previous);
177
- setError(err instanceof Error ? err.message : 'Não foi possível salvar a ordem.');
181
+ setError(err instanceof Error ? err.message : copy.saveFailed);
178
182
  })
179
183
  .finally(() => setSaving(false));
180
184
  },
@@ -201,6 +205,7 @@ function DeclinePolicyControl({
201
205
  client: PaymentsSettingsClient;
202
206
  onChanged?: (next: MerchantSettingsView) => void;
203
207
  }) {
208
+ const copy = usePaymentsSettingsCopy().priority;
204
209
  const [saving, setSaving] = useState(false);
205
210
  const cascades = view.failoverPolicy === 'TECHNICAL_AND_DECLINE';
206
211
 
@@ -212,7 +217,9 @@ function DeclinePolicyControl({
212
217
  size="small"
213
218
  checked={cascades}
214
219
  disabled={saving}
215
- inputProps={{ 'aria-label': 'Tentar cartão recusado no próximo provedor' }}
220
+ // The accessible name is the same sentence with its emphasis
221
+ // markers stripped: a screen reader must not spell out asterisks.
222
+ inputProps={{ 'aria-label': copy.retryDeclinedLabel.replace(/\*\*/g, '') }}
216
223
  onChange={(e) => {
217
224
  setSaving(true);
218
225
  void client
@@ -223,28 +230,25 @@ function DeclinePolicyControl({
223
230
  />
224
231
  }
225
232
  label={
226
- <Typography variant="body2">
227
- Tentar cartão <strong>recusado</strong> no próximo provedor
228
- </Typography>
233
+ <Typography variant="body2">{richText(copy.retryDeclinedLabel)}</Typography>
229
234
  }
230
235
  />
231
236
  <Typography variant="caption" color="text.secondary" display="block">
232
- {cascades
233
- ? 'Uma recusa passa para o próximo da lista. Isso pode aumentar custos de transação e sinais de fraude.'
234
- : 'Padrão: uma recusa encerra a cobrança. Só falhas técnicas passam para o próximo.'}
237
+ {cascades ? copy.retryDeclinedOn : copy.retryDeclinedOff}
235
238
  </Typography>
236
239
  </Box>
237
240
  );
238
241
  }
239
242
 
240
243
  export function ProviderPriorityList({ view, client, onReordered }: ProviderPriorityListProps) {
244
+ const copy = usePaymentsSettingsCopy().priority;
241
245
  const { chain, error, saving, move } = useChainReorder(view, client, onReordered);
242
246
  const [dragging, setDragging] = useState<number | null>(null);
243
247
 
244
248
  if (chain.length === 0) {
245
249
  return (
246
250
  <Alert severity="warning" data-testid="payments-priority-empty">
247
- Nenhum provedor está ativo. O checkout não conseguirá cobrar até que você ative ao menos um.
251
+ {copy.noneActive}
248
252
  </Alert>
249
253
  );
250
254
  }
@@ -252,12 +256,10 @@ export function ProviderPriorityList({ view, client, onReordered }: ProviderPrio
252
256
  return (
253
257
  <Box data-testid="payments-priority-list">
254
258
  <Typography variant="subtitle2" gutterBottom>
255
- Ordem de tentativa
259
+ {copy.orderHeading}
256
260
  </Typography>
257
261
  <Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
258
- O checkout tenta <strong>{labelOf(view.providers, chain[0] ?? '')}</strong> primeiro. Se uma
259
- cobrança falhar por motivo técnico, ele tenta o próximo da lista — mas só quando é possível
260
- comprovar que a tentativa anterior não gerou cobrança.
262
+ {richText(copy.chainExplainer(labelOf(view.providers, chain[0] ?? '')))}
261
263
  </Typography>
262
264
 
263
265
  {error ? (
@@ -6,6 +6,8 @@ import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments
6
6
 
7
7
  import { isConnected } from './connection-state';
8
8
  import { T } from './panel-tokens';
9
+ import type { ConnectionStatusCopy } from './settings-copy';
10
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
9
11
 
10
12
  /**
11
13
  * The provider's headline: what state it is in, and the switch that decides
@@ -37,19 +39,22 @@ interface StatusBadge {
37
39
  * — a real payment that actually landed — and is the only one painted green.
38
40
  */
39
41
  export function statusBadge(
42
+ copy: ConnectionStatusCopy,
40
43
  config: MaskedProviderConfig | null,
41
44
  descriptor?: ProviderDescriptor,
42
45
  ): StatusBadge {
43
- if (config?.chargeVerifiedAt) return { label: 'VERIFICADO', color: 'success' };
46
+ if (config?.chargeVerifiedAt) return { label: copy.verified, color: 'success' };
44
47
  // A store that has entered nothing has not FAILED at anything. The stored
45
48
  // status outlives the credential it described — clearing the field leaves the
46
49
  // last verdict behind — so a red FALHOU sat above an empty form, accusing the
47
50
  // owner of a mistake they had not yet had the chance to make.
48
51
  if (descriptor && !anyCredentialStored(config, descriptor)) {
49
- return { label: 'NÃO VERIFICADO', color: 'default' };
52
+ return { label: copy.unverified, color: 'default' };
50
53
  }
51
- if (config?.status === 'RECONNECT_REQUIRED') return { label: 'RECONECTAR', color: 'error' };
52
- if (isConnected(config)) return { label: 'CONEXÃO OK', color: 'info' };
54
+ if (config?.status === 'RECONNECT_REQUIRED') {
55
+ return { label: copy.reconnectRequired, color: 'error' };
56
+ }
57
+ if (isConnected(config)) return { label: copy.connectionOk, color: 'info' };
53
58
  // A FAILED probe lands here, and it says NÃO VERIFICADO like every other
54
59
  // not-yet-connected state. `FALHOU` was a third word for the same fact — the
55
60
  // store cannot take money through this provider yet — and it was the only
@@ -59,7 +64,7 @@ export function statusBadge(
59
64
  //
60
65
  // `RECONNECT_REQUIRED` stays red on purpose: that one is a connection that
61
66
  // WAS working and stopped, which is news rather than a step.
62
- return { label: 'NÃO VERIFICADO', color: 'default' };
67
+ return { label: copy.unverified, color: 'default' };
63
68
  }
64
69
 
65
70
  /**
@@ -107,6 +112,7 @@ function anyCredentialStored(
107
112
  * does not have, and quoting it from here could only ever be a guess.
108
113
  */
109
114
  function toggleGate(
115
+ copy: ConnectionStatusCopy,
110
116
  descriptor: ProviderDescriptor,
111
117
  config: MaskedProviderConfig | null,
112
118
  enabled: boolean,
@@ -119,9 +125,7 @@ function toggleGate(
119
125
  if (!lockedOff) return { lockedOff, hint: '' };
120
126
  return {
121
127
  lockedOff,
122
- hint: provable
123
- ? 'Este provedor só passa a receber vendas depois dos 3 passos abaixo.'
124
- : 'Conecte e verifique o provedor antes de ativar as vendas.',
128
+ hint: provable ? copy.threeStepsAhead : copy.connectAndVerifyFirst,
125
129
  };
126
130
  }
127
131
 
@@ -133,24 +137,19 @@ function toggleGate(
133
137
  * opposite situations: the first is a step outstanding, the second a decision
134
138
  * the owner made and can undo in one click.
135
139
  */
136
- function headline(io: { enabled: boolean; paused: boolean; lockedOff: boolean; hint: string }) {
140
+ function headline(
141
+ copy: ConnectionStatusCopy,
142
+ io: { enabled: boolean; paused: boolean; lockedOff: boolean; hint: string },
143
+ ) {
137
144
  if (io.enabled) {
138
- return {
139
- state: 'Recebendo vendas',
140
- sub: 'Sua loja está recebendo por este provedor.',
141
- tone: T.ok,
142
- };
145
+ return { ...copy.receiving, tone: T.ok };
143
146
  }
144
147
  if (io.paused) {
145
- return {
146
- state: 'Pausado',
147
- sub: 'Conexão pronta e pausada por você — nenhum pedido novo é cobrado aqui.',
148
- tone: T.warn,
149
- };
148
+ return { ...copy.pausedByOwner, tone: T.warn };
150
149
  }
151
150
  return {
152
- state: 'Ainda não está recebendo',
153
- sub: io.lockedOff ? io.hint : 'Tudo pronto — ligue a chave para começar a receber.',
151
+ state: copy.readyNotReceiving.state,
152
+ sub: io.lockedOff ? io.hint : copy.readyNotReceiving.sub,
154
153
  tone: T.ink3,
155
154
  };
156
155
  }
@@ -231,12 +230,13 @@ export function ProviderStatusBar({
231
230
  onToggle: (enabled: boolean) => void;
232
231
  }) {
233
232
  const enabled = config?.enabled ?? false;
234
- const { lockedOff, hint } = toggleGate(descriptor, config, enabled);
235
- const badge = statusBadge(config, descriptor);
233
+ const copy = usePaymentsSettingsCopy().status;
234
+ const { lockedOff, hint } = toggleGate(copy, descriptor, config, enabled);
235
+ const badge = statusBadge(copy, config, descriptor);
236
236
 
237
237
  const proven = Boolean(config?.chargeVerifiedAt);
238
238
  const paused = proven && !enabled;
239
- const { state, sub, tone } = headline({ enabled, paused, lockedOff, hint });
239
+ const { state, sub, tone } = headline(copy, { enabled, paused, lockedOff, hint });
240
240
 
241
241
  return (
242
242
  <Stack direction="row" alignItems="flex-start" gap="14px" flexWrap="wrap">
@@ -7,6 +7,7 @@ import type { ProviderSetupGuide as Guide, SetupSection } from '@12-apps/payment
7
7
 
8
8
  import { DoneRow } from './CredentialFields';
9
9
  import { ProviderSetupGuide } from './ProviderSetupGuide';
10
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
10
11
 
11
12
  /**
12
13
  * The walkthrough, plus the one step whose completion only the OWNER can
@@ -34,9 +35,10 @@ export const CHECKOUT_CONFIRM_ACTION = 'checkout-integrado-confirmado';
34
35
  * and Stone both own a step no API can report, and neither of them has a
35
36
  * "Checkout Integrado" to have enabled. Sections say what they are asking about
36
37
  * via `SetupSection.confirmLabel`; this fallback keeps InfinitePay's rendered
37
- * guide identical.
38
+ * guide identical. It is `setupGuide.defaultConfirmLabel` on the host's pack
39
+ * now, so the vendor's product name is at least chosen rather than compiled in.
38
40
  */
39
- const CONFIRM_LABEL_FALLBACK = 'Já habilitei o Checkout Integrado';
41
+
40
42
 
41
43
  /** Does this section end in a question only the owner can answer? */
42
44
  function isConfirmable(section: SetupSection): boolean {
@@ -134,12 +136,13 @@ export interface SetupGuideSectionProps {
134
136
  * still there to press Revisar on.
135
137
  */
136
138
  function ConfirmedRow({ section, onReopen }: { section: SetupSection; onReopen: () => void }) {
139
+ const copy = usePaymentsSettingsCopy().setupGuide;
137
140
  return (
138
141
  <DoneRow
139
142
  testId="payments-setup-confirmed"
140
143
  label={section.doneSummary?.label ?? section.title}
141
- value={section.doneSummary?.value ?? 'Confirmado por você'}
142
- editLabel="Revisar"
144
+ value={section.doneSummary?.value ?? copy.confirmedByYou}
145
+ editLabel={copy.reviewAction}
143
146
  onEdit={onReopen}
144
147
  />
145
148
  );
@@ -157,12 +160,13 @@ function ConfirmedRow({ section, onReopen }: { section: SetupSection; onReopen:
157
160
  function confirmationOf(
158
161
  guide: Guide,
159
162
  { stage, confirmed, editing }: { stage: number; confirmed: boolean; editing: boolean },
163
+ fallbackLabel: string,
160
164
  ): { label: string; settledSection: SetupSection | null } {
161
165
  const asking = confirmableStage(guide);
162
166
  const section = asking >= 0 ? sectionAt(guide, asking) : null;
163
167
  const settled = confirmed && !editing && asking >= 0 && stage > asking;
164
168
  return {
165
- label: section?.confirmLabel ?? CONFIRM_LABEL_FALLBACK,
169
+ label: section?.confirmLabel ?? fallbackLabel,
166
170
  settledSection: settled ? section : null,
167
171
  };
168
172
  }
@@ -193,7 +197,12 @@ export function SetupGuideSection({
193
197
 
194
198
  const stage = effectiveStage(guide, confirmed, editing, stored);
195
199
  const open = sectionAt(guide, stage);
196
- const { label, settledSection } = confirmationOf(guide, { stage, confirmed, editing });
200
+ const guideCopy = usePaymentsSettingsCopy().setupGuide;
201
+ const { label, settledSection } = confirmationOf(
202
+ guide,
203
+ { stage, confirmed, editing },
204
+ guideCopy.defaultConfirmLabel,
205
+ );
197
206
 
198
207
  return (
199
208
  <Box data-testid="payments-setup">
@@ -1,8 +1,13 @@
1
1
  import { Box } from "@mui/material";
2
2
  import { useRef, type JSX } from "react";
3
3
 
4
+ import { useCheckoutCopy } from "./copy-context";
5
+ import type { WalletCopy } from "./screens-copy";
4
6
  import type { CheckoutOrder } from "./types";
5
7
 
8
+ /** The Apple half of {@link WalletCopy} — what this button and its sheet say. */
9
+ type ApplePayCopy = WalletCopy["applePay"];
10
+
6
11
  /**
7
12
  * The Apple-owned pay button (FUT-472), on `ApplePaySession` — the API Safari
8
13
  * ships natively; there is no script to load. This component owns token
@@ -85,13 +90,16 @@ export function applePaySupported(): boolean {
85
90
  }
86
91
 
87
92
  /** The sheet's payment request, from the server-authoritative order total. */
88
- function paymentRequest(order: CheckoutOrder): ApplePayPaymentRequest {
93
+ function paymentRequest(order: CheckoutOrder, copy: ApplePayCopy): ApplePayPaymentRequest {
89
94
  return {
90
95
  countryCode: "BR",
91
96
  currencyCode: "BRL",
92
97
  supportedNetworks: APPLE_PAY_SUPPORTED_NETWORKS,
93
98
  merchantCapabilities: ["supports3DS"],
94
- total: { label: "Total do pedido", amount: (order.totalCents / 100).toFixed(2) },
99
+ // The one line Apple's own sheet renders from us, so it is the host's
100
+ // (FUT-760) — the rest of that sheet is Apple's, in the buyer's own
101
+ // system language.
102
+ total: { label: copy.orderTotal, amount: (order.totalCents / 100).toFixed(2) },
95
103
  };
96
104
  }
97
105
 
@@ -109,8 +117,8 @@ export interface ApplePayButtonProps {
109
117
  * The host's merchant-validation port: exchange `validationURL` for an
110
118
  * Apple merchant session, SERVER-SIDE (the merchant identity certificate
111
119
  * must never reach a browser). Absent — the external prerequisites are not
112
- * done, or the host has not wired it — the session aborts with a pt-BR
113
- * message and the card form remains the way to pay.
120
+ * done, or the host has not wired it — the session aborts with the host's
121
+ * `cannotStart` sentence and the card form remains the way to pay.
114
122
  */
115
123
  validateMerchant?: (validationURL: string) => Promise<unknown>;
116
124
  }
@@ -120,20 +128,21 @@ function runSession(
120
128
  Session: ApplePaySessionClass,
121
129
  order: CheckoutOrder,
122
130
  handlers: ApplePayButtonProps,
131
+ copy: ApplePayCopy,
123
132
  ): void {
124
- const session = new Session(APPLE_PAY_VERSION, paymentRequest(order));
133
+ const session = new Session(APPLE_PAY_VERSION, paymentRequest(order, copy));
125
134
  session.onvalidatemerchant = (event) => {
126
135
  const validate = handlers.validateMerchant;
127
136
  if (!validate) {
128
137
  session.abort();
129
- handlers.onError("Não foi possível iniciar o Apple Pay nesta loja. Pague com cartão.");
138
+ handlers.onError(copy.cannotStart);
130
139
  return;
131
140
  }
132
141
  validate(event.validationURL)
133
142
  .then((merchantSession) => session.completeMerchantValidation(merchantSession))
134
143
  .catch(() => {
135
144
  session.abort();
136
- handlers.onError("Não foi possível iniciar o Apple Pay. Tente novamente ou pague com cartão.");
145
+ handlers.onError(copy.cannotComplete);
137
146
  });
138
147
  };
139
148
  session.onpaymentauthorized = (event) => {
@@ -162,15 +171,16 @@ export function ApplePayButton(props: ApplePayButtonProps): JSX.Element | null {
162
171
  // over a stale charge target.
163
172
  const current = useRef(props);
164
173
  current.current = props;
174
+ const copy = useCheckoutCopy().screens.wallet.applePay;
165
175
  const Session = applePaySessionClass();
166
176
  if (!Session || !applePaySupported()) return null;
167
177
  return (
168
178
  <Box
169
179
  component="button"
170
180
  type="button"
171
- aria-label="Pagar com Apple Pay"
181
+ aria-label={copy.payAction}
172
182
  data-testid="apple-pay-button"
173
- onClick={() => runSession(Session, current.current.order, current.current)}
183
+ onClick={() => runSession(Session, current.current.order, current.current, copy)}
174
184
  sx={{
175
185
  WebkitAppearance: "-apple-pay-button",
176
186
  // Apple draws the button; these only give it room. The fallback
@@ -28,7 +28,7 @@
28
28
  * the two must not disagree about what a chain needs.
29
29
  */
30
30
 
31
- import { validateCpf } from "../../card";
31
+ import { isValidCpf } from "../../card";
32
32
 
33
33
  import type { CheckoutChainLink, CheckoutCustomerField, PaymentMethod } from "./types";
34
34
 
@@ -105,8 +105,10 @@ const RULES: Record<CheckoutCustomerField["type"], (value: string) => boolean> =
105
105
  PHONE: (value) => /^\d{10,11}$/.test(value.replace(/\D/g, "")),
106
106
  MOBILE: (value) => /^\d{2}9\d{8}$/.test(value.replace(/\D/g, "")),
107
107
  // The real check-digit rule, not a length test: the same validator the CPF
108
- // input has always used, so the form and the gate cannot disagree.
109
- CPF: (value) => validateCpf(value) === undefined,
108
+ // input has always used, so the form and the gate cannot disagree. The
109
+ // PREDICATE rather than the message — this is a gate, and it would only be
110
+ // throwing the sentence away (FUT-760).
111
+ CPF: (value) => isValidCpf(value),
110
112
  };
111
113
 
112
114
  /** Whether a declared field is satisfied by what the buyer typed. */