@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
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Every sentence the LEGACY `CheckoutPayment` step renders (FUT-760).
3
+ *
4
+ * A required prop rather than a context, unlike the factory's screens: this
5
+ * component is one plug-and-play step a host drops in itself, and there is no
6
+ * tree of pieces below it to reach. The host already passes `reference`,
7
+ * `amount` and `tokenizeCard` at the same call; the words go beside them.
8
+ *
9
+ * The split is the one the whole port draws. WHICH state the charge is in — a
10
+ * PIX code waiting to be scanned, a hosted page the buyer is about to be sent
11
+ * to, a store with no provider at all — is knowledge of the payments
12
+ * lifecycle, and it stays here. The words are the host's.
13
+ */
14
+
15
+ /** The money line, and the locale that renders it. */
16
+ export interface LegacyMoneyCopy {
17
+ /** The heading above the amount being paid. */
18
+ totalLabel(formattedAmount: string): string;
19
+ /** The button that authorizes it, on both the saved-card and hosted paths. */
20
+ payAction(formattedAmount: string): string;
21
+ /**
22
+ * The BCP-47 tag the amount is formatted in — `'pt-BR'` here, and hard-coded
23
+ * before this port. The CURRENCY is not here: it arrives on the `Money` the
24
+ * host passes, which is the store's own, not a translator's.
25
+ */
26
+ amountLocale: string;
27
+ }
28
+
29
+ /** The two method cards, before a charge exists. */
30
+ export interface LegacyMethodCopy {
31
+ groupLabel: string;
32
+ pixTitle: string;
33
+ pixSubtitle: string;
34
+ cardTitle: string;
35
+ cardSubtitle: string;
36
+ /** PIX's own action — this step generates the code rather than charging. */
37
+ generatePixAction: string;
38
+ /** A REDIRECT provider offers no choice: one button, and the buyer leaves. */
39
+ continueToPaymentAction: string;
40
+ }
41
+
42
+ /** The PIX panel: the code, and the wait. */
43
+ export interface LegacyPixCopy {
44
+ qrAlt: string;
45
+ /** The label on the copy-and-paste payload field. */
46
+ copyPasteLabel: string;
47
+ copyAction: string;
48
+ copiedAction: string;
49
+ awaiting: string;
50
+ }
51
+
52
+ /** The card form, and the saved cards above it. */
53
+ export interface LegacyCardCopy {
54
+ /** The heading over the saved-card radios. */
55
+ heading: string;
56
+ numberLabel: string;
57
+ holderLabel: string;
58
+ /** Its parenthetical spells the ORDER — "MM/AA" in pt-BR, "MM/YY" in en. */
59
+ expiryLabel: string;
60
+ cvvLabel: string;
61
+ payAction: string;
62
+ /** The trailing radio that opens the form. */
63
+ newCard: string;
64
+ /**
65
+ * How a saved card reads. Takes the parts rather than a sentence, because
66
+ * the expiry is optional — a provider that shared none leaves it out.
67
+ */
68
+ savedCard(brand: string, last4: string, expiry?: string): string;
69
+ }
70
+
71
+ /** What this step says when it cannot take a payment at all. */
72
+ export interface LegacyRefusalCopy {
73
+ /** No provider is configured — nothing the buyer can do. */
74
+ paymentsOff: string;
75
+ /** The host wired no `tokenizeCard`, so a card cannot be minted here. */
76
+ cardUnavailable: string;
77
+ /** The interstitial before a hosted provider's own page. */
78
+ redirectNotice: string;
79
+ }
80
+
81
+ /** Everything above, in one object a host passes at the mount. */
82
+ export interface CheckoutPaymentCopy {
83
+ money: LegacyMoneyCopy;
84
+ method: LegacyMethodCopy;
85
+ pix: LegacyPixCopy;
86
+ card: LegacyCardCopy;
87
+ refusal: LegacyRefusalCopy;
88
+ }
@@ -0,0 +1,50 @@
1
+ import type { CheckoutPaymentCopy } from './checkout-payment-copy';
2
+
3
+ /**
4
+ * The pt-BR pack for the legacy `CheckoutPayment` step — a NAMED constant a
5
+ * host passes by hand, never a default.
6
+ *
7
+ * The filename is what exempts this file from the copy-portability gate:
8
+ * Portuguese may ship, it may not be silent. Every sentence is VERBATIM what
9
+ * the step used to render, so a host adopting it sees no change — what changes
10
+ * is that the words are chosen in a diff.
11
+ */
12
+ export const PT_BR_CHECKOUT_PAYMENT_COPY: CheckoutPaymentCopy = {
13
+ money: {
14
+ totalLabel: (formattedAmount) => `Total: ${formattedAmount}`,
15
+ payAction: (formattedAmount) => `Pagar ${formattedAmount}`,
16
+ amountLocale: 'pt-BR',
17
+ },
18
+ method: {
19
+ groupLabel: 'Forma de pagamento',
20
+ pixTitle: 'PIX',
21
+ pixSubtitle: 'Aprovação imediata',
22
+ cardTitle: 'Cartão',
23
+ cardSubtitle: 'Crédito à vista',
24
+ generatePixAction: 'Gerar QR Code PIX',
25
+ continueToPaymentAction: 'Continuar para o pagamento',
26
+ },
27
+ pix: {
28
+ qrAlt: 'QR Code PIX',
29
+ copyPasteLabel: 'PIX copia e cola',
30
+ copyAction: 'Copiar código',
31
+ copiedAction: 'Copiado!',
32
+ awaiting: 'Aguardando pagamento…',
33
+ },
34
+ card: {
35
+ heading: 'Pague com cartão',
36
+ numberLabel: 'Número do cartão',
37
+ holderLabel: 'Nome impresso no cartão',
38
+ expiryLabel: 'Validade (MM/AA)',
39
+ cvvLabel: 'CVV',
40
+ payAction: 'Pagar com cartão',
41
+ newCard: 'Novo cartão — inserir outro cartão',
42
+ savedCard: (brand, last4, expiry) =>
43
+ `${brand} •••• ${last4}${expiry ? ` — Validade ${expiry}` : ''}`,
44
+ },
45
+ refusal: {
46
+ paymentsOff: 'Esta loja ainda não aceita pagamentos online.',
47
+ cardUnavailable: 'Pagamento com cartão indisponível.',
48
+ redirectNotice: 'Você será direcionado para concluir o pagamento com segurança.',
49
+ },
50
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The two shapes the legacy `CheckoutPayment` step and its views both name.
3
+ *
4
+ * Their own module so the views can import them without importing the
5
+ * container that renders the views — the circular import that would otherwise
6
+ * follow from splitting the file (FUT-760). Both stay exported from the
7
+ * package root, where they have always been.
8
+ */
9
+
10
+ /** What the card form collects, before anything is tokenized. */
11
+ export interface CardFormValues {
12
+ number: string;
13
+ holder: string;
14
+ expiry: string;
15
+ cvv: string;
16
+ }
17
+
18
+ /** A provider-vaulted card the buyer may reuse ("Mastercard •••• 7599"). */
19
+ export interface SavedCardOption {
20
+ savedCardToken: string;
21
+ brand: string;
22
+ last4: string;
23
+ /** e.g. "02/2034" — shown beside the brand, worded by the host. */
24
+ expiry?: string;
25
+ }
@@ -0,0 +1,225 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The legacy `CheckoutPayment` step's PRESENTATION — the PIX panel, the card
5
+ * form, the saved-card list and the two method cards.
6
+ *
7
+ * Split from `CheckoutPayment.tsx` when the copy port (FUT-760) pushed that
8
+ * file past the size gate, along the seam it already had: everything here
9
+ * takes props and renders, and nothing here holds a client, a charge or an
10
+ * effect. The container keeps those.
11
+ */
12
+
13
+ import {
14
+ Box,
15
+ Button,
16
+ CircularProgress,
17
+ FormControlLabel,
18
+ Radio,
19
+ RadioGroup,
20
+ Stack,
21
+ TextField,
22
+ Typography,
23
+ } from '@mui/material';
24
+ import { useState } from 'react';
25
+
26
+ import type { ClientChargeView, Money } from '@12-apps/payments-backend';
27
+
28
+ import type { ClientPaymentsConfig } from '../client';
29
+
30
+ import type {
31
+ CheckoutPaymentCopy,
32
+ LegacyCardCopy,
33
+ LegacyMethodCopy,
34
+ LegacyMoneyCopy,
35
+ LegacyPixCopy,
36
+ } from './checkout-payment-copy';
37
+ import type { CardFormValues, SavedCardOption } from './checkout-payment-types';
38
+
39
+ /**
40
+ * The amount, in the host's locale and the store's currency.
41
+ *
42
+ * The locale is the host's say (FUT-760) — it decides the decimal mark, the
43
+ * grouping and where the symbol sits. The currency is not: it rides on the
44
+ * `Money` the host already passes, and is a fact about the charge.
45
+ */
46
+ export function formatAmount(amount: Money, copy: LegacyMoneyCopy): string {
47
+ return (amount.amountCents / 100).toLocaleString(copy.amountLocale, {
48
+ style: 'currency',
49
+ currency: amount.currency,
50
+ });
51
+ }
52
+
53
+ export function PixPanel({ charge, copy }: { charge: ClientChargeView; copy: LegacyPixCopy }) {
54
+ const [copied, setCopied] = useState(false);
55
+ if (!charge.pix) return null;
56
+ return (
57
+ <Stack spacing={1} alignItems="flex-start">
58
+ {charge.pix.qrImageUrl ? (
59
+ <Box component="img" src={charge.pix.qrImageUrl} alt={copy.qrAlt} sx={{ width: 220 }} />
60
+ ) : null}
61
+ <TextField fullWidth multiline size="small" label={copy.copyPasteLabel} value={charge.pix.qrText} />
62
+ <Button
63
+ size="small"
64
+ onClick={() => {
65
+ void navigator.clipboard.writeText(charge.pix?.qrText ?? '').then(() => setCopied(true));
66
+ }}
67
+ >
68
+ {copied ? copy.copiedAction : copy.copyAction}
69
+ </Button>
70
+ <Stack direction="row" spacing={1} alignItems="center">
71
+ <CircularProgress size={16} />
72
+ <Typography variant="body2">{copy.awaiting}</Typography>
73
+ </Stack>
74
+ </Stack>
75
+ );
76
+ }
77
+
78
+ function CardPanel({
79
+ disabled,
80
+ onSubmit,
81
+ copy,
82
+ }: {
83
+ disabled: boolean;
84
+ onSubmit: (values: CardFormValues) => void;
85
+ copy: LegacyCardCopy;
86
+ }) {
87
+ const [values, setValues] = useState<CardFormValues>({ number: '', holder: '', expiry: '', cvv: '' });
88
+ const field = (key: keyof CardFormValues, label: string, width?: number) => (
89
+ <TextField
90
+ size="small"
91
+ label={label}
92
+ value={values[key]}
93
+ sx={width ? { width } : undefined}
94
+ onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
95
+ />
96
+ );
97
+ return (
98
+ <Stack spacing={2}>
99
+ {field('number', copy.numberLabel)}
100
+ {field('holder', copy.holderLabel)}
101
+ <Stack direction="row" spacing={2}>
102
+ {field('expiry', copy.expiryLabel, 140)}
103
+ {field('cvv', copy.cvvLabel, 100)}
104
+ </Stack>
105
+ <Button variant="contained" disabled={disabled} onClick={() => onSubmit(values)}>
106
+ {copy.payAction}
107
+ </Button>
108
+ </Stack>
109
+ );
110
+ }
111
+
112
+ interface CardSectionProps {
113
+ amount: Money;
114
+ savedCards: SavedCardOption[];
115
+ disabled: boolean;
116
+ onNewCard: (values: CardFormValues) => void;
117
+ onSavedCard: (card: SavedCardOption) => void;
118
+ copy: CheckoutPaymentCopy;
119
+ }
120
+
121
+ /** "Pague com cartão": saved cards as radios + "Novo cartão" fallback. */
122
+ function CardSection({
123
+ amount,
124
+ savedCards,
125
+ disabled,
126
+ onNewCard,
127
+ onSavedCard,
128
+ copy,
129
+ }: CardSectionProps) {
130
+ const [selected, setSelected] = useState(savedCards[0]?.savedCardToken ?? 'new');
131
+ const chosen = savedCards.find((c) => c.savedCardToken === selected);
132
+ if (savedCards.length === 0) {
133
+ return <CardPanel disabled={disabled} onSubmit={onNewCard} copy={copy.card} />;
134
+ }
135
+ return (
136
+ <Stack spacing={2}>
137
+ <Typography variant="subtitle2">{copy.card.heading}</Typography>
138
+ <RadioGroup value={selected} onChange={(_, v) => setSelected(v)}>
139
+ {savedCards.map((card) => (
140
+ <FormControlLabel
141
+ key={card.savedCardToken}
142
+ value={card.savedCardToken}
143
+ control={<Radio />}
144
+ label={copy.card.savedCard(card.brand, card.last4, card.expiry)}
145
+ />
146
+ ))}
147
+ <FormControlLabel value="new" control={<Radio />} label={copy.card.newCard} />
148
+ </RadioGroup>
149
+ {chosen ? (
150
+ <Button variant="contained" disabled={disabled} onClick={() => onSavedCard(chosen)}>
151
+ {copy.money.payAction(formatAmount(amount, copy.money))}
152
+ </Button>
153
+ ) : (
154
+ <CardPanel disabled={disabled} onSubmit={onNewCard} copy={copy.card} />
155
+ )}
156
+ </Stack>
157
+ );
158
+ }
159
+
160
+ interface MethodChooserProps {
161
+ config: ClientPaymentsConfig;
162
+ amount: Money;
163
+ savedCards: SavedCardOption[];
164
+ loading: boolean;
165
+ startPix: () => void;
166
+ startCard: (values: CardFormValues) => void;
167
+ startSavedCard: (card: SavedCardOption) => void;
168
+ copy: CheckoutPaymentCopy;
169
+ }
170
+
171
+ /** One "Forma de pagamento" option card (PIX / Cartão). */
172
+ function MethodCard(props: { title: string; subtitle: string; selected: boolean; onClick: () => void }) {
173
+ return (
174
+ <Button
175
+ variant="outlined"
176
+ onClick={props.onClick}
177
+ sx={{ flex: 1, justifyContent: 'flex-start', textAlign: 'left', borderWidth: props.selected ? 2 : 1 }}
178
+ >
179
+ <Stack alignItems="flex-start">
180
+ <Typography variant="subtitle2">{props.title}</Typography>
181
+ <Typography variant="caption" color="text.secondary">
182
+ {props.subtitle}
183
+ </Typography>
184
+ </Stack>
185
+ </Button>
186
+ );
187
+ }
188
+
189
+ /** Pre-charge phase: "Forma de pagamento" method cards, then the flow. */
190
+ export function MethodChooser(props: MethodChooserProps) {
191
+ const { config, amount, savedCards, loading, startPix, startCard, startSavedCard, copy } = props;
192
+ const words: LegacyMethodCopy = copy.method;
193
+ const [method, setMethod] = useState<'PIX' | 'CARD'>('PIX');
194
+ if (config.tokenization === 'REDIRECT') {
195
+ return (
196
+ <Button variant="contained" disabled={loading} onClick={startPix}>
197
+ {words.continueToPaymentAction}
198
+ </Button>
199
+ );
200
+ }
201
+ return (
202
+ <>
203
+ <Typography variant="subtitle2">{words.groupLabel}</Typography>
204
+ <Stack direction="row" spacing={2}>
205
+ <MethodCard title={words.pixTitle} subtitle={words.pixSubtitle} selected={method === 'PIX'} onClick={() => setMethod('PIX')} />
206
+ <MethodCard title={words.cardTitle} subtitle={words.cardSubtitle} selected={method === 'CARD'} onClick={() => setMethod('CARD')} />
207
+ </Stack>
208
+ {method === 'PIX' ? (
209
+ <Button variant="contained" disabled={loading} onClick={startPix}>
210
+ {words.generatePixAction}
211
+ </Button>
212
+ ) : (
213
+ <CardSection
214
+ amount={amount}
215
+ savedCards={savedCards}
216
+ disabled={loading}
217
+ onNewCard={startCard}
218
+ onSavedCard={startSavedCard}
219
+ copy={copy}
220
+ />
221
+ )}
222
+ </>
223
+ );
224
+ }
225
+
@@ -1,4 +1,5 @@
1
1
  import type { MaskedProviderConfig } from '@12-apps/payments-backend';
2
+ import type { ConnectionBadgeCopy } from './settings-copy';
2
3
 
3
4
  /**
4
5
  * Is there actually a connection here?
@@ -77,21 +78,32 @@ export function expiryProximity(
77
78
  return at - now.getTime() <= EXPIRY_WARNING_WINDOW_MS ? 'NEAR' : 'SAFE';
78
79
  }
79
80
 
80
- /** What a provider card reports at a glance. */
81
+ /**
82
+ * What a provider card reports at a glance.
83
+ *
84
+ * The union used to spell its four labels in Portuguese — an exported TYPE
85
+ * carrying one product's words, which is the structural half of the same leak
86
+ * `CardBrand` had (FUT-760). It names the STATE now; the word is the host's.
87
+ */
81
88
  type ConnectionBadge =
82
- | { label: 'Ativo'; color: 'success' }
83
- | { label: 'Conectado'; color: 'default' }
84
- | { label: 'Reconectar'; color: 'error' }
85
- | { label: 'Não conectado'; color: 'warning' };
89
+ | { label: string; state: 'ACTIVE'; color: 'success' }
90
+ | { label: string; state: 'CONNECTED'; color: 'default' }
91
+ | { label: string; state: 'RECONNECT'; color: 'error' }
92
+ | { label: string; state: 'NOT_CONNECTED'; color: 'warning' };
86
93
 
87
94
  /**
88
95
  * `enabled` is deliberately checked first: it is the only state that means
89
96
  * money can move, and a store reading `Ativo` has already passed everything
90
97
  * below it.
91
98
  */
92
- export function connectionBadge(config: MaskedProviderConfig | null): ConnectionBadge {
93
- if (config?.enabled) return { label: 'Ativo', color: 'success' };
94
- if (config?.status === 'RECONNECT_REQUIRED') return { label: 'Reconectar', color: 'error' };
95
- if (isConnected(config)) return { label: 'Conectado', color: 'default' };
96
- return { label: 'Não conectado', color: 'warning' };
99
+ export function connectionBadge(
100
+ copy: ConnectionBadgeCopy,
101
+ config: MaskedProviderConfig | null,
102
+ ): ConnectionBadge {
103
+ if (config?.enabled) return { label: copy.active, state: 'ACTIVE', color: 'success' };
104
+ if (config?.status === 'RECONNECT_REQUIRED') {
105
+ return { label: copy.reconnect, state: 'RECONNECT', color: 'error' };
106
+ }
107
+ if (isConnected(config)) return { label: copy.connected, state: 'CONNECTED', color: 'default' };
108
+ return { label: copy.notConnected, state: 'NOT_CONNECTED', color: 'warning' };
97
109
  }
@@ -6,6 +6,7 @@ import type {
6
6
  } from '@12-apps/payments-backend';
7
7
 
8
8
  import type { PendingSave } from './ConfirmCredentialSave';
9
+ import type { CredentialFormCopy } from './settings-copy';
9
10
 
10
11
  /**
11
12
  * The DECISIONS the credential form makes, with no JSX anywhere near them.
@@ -91,11 +92,15 @@ export function fieldsWellFormed(
91
92
  * is the more useful of the two truths. Naming one of Stripe's four would be a
92
93
  * lie about what the button writes, which is why only the fallback changes.
93
94
  */
94
- export function saveLabel(descriptor: ProviderDescriptor, complete: boolean): string {
95
+ export function saveLabel(
96
+ copy: CredentialFormCopy,
97
+ descriptor: ProviderDescriptor,
98
+ complete: boolean,
99
+ ): string {
95
100
  const required = descriptor.credentialSchema.filter((field) => field.required);
96
101
  const only = required.length === 1 ? required[0] : undefined;
97
- if (!only) return complete ? 'Salvar e testar conexão' : 'Salvar';
98
- return `Salvar ${only.label.replace(/\s*\([^)]*\)\s*$/, '')}`;
102
+ if (!only) return complete ? copy.saveAndTest : copy.save;
103
+ return copy.saveOnly(only.label.replace(/\s*\([^)]*\)\s*$/, ''));
99
104
  }
100
105
 
101
106
  /**
@@ -24,6 +24,10 @@ import { CARD_SX, ConnectEnvironmentCard } from './ConnectEnvironmentCard';
24
24
  * (`consultConnectApplications` in `@12-apps/payments-backend`) and passes it
25
25
  * here, so the host page is a thin mount — page chrome, auth and loading
26
26
  * belong to the host; the screen itself lives in this package.
27
+ *
28
+ * English, like the rest of this platform surface (FUT-760): the reader is the
29
+ * deployment's own integrator, reading redirect URIs and environment variable
30
+ * names, and everything a developer reads in this repo is English.
27
31
  */
28
32
  export interface ConnectApplicationPanelProps {
29
33
  /** The consult report, as the backend's `consultConnectApplications` answers. */
@@ -44,7 +48,7 @@ export function ConnectApplicationPanel(props: ConnectApplicationPanelProps): Re
44
48
  <Stack spacing={2} data-testid="connect-application-panel">
45
49
  <Stack spacing={0.5} data-testid="connect-expected-redirect" sx={CARD_SX}>
46
50
  <Typography variant="caption" color="text.secondary" fontWeight={600}>
47
- Callback desta instalação (o valor que precisa estar registrado)
51
+ Callback this deployment uses (the value that must be registered)
48
52
  </Typography>
49
53
  <Box
50
54
  component="code"
@@ -68,7 +72,7 @@ export function ConnectApplicationPanel(props: ConnectApplicationPanelProps): Re
68
72
  onClick={() => onRefresh()}
69
73
  data-testid="connect-refresh"
70
74
  >
71
- Consultar novamente
75
+ Consult again
72
76
  </Button>
73
77
  </Box>
74
78
  ) : null}
@@ -7,15 +7,18 @@ import type { ConnectApplicationStatus, PaymentEnvironment } from '@12-apps/paym
7
7
 
8
8
  /**
9
9
  * One environment's Connect application (FUT-479, packaged by FUT-573).
10
- * Sandbox and produção are SEPARATE applications with separate id/secret
10
+ * Sandbox and production are SEPARATE applications with separate id/secret
11
11
  * pairs, so each card stands on its own: configured or not, what PagBank says
12
12
  * is registered, and whether the registered redirect URI matches the
13
13
  * deployment's callback.
14
+ *
15
+ * English, like the rest of this platform surface (FUT-760) — see
16
+ * `ConnectApplicationPanel` for why.
14
17
  */
15
18
 
16
19
  const ENV_LABEL: Record<PaymentEnvironment, string> = {
17
20
  SANDBOX: 'Sandbox',
18
- PRODUCTION: 'Produção',
21
+ PRODUCTION: 'Production',
19
22
  };
20
23
 
21
24
  /** The bordered-card look every block of these screens shares. */
@@ -47,22 +50,23 @@ function MismatchAlert({ status }: { status: ConnectApplicationStatus }): ReactN
47
50
  if (status.redirectUriMismatch === true) {
48
51
  return (
49
52
  <Alert severity="error" data-testid={`connect-mismatch-${status.environment}`}>
50
- A redirect_uri registrada no PagBank é diferente do callback desta instalação. O fluxo
51
- de autorização OAuth falha silenciosamente até o cadastro ser corrigido no PagBank.
53
+ The redirect_uri registered with PagBank differs from the callback this deployment
54
+ uses. The OAuth authorization flow fails silently until the registration is corrected
55
+ at PagBank.
52
56
  </Alert>
53
57
  );
54
58
  }
55
59
  if (status.redirectUriMismatch === false) {
56
60
  return (
57
61
  <Alert severity="success" data-testid={`connect-match-${status.environment}`}>
58
- A redirect_uri registrada confere com o callback desta instalação.
62
+ The registered redirect_uri matches the callback this deployment uses.
59
63
  </Alert>
60
64
  );
61
65
  }
62
66
  return (
63
67
  <Alert severity="warning" data-testid={`connect-unknown-${status.environment}`}>
64
- A resposta do PagBank não informou a redirect_uri não foi possível comparar com o
65
- callback desta instalação.
68
+ The PagBank response carried no redirect_uri, so it could not be compared with the
69
+ callback this deployment uses.
66
70
  </Alert>
67
71
  );
68
72
  }
@@ -75,16 +79,16 @@ function ApplicationFields({ status }: { status: ConnectApplicationStatus }): Re
75
79
  return (
76
80
  <Stack spacing={1.5}>
77
81
  <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2.5 }}>
78
- <Field label="Nome (exibido ao lojista)">{app.name ?? '—'}</Field>
82
+ <Field label="Name (shown to the merchant)">{app.name ?? '—'}</Field>
79
83
  <Field label="Site">{app.site ?? '—'}</Field>
80
- <Field label="Descrição">{app.description ?? '—'}</Field>
84
+ <Field label="Description">{app.description ?? '—'}</Field>
81
85
  <Field label="Logo">{app.logo ?? '—'}</Field>
82
- <Field label="redirect_uri registrada">{app.redirectUri ?? 'não informada'}</Field>
86
+ <Field label="Registered redirect_uri">{app.redirectUri ?? 'not reported'}</Field>
83
87
  </Box>
84
88
  {extraKeys.length > 0 ? (
85
89
  <Box data-testid={`connect-extra-${status.environment}`}>
86
90
  <Typography variant="caption" color="text.secondary" fontWeight={600}>
87
- Outros campos retornados (schema não documentado)
91
+ Other fields returned (undocumented schema)
88
92
  </Typography>
89
93
  <Box component="pre" sx={{ m: 0, fontSize: 12, overflowX: 'auto' }}>
90
94
  {JSON.stringify(app.extra, null, 2)}
@@ -117,13 +121,13 @@ function ConfigHelp({
117
121
  onClick={() => setOpen((value) => !value)}
118
122
  data-testid={`connect-config-toggle-${environment}`}
119
123
  >
120
- {open ? 'Ocultar variáveis de ambiente' : 'Ver variáveis de ambiente'}
124
+ {open ? 'Hide environment variables' : 'Show environment variables'}
121
125
  </Button>
122
126
  {open ? (
123
127
  <Box data-testid={`connect-config-details-${environment}`}>
124
128
  <Typography variant="caption" color="text.secondary" component="p">
125
- A aplicação deste ambiente é resolvida estritamente por estas variáveis (sem
126
- fallback entre ambientes):
129
+ This environment's application is resolved strictly from these variables, with no
130
+ fallback between environments:
127
131
  </Typography>
128
132
  <Box component="ul" sx={{ m: 0, pl: 2.5 }}>
129
133
  {configVars.map((name) => (
@@ -159,7 +163,7 @@ export function ConnectEnvironmentCard({
159
163
  </Box>
160
164
  {!status.configured ? (
161
165
  <Typography variant="body2" color="text.secondary">
162
- Nenhuma aplicação configurada neste ambiente.
166
+ No application configured in this environment.
163
167
  </Typography>
164
168
  ) : null}
165
169
  {status.error !== null ? (
@@ -11,6 +11,16 @@ import { CARD_SX } from './ConnectEnvironmentCard';
11
11
  * The paste-ready homologação answers (FUT-483, packaged by FUT-573) — the
12
12
  * Pipefy form with every deployment-specific value already computed, and the
13
13
  * services list naming BOTH Order and Connect.
14
+ *
15
+ * ## Why this screen is English and its field labels are not (FUT-760)
16
+ *
17
+ * The reader is the PLATFORM's own integrator — the person registering a
18
+ * Connect application and chasing a `403 ACCESS_DENIED` — so the prose here
19
+ * follows the same rule as every other developer-facing sentence in this
20
+ * repo. The field names beside each value are quotes of PagBank's Pipefy
21
+ * form, and they arrive on `guide.fieldLabels` rather than being written
22
+ * here: this card is USED by matching each block to the box it goes in, and a
23
+ * translated box name has nothing to match.
14
24
  */
15
25
 
16
26
  /** One paste-ready value, in a copyable code block. */
@@ -42,42 +52,36 @@ export function HomologacaoGuideCard({ guide }: { guide: HomologacaoGuide }): Re
42
52
  return (
43
53
  <Stack spacing={1.5} data-testid="homologacao-guide-card" sx={CARD_SX}>
44
54
  <Typography variant="body2" fontWeight={600}>
45
- Formulário de homologação respostas prontas
55
+ Homologation formanswers ready to paste
46
56
  </Typography>
47
57
  <Typography variant="body2" color="text.secondary" component="p">
48
- Abra o{' '}
58
+ Open the{' '}
49
59
  <Link
50
60
  href={guide.formUrl}
51
61
  target="_blank"
52
62
  rel="noreferrer"
53
63
  data-testid="homologacao-form-link"
54
64
  >
55
- formulário oficial de homologação
65
+ official homologation form
56
66
  </Link>{' '}
57
- e preencha com os valores abaixo. Em paralelo, abra um chamado no{' '}
67
+ and fill it in with the values below. In parallel, open a ticket with{' '}
58
68
  <Link href={guide.supportFormUrl} target="_blank" rel="noreferrer">
59
- SIP — Suporte Integração PagBank
69
+ SIP — PagBank integration support
60
70
  </Link>{' '}
61
- citando o 403 ACCESS_DENIED o que responder primeiro resolve a dúvida de o
62
- formulário cobrir ou não o Connect. Documentação:{' '}
71
+ quoting the 403 ACCESS_DENIED: whichever answers first settles whether the form
72
+ covers Connect. Documentation:{' '}
63
73
  <Link href={guide.docsUrl} target="_blank" rel="noreferrer">
64
- solicitar homologação
74
+ requesting homologation
65
75
  </Link>
66
76
  .
67
77
  </Typography>
68
- <Answer label="Selecione o tipo de integração">{guide.integrationType}</Answer>
78
+ <Answer label={guide.fieldLabels.integrationType}>{guide.integrationType}</Answer>
69
79
  <Box data-testid="homologacao-services">
70
- <Answer label="Selecione qual serviço você integrou (marque OS DOIS)">
71
- {guide.services.join('\n')}
72
- </Answer>
80
+ <Answer label={guide.fieldLabels.services}>{guide.services.join('\n')}</Answer>
73
81
  </Box>
74
- <Answer label="Instruções de acesso ao seu ambiente (limite de 255 caracteres)">
75
- {guide.accessInstructions}
76
- </Answer>
77
- <Answer label="URL do site">{guide.siteUrl}</Answer>
78
- <Answer label="Detalhe quais produtos/serviços serão comercializados">
79
- {guide.productsDescription}
80
- </Answer>
82
+ <Answer label={guide.fieldLabels.accessInstructions}>{guide.accessInstructions}</Answer>
83
+ <Answer label={guide.fieldLabels.siteUrl}>{guide.siteUrl}</Answer>
84
+ <Answer label={guide.fieldLabels.productsDescription}>{guide.productsDescription}</Answer>
81
85
  <Typography variant="caption" color="text.secondary" component="p">
82
86
  {guide.slaText}
83
87
  </Typography>