@12-apps/payments-frontend 3.9.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/package.json +2 -2
  2. package/src/activation/charge-copy.ts +10 -0
  3. package/src/activation/use-activation-charge.ts +10 -8
  4. package/src/card/copy.ts +115 -0
  5. package/src/card/cpf.ts +12 -5
  6. package/src/card/fields.tsx +17 -12
  7. package/src/card/format.ts +32 -16
  8. package/src/card/index.ts +3 -1
  9. package/src/card/pt-BR.ts +51 -0
  10. package/src/card/stripe-token.ts +5 -6
  11. package/src/card/tokenize.ts +28 -32
  12. package/src/components/CheckoutPayment.tsx +37 -201
  13. package/src/components/ConfirmCredentialSave.tsx +8 -10
  14. package/src/components/ConnectionCard.tsx +23 -18
  15. package/src/components/ConnectionProbe.tsx +3 -1
  16. package/src/components/CredentialFieldStack.tsx +8 -2
  17. package/src/components/CredentialFields.tsx +13 -6
  18. package/src/components/CredentialFormAlerts.tsx +28 -19
  19. package/src/components/EnvironmentTabs.tsx +23 -15
  20. package/src/components/OAuthPanel.tsx +5 -6
  21. package/src/components/PaymentProviderSettings.tsx +45 -13
  22. package/src/components/ProviderConnection.tsx +37 -28
  23. package/src/components/ProviderList.tsx +6 -3
  24. package/src/components/ProviderPriorityList.tsx +18 -16
  25. package/src/components/ProviderStatusBar.tsx +24 -24
  26. package/src/components/SetupGuideSection.tsx +15 -6
  27. package/src/components/checkout/apple-pay-button.tsx +19 -9
  28. package/src/components/checkout/buyer-fields.ts +5 -3
  29. package/src/components/checkout/buyer-gate.ts +68 -0
  30. package/src/components/checkout/buyer-info-form.tsx +14 -15
  31. package/src/components/checkout/card-instruments.ts +31 -12
  32. package/src/components/checkout/card-view.tsx +11 -7
  33. package/src/components/checkout/checkout-flow.tsx +4 -1
  34. package/src/components/checkout/checkout-steps.tsx +3 -1
  35. package/src/components/checkout/client-context.tsx +28 -16
  36. package/src/components/checkout/client.ts +43 -16
  37. package/src/components/checkout/copy-context.tsx +83 -0
  38. package/src/components/checkout/google-pay-button.tsx +19 -7
  39. package/src/components/checkout/method-picker.tsx +34 -10
  40. package/src/components/checkout/payer-summary.tsx +8 -6
  41. package/src/components/checkout/payment-error-panel.tsx +8 -5
  42. package/src/components/checkout/pix-view.tsx +16 -9
  43. package/src/components/checkout/providers/hosted-link.tsx +26 -16
  44. package/src/components/checkout/pt-BR.ts +27 -0
  45. package/src/components/checkout/screens-copy.ts +201 -0
  46. package/src/components/checkout/screens-pt-BR.ts +101 -0
  47. package/src/components/checkout/transport.ts +34 -8
  48. package/src/components/checkout/use-card-checkout.ts +13 -5
  49. package/src/components/checkout/use-checkout-controller.ts +5 -48
  50. package/src/components/checkout/view-copy.ts +13 -0
  51. package/src/components/checkout/wallet-pane.tsx +13 -8
  52. package/src/components/checkout-payment-copy.ts +88 -0
  53. package/src/components/checkout-payment-pt-BR.ts +50 -0
  54. package/src/components/checkout-payment-types.ts +25 -0
  55. package/src/components/checkout-payment-views.tsx +225 -0
  56. package/src/components/connection-state.ts +22 -10
  57. package/src/components/credential-rules.ts +8 -3
  58. package/src/components/platform/ConnectApplicationPanel.tsx +6 -2
  59. package/src/components/platform/ConnectEnvironmentCard.tsx +19 -15
  60. package/src/components/platform/HomologacaoGuideCard.tsx +23 -19
  61. package/src/components/platform/HomologacaoOutcomeCard.tsx +24 -16
  62. package/src/components/platform/PlatformHomologacao.tsx +12 -8
  63. package/src/components/settings-copy-context.tsx +50 -0
  64. package/src/components/settings-copy.ts +304 -0
  65. package/src/components/settings-pt-BR.ts +169 -0
  66. package/src/flows/copy.ts +12 -0
  67. package/src/flows/create-payment-flows.tsx +8 -1
  68. package/src/flows/runtime.tsx +12 -4
  69. package/src/flows/screens-vault.tsx +10 -10
  70. package/src/flows/types.ts +5 -2
  71. package/src/flows/use-add-card.ts +10 -5
  72. package/src/index.ts +63 -0
@@ -8,6 +8,7 @@ import type { MaskedProviderConfig, ProviderDescriptor } from '@12-apps/payments
8
8
  import type { PaymentsSettingsClient } from '../client';
9
9
  import { isConnected } from './connection-state';
10
10
  import { ProbeAlert, ProbeChecklist, type VerifyProbe } from './CredentialFormAlerts';
11
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
11
12
 
12
13
  /**
13
14
  * "Testar conexão" for a store whose connection is a GRANT, not a form.
@@ -41,6 +42,7 @@ export function ConnectionProbe({
41
42
  const [busy, setBusy] = useState(false);
42
43
  const [error, setError] = useState<string | null>(null);
43
44
  const [probe, setProbe] = useState<VerifyProbe | null>(null);
45
+ const copy = usePaymentsSettingsCopy().credentials;
44
46
  if (!isConnected(config)) return null;
45
47
 
46
48
  const verify = async () => {
@@ -69,7 +71,7 @@ export function ConnectionProbe({
69
71
  onClick={() => void verify()}
70
72
  sx={{ textTransform: 'none' }}
71
73
  >
72
- {busy ? <CircularProgress size={18} /> : 'Testar conexão'}
74
+ {busy ? <CircularProgress size={18} /> : copy.probeAction}
73
75
  </Button>
74
76
  {error ? <Alert severity="error">{error}</Alert> : null}
75
77
  {probe ? <ProbeAlert probe={probe} busy={busy} onRetry={() => void verify()} /> : null}
@@ -8,6 +8,7 @@ import { CredentialField } from './CredentialFields';
8
8
  import { saveLabel } from './credential-rules';
9
9
  import { FormActions, ReverifyWarning } from './CredentialFormAlerts';
10
10
  import type { CredentialFormState } from './ProviderCredentialForm';
11
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
11
12
 
12
13
  /**
13
14
  * The live inputs for the step still owed, plus the one button that commits
@@ -27,9 +28,10 @@ export function CredentialFields({
27
28
  /** A real charge has landed through this connection — see `ReverifyWarning`. */
28
29
  proven: boolean;
29
30
  }) {
31
+ const copy = usePaymentsSettingsCopy().credentials;
30
32
  return (
31
33
  <Stack spacing={2}>
32
- {proven ? <ReverifyWarning /> : null}
34
+ {proven ? <ReverifyWarning displayName={descriptor.displayName} /> : null}
33
35
  {descriptor.credentialSchema.map((spec) => (
34
36
  <CredentialField
35
37
  key={spec.key}
@@ -46,7 +48,11 @@ export function CredentialFields({
46
48
  ))}
47
49
  <FormActions
48
50
  busy={form.busy}
49
- label={saveLabel(descriptor, form.complete)}
51
+ label={saveLabel(copy, descriptor, form.complete)}
52
+ // The FACT, not the words. This bar used to read `label.includes(
53
+ // 'testar')` to decide which promise to print — which quietly made the
54
+ // sentence depend on how a host spelled its button (FUT-760).
55
+ willTest={form.complete}
50
56
  disabled={form.nothingEdited || !form.valid}
51
57
  onSave={form.requestSave}
52
58
  />
@@ -5,6 +5,8 @@ import { Box, Button, Stack, TextField, Typography } from '@mui/material';
5
5
  import type { CredentialFieldSpec, MaskedFieldState } from '@12-apps/payments-backend';
6
6
 
7
7
  import { LINKISH_SX, T } from './panel-tokens';
8
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
9
+ import type { CredentialFormCopy } from './settings-copy';
8
10
 
9
11
  /**
10
12
  * The schema-driven credential inputs, and the one-line summary a finished
@@ -27,6 +29,7 @@ function secretPresentation(
27
29
  state: MaskedFieldState | undefined,
28
30
  value: string | undefined,
29
31
  required: boolean,
32
+ copy: CredentialFormCopy,
30
33
  ): FieldPresentation {
31
34
  const configured = state?.configured ?? false;
32
35
  return {
@@ -34,7 +37,7 @@ function secretPresentation(
34
37
  value: value ?? '',
35
38
  required: required && !configured,
36
39
  placeholder: configured ? (state?.hint ?? '') : undefined,
37
- helperText: configured ? 'Configurado — deixe em branco para manter o valor atual.' : undefined,
40
+ helperText: configured ? copy.configuredKeepBlank : undefined,
38
41
  };
39
42
  }
40
43
 
@@ -42,8 +45,9 @@ function fieldPresentation(
42
45
  spec: CredentialFieldSpec,
43
46
  state: MaskedFieldState | undefined,
44
47
  value: string | undefined,
48
+ copy: CredentialFormCopy,
45
49
  ): FieldPresentation {
46
- if (spec.secret) return secretPresentation(state, value, spec.required);
50
+ if (spec.secret) return secretPresentation(state, value, spec.required, copy);
47
51
  const configured = state?.configured ?? false;
48
52
  return {
49
53
  type: 'text',
@@ -83,7 +87,8 @@ interface CredentialFieldProps {
83
87
 
84
88
  /** One schema-driven form field. Secrets are write-only: hint, never value. */
85
89
  export function CredentialField({ spec, state, value, onChange, check }: CredentialFieldProps) {
86
- const presentation = fieldPresentation(spec, state, value);
90
+ const copy = usePaymentsSettingsCopy().credentials;
91
+ const presentation = fieldPresentation(spec, state, value, copy);
87
92
  // Label ABOVE the box, not floating in it. Four credentials whose names are
88
93
  // the only thing distinguishing them (`sk_`, `pk_`, `whsec_`, `acct_`) are
89
94
  // read as a column, and a floating label disappears the moment a value is
@@ -103,7 +108,7 @@ export function CredentialField({ spec, state, value, onChange, check }: Credent
103
108
  {spec.label}
104
109
  {spec.advanced ? (
105
110
  <Box component="span" sx={{ fontWeight: 500, color: T.ink4 }}>
106
- {' · só para plataformas Connect'}
111
+ {copy.advancedSuffix}
107
112
  </Box>
108
113
  ) : null}
109
114
  </Typography>
@@ -222,16 +227,18 @@ export function DoneRow({
222
227
  value,
223
228
  mono,
224
229
  onEdit,
225
- editLabel = 'Alterar',
230
+ editLabel,
226
231
  testId,
227
232
  }: {
228
233
  label: string;
229
234
  value: string;
230
235
  mono?: boolean;
231
236
  onEdit: () => void;
237
+ /** Defaults to the host's `changeAction` when the caller names none. */
232
238
  editLabel?: string;
233
239
  testId: string;
234
240
  }) {
241
+ const copy = usePaymentsSettingsCopy().credentials;
235
242
  return (
236
243
  // Green, not grey: a finished step reads as an achievement at a glance, and
237
244
  // the whole point of collapsing it is that the eye can skip it on the way to
@@ -263,7 +270,7 @@ export function DoneRow({
263
270
  </Typography>
264
271
  <Box sx={{ flexGrow: 1 }} />
265
272
  <Button size="small" onClick={onEdit} data-testid={`${testId}-edit`} sx={LINKISH_SX}>
266
- {editLabel}
273
+ {editLabel ?? copy.changeAction}
267
274
  </Button>
268
275
  </Stack>
269
276
  );
@@ -4,8 +4,11 @@ import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/ma
4
4
 
5
5
  import type { VerifiedProviderConfig } from '@12-apps/payments-backend';
6
6
 
7
- import { ENVIRONMENT_LABELS } from './EnvironmentTabs';
7
+ import { environmentLabels } from './EnvironmentTabs';
8
+ import type { CredentialFormCopy } from './settings-copy';
9
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
8
10
  import { BAR_MSG_SX, BAR_SX, BTN_PRIMARY_SX } from './panel-tokens';
11
+ import { richText } from './rich-text';
9
12
 
10
13
  /**
11
14
  * The credential step's alerts and its one button.
@@ -41,14 +44,13 @@ export function ProbeAlert({
41
44
  onRetry: () => void;
42
45
  }) {
43
46
  if (probe.ok) return null;
44
- const where = ENVIRONMENT_LABELS[probe.environment];
47
+ const copy = usePaymentsSettingsCopy();
48
+ const where = environmentLabels(copy.environment)[probe.environment];
45
49
  // The ADAPTER's sentence when it has one. Only it knows the provider's name,
46
50
  // what that provider's own app calls this field and which screen states it —
47
51
  // "confira as credenciais deste ambiente" is advice you can follow all
48
52
  // afternoon without ever finding the value you were asked to check.
49
- const message =
50
- probe.message ??
51
- `Não foi possível conectar em ${where}. Confira as credenciais deste ambiente.`;
53
+ const message = probe.message ?? copy.credentials.probeFailed(where);
52
54
 
53
55
  // An outage is not a rejection. The credential is already SAVED and may well
54
56
  // be perfect; nothing was learned about it. So it is amber rather than red,
@@ -69,7 +71,7 @@ export function ProbeAlert({
69
71
  data-testid="payments-verify-retry"
70
72
  sx={{ textTransform: 'none' }}
71
73
  >
72
- Testar conexão
74
+ {copy.credentials.probeAction}
73
75
  </Button>
74
76
  ) : undefined
75
77
  }
@@ -94,11 +96,13 @@ const SCREEN_READER_ONLY = {
94
96
  } as const;
95
97
 
96
98
  /** How each verdict reads at a glance — the mark, and what it is called. */
97
- const CHECK_MARKS = {
98
- PASS: { mark: '✓', color: 'success.main', label: 'Verificado' },
99
- FAIL: { mark: '', color: 'error.main', label: 'Corrigir' },
100
- UNCHECKED: { mark: '', color: 'text.secondary', label: 'Não verificável' },
101
- } as const;
99
+ function checkMarks(copy: CredentialFormCopy) {
100
+ return {
101
+ PASS: { mark: '', color: 'success.main', label: copy.checkPass },
102
+ FAIL: { mark: '', color: 'error.main', label: copy.checkFail },
103
+ UNCHECKED: { mark: '–', color: 'text.secondary', label: copy.uncheckable },
104
+ } as const;
105
+ }
102
106
 
103
107
  /**
104
108
  * What the probe established, credential by credential (FUT-796).
@@ -122,12 +126,13 @@ export function ProbeChecklist({
122
126
  /** Keep only the verdicts this predicate accepts — see `ProviderForm`. */
123
127
  only?: (key: string) => boolean;
124
128
  }) {
129
+ const marks = checkMarks(usePaymentsSettingsCopy().credentials);
125
130
  const shown = (probe.checks ?? []).filter((check) => only?.(check.key) ?? true);
126
131
  if (shown.length === 0) return null;
127
132
  return (
128
133
  <Stack spacing={0.5} data-testid="payments-probe-checks">
129
134
  {shown.map((check) => {
130
- const { mark, color, label } = CHECK_MARKS[check.status];
135
+ const { mark, color, label } = marks[check.status];
131
136
  return (
132
137
  <Stack
133
138
  key={check.key}
@@ -164,11 +169,11 @@ export function ProbeChecklist({
164
169
  * correct and entirely invisible, and an owner who came to fix a typo deserves
165
170
  * to learn it before typing rather than from a quiet storefront.
166
171
  */
167
- export function ReverifyWarning() {
172
+ export function ReverifyWarning({ displayName }: { displayName: string }) {
173
+ const copy = usePaymentsSettingsCopy().credentials;
168
174
  return (
169
175
  <Alert severity="warning" data-testid="payments-reverify-warning">
170
- Esta loja já está verificada. Trocar a InfiniteTag <strong>exige uma nova verificação</strong>{' '}
171
- e a loja <strong>para de receber</strong> por este provedor até que ela termine.
176
+ {richText(copy.reverifyWarning(displayName))}
172
177
  </Alert>
173
178
  );
174
179
  }
@@ -187,23 +192,27 @@ export function ReverifyWarning() {
187
192
  export function FormActions({
188
193
  busy,
189
194
  label,
195
+ willTest,
190
196
  disabled,
191
197
  onSave,
192
198
  }: {
193
199
  busy: string | null;
194
200
  label: string;
201
+ /** Whether pressing save will also SEND the set to the provider. */
202
+ willTest: boolean;
195
203
  disabled: boolean;
196
204
  onSave: () => void;
197
205
  }) {
198
206
  // The bar states what pressing it costs, which is the half a verb cannot
199
207
  // carry: a complete set is sent to the provider on save, and a partial one is
200
208
  // only written down. The owner reads the promise and the button in one line.
209
+ const copy = usePaymentsSettingsCopy().credentials;
201
210
  const message =
202
211
  busy === 'verify'
203
- ? 'Testando a conexão…'
204
- : label.includes('testar')
205
- ? 'Salvamos e testamos as chaves no provedor antes de seguir.'
206
- : 'Guardamos o que você já preencheu. Complete os campos para testar.';
212
+ ? copy.probeRunning
213
+ : willTest
214
+ ? copy.probeSaveNote
215
+ : copy.probeIncompleteNote;
207
216
 
208
217
  return (
209
218
  <Box sx={BAR_SX} data-testid="payments-form-bar">
@@ -5,12 +5,19 @@ import { Alert, Box, Tab, Tabs, Typography } from '@mui/material';
5
5
  import type { PaymentEnvironment } from '@12-apps/payments-backend';
6
6
 
7
7
  import { T } from './panel-tokens';
8
+ import type { EnvironmentCopy } from './settings-copy';
9
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
8
10
 
9
- /** One environment's human name — the tabs and the probe result share it. */
10
- export const ENVIRONMENT_LABELS: Record<PaymentEnvironment, string> = {
11
- SANDBOX: 'Sandbox',
12
- PRODUCTION: 'Produção',
13
- };
11
+ /**
12
+ * One environment's human name — the tabs and the probe result share it.
13
+ *
14
+ * Built from the host's pack rather than declared here (FUT-760): the names
15
+ * appear inside the probe's own failure sentence too, so one spelling has to
16
+ * serve both or the screen contradicts itself.
17
+ */
18
+ export function environmentLabels(copy: EnvironmentCopy): Record<PaymentEnvironment, string> {
19
+ return { SANDBOX: copy.sandbox, PRODUCTION: copy.production };
20
+ }
14
21
 
15
22
  /**
16
23
  * Sentence case, because these are place names rather than shouted commands —
@@ -47,6 +54,7 @@ export function EnvironmentSelector({
47
54
  environment: PaymentEnvironment;
48
55
  onChange: (next: PaymentEnvironment) => void;
49
56
  }) {
57
+ const copy = usePaymentsSettingsCopy().environment;
50
58
  // A segmented control, not underlined tabs. Two options that CHANGE WHAT
51
59
  // EVERY FIELD BELOW MEANS should look like a switch being thrown, and the
52
60
  // label above says which switch — "Ambiente" alone reads as a page section.
@@ -55,12 +63,12 @@ export function EnvironmentSelector({
55
63
  <Typography
56
64
  sx={{ display: 'block', fontSize: '12px', fontWeight: 650, color: T.ink2, mb: '5px' }}
57
65
  >
58
- Ambiente desta conexão
66
+ {copy.groupLabel}
59
67
  </Typography>
60
68
  <Tabs
61
69
  value={environment}
62
70
  onChange={(_, next: PaymentEnvironment) => onChange(next)}
63
- aria-label="Ambiente desta conexão"
71
+ aria-label={copy.groupLabel}
64
72
  data-testid="payments-environment-tabs"
65
73
  slotProps={{ indicator: { sx: { display: 'none' } } }}
66
74
  sx={{
@@ -73,13 +81,13 @@ export function EnvironmentSelector({
73
81
  }}
74
82
  >
75
83
  <Tab
76
- label="Sandbox"
84
+ label={copy.sandbox}
77
85
  value="SANDBOX"
78
86
  data-testid="payments-environment-SANDBOX"
79
87
  sx={TAB_SX}
80
88
  />
81
89
  <Tab
82
- label="Produção"
90
+ label={copy.production}
83
91
  value="PRODUCTION"
84
92
  data-testid="payments-environment-PRODUCTION"
85
93
  sx={TAB_SX}
@@ -126,7 +134,9 @@ export function EnvironmentNotice({
126
134
  */
127
135
  band?: boolean;
128
136
  }) {
129
- const elsewhere = active !== null && active !== environment ? ENVIRONMENT_LABELS[active] : null;
137
+ const copy = usePaymentsSettingsCopy().environment;
138
+ const elsewhere =
139
+ active !== null && active !== environment ? environmentLabels(copy)[active] : null;
130
140
  const production = environment === 'PRODUCTION';
131
141
  return (
132
142
  <Alert
@@ -137,12 +147,10 @@ export function EnvironmentNotice({
137
147
  sx={band ? { borderRadius: 0, py: 0.5, px: 3 } : { py: 0.5 }}
138
148
  >
139
149
  <strong>
140
- {production ? 'Produção — dinheiro real.' : 'Sandbox — ambiente de teste.'}
150
+ {production ? copy.productionMeaning : copy.sandboxMeaning}
141
151
  </strong>{' '}
142
- {production
143
- ? 'Tudo o que você fizer aqui vale para as vendas da loja.'
144
- : 'Nenhum valor sai ou entra de verdade.'}
145
- {elsewhere ? ` Hoje a loja está usando ${elsewhere}.` : ''}
152
+ {production ? copy.productionConsequence : copy.sandboxConsequence}
153
+ {elsewhere ? copy.storeIsUsing(elsewhere) : ''}
146
154
  </Alert>
147
155
  );
148
156
  }
@@ -8,6 +8,7 @@ import { LINKISH_SX } from './panel-tokens';
8
8
  import { ProviderConnection } from './ProviderConnection';
9
9
  import type { ActivePanelProps } from './ProviderPanel';
10
10
  import { ProviderCard } from './ProviderPanel';
11
+ import { usePaymentsSettingsCopy } from './settings-copy-context';
11
12
 
12
13
  /**
13
14
  * The OAuth branch of a provider's screen, and the switch between its two
@@ -108,6 +109,7 @@ export function OAuthPanel({
108
109
  walkthrough: (path: 'oauth' | 'credentials') => ReactNode;
109
110
  form: ReactNode;
110
111
  }) {
112
+ const copy = usePaymentsSettingsCopy().oauth;
111
113
  // Which path the owner is on. The disclosure is no longer just a container —
112
114
  // opening it CHOOSES the credentials path, so the panel has to know.
113
115
  const [manual, setManual] = useState(false);
@@ -119,10 +121,7 @@ export function OAuthPanel({
119
121
  if (!prepareConnect) {
120
122
  return (
121
123
  <ProviderCard header={statusBar}>
122
- <Alert severity="info">
123
- Este provedor conecta por autorização, mas o botão de conexão não está disponível nesta
124
- instalação. Você ainda pode conectar informando as credenciais manualmente.
125
- </Alert>
124
+ <Alert severity="info">{copy.connectUnavailable}</Alert>
126
125
  {form}
127
126
  {probe}
128
127
  {verification}
@@ -134,7 +133,7 @@ export function OAuthPanel({
134
133
  <ProviderCard header={statusBar}>
135
134
  {manual ? (
136
135
  <PathSwitch
137
- label="Prefiro conectar por autorização"
136
+ label={copy.preferOAuth}
138
137
  expanded={false}
139
138
  onChange={() => setManual(false)}
140
139
  testId="payments-oauth-fallback"
@@ -153,7 +152,7 @@ export function OAuthPanel({
153
152
  {probe}
154
153
  {descriptor.credentialSchema.length > 0 ? (
155
154
  <PathSwitch
156
- label="Prefiro informar as credenciais manualmente"
155
+ label={copy.preferCredentials}
157
156
  expanded={manual}
158
157
  onChange={setManual}
159
158
  testId="payments-manual-fallback"
@@ -21,6 +21,8 @@ import {
21
21
  type ProviderChangeHandler,
22
22
  } from './settings-state';
23
23
  import { openSection, SetupGuideSection } from './SetupGuideSection';
24
+ import type { PaymentsSettingsCopy } from './settings-copy';
25
+ import { PaymentsSettingsCopyProvider } from './settings-copy-context';
24
26
 
25
27
  /**
26
28
  * Plug-and-play settings page for payment providers — the reusable
@@ -40,6 +42,15 @@ import { openSection, SetupGuideSection } from './SetupGuideSection';
40
42
  */
41
43
  export interface PaymentProviderSettingsProps {
42
44
  client: PaymentsSettingsClient;
45
+ /**
46
+ * Every sentence this surface renders (FUT-760).
47
+ *
48
+ * REQUIRED, with no default in the package: a default would be one product's
49
+ * Portuguese compiled into every other product's settings screen. Passed
50
+ * once, here, and read from context by the fifteen components below —
51
+ * `PT_BR_PAYMENTS_SETTINGS_COPY` reproduces today's wording verbatim.
52
+ */
53
+ copy: PaymentsSettingsCopy;
43
54
  /** Called after any state-changing action, e.g. to refresh host UI. */
44
55
  onChanged?: (config: MaskedProviderConfig) => void;
45
56
  /**
@@ -233,8 +244,34 @@ function ProviderScreen({ onBack, ...panel }: ProviderScreenProps) {
233
244
  );
234
245
  }
235
246
 
247
+ /**
248
+ * The walkthrough slot the panel renders, per the connection path it has open.
249
+ *
250
+ * Extracted for the size gate, and it reads better here anyway: the mount above
251
+ * is about resolving WHICH provider is open, and this is about what its
252
+ * walkthrough shows once one is.
253
+ */
254
+ function guideSlot(
255
+ guide: ProviderSetupGuide | null,
256
+ ack: { confirmed: boolean; confirm: () => void; withdraw: () => void },
257
+ ) {
258
+ return ({ path, ...slots }: Parameters<NonNullable<ProviderScreenProps['guide']>>[0]) => (
259
+ <SetupGuideSection
260
+ // Which walkthrough, per the path the panel has open. Only the SECTIONS
261
+ // differ; `blocked`/`hidden` at the call site stay computed from the base
262
+ // guide, which `credentialsPath` is required to mirror.
263
+ guide={guideForPath(guide, path)}
264
+ confirmed={ack.confirmed}
265
+ onConfirm={ack.confirm}
266
+ onReopen={ack.withdraw}
267
+ {...slots}
268
+ />
269
+ );
270
+ }
271
+
236
272
  export function PaymentProviderSettings({
237
273
  client,
274
+ copy,
238
275
  onChanged,
239
276
  prepareConnect,
240
277
  initialProvider = null,
@@ -268,10 +305,15 @@ export function PaymentProviderSettings({
268
305
  if (!view) return <CircularProgress data-testid="payments-settings-loading" />;
269
306
 
270
307
  if (!active) {
271
- return <ProviderList view={view} client={client} reload={reload} onSelect={openProvider} />;
308
+ return (
309
+ <PaymentsSettingsCopyProvider copy={copy}>
310
+ <ProviderList view={view} client={client} reload={reload} onSelect={openProvider} />
311
+ </PaymentsSettingsCopyProvider>
312
+ );
272
313
  }
273
314
 
274
315
  return (
316
+ <PaymentsSettingsCopyProvider copy={copy}>
275
317
  <ProviderScreen
276
318
  descriptor={active}
277
319
  config={activeConfig}
@@ -296,18 +338,8 @@ export function PaymentProviderSettings({
296
338
  onVerified: () => void reload(),
297
339
  onSetupIncomplete: ack.withdraw,
298
340
  })}
299
- guide={({ path, ...slots }) => (
300
- <SetupGuideSection
301
- // Which walkthrough, per the path the panel has open. Only the
302
- // SECTIONS differ; `blocked`/`hidden` above stay computed from the
303
- // base guide, which `credentialsPath` is required to mirror.
304
- guide={guideForPath(guide, path)}
305
- confirmed={ack.confirmed}
306
- onConfirm={ack.confirm}
307
- onReopen={ack.withdraw}
308
- {...slots}
309
- />
310
- )}
341
+ guide={guideSlot(guide, ack)}
311
342
  />
343
+ </PaymentsSettingsCopyProvider>
312
344
  );
313
345
  }