@12-apps/payments-frontend 3.12.0 → 3.14.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.
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.12.0",
3
+ "version": "3.14.0",
4
4
  "type": "module",
5
5
  "description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
6
6
  "exports": {
7
- ".": "./src/index.ts"
7
+ ".": "./src/index.ts",
8
+ "./locales": "./src/locales.ts"
8
9
  },
9
10
  "scripts": {
10
11
  "clean": "rm -rf node_modules coverage storybook-static",
@@ -17,7 +18,7 @@
17
18
  "storybook:build": "storybook build"
18
19
  },
19
20
  "dependencies": {
20
- "@12-apps/payments-backend": "^4.18.0",
21
+ "@12-apps/payments-backend": "^4.19.0",
21
22
  "react-qr-code": "^2.2.0"
22
23
  },
23
24
  "peerDependencies": {
package/src/card/copy.ts CHANGED
@@ -41,6 +41,12 @@ export interface CardFieldCopy {
41
41
  * flagged it, and getting it wrong makes a buyer type the year first.
42
42
  */
43
43
  expiryLabel: string;
44
+ /**
45
+ * The same order, inside the box. It sat here as `"MM/AA"` while the LABEL
46
+ * beside it was already required config — so a host could translate the
47
+ * label and be contradicted by the field one line down.
48
+ */
49
+ expiryPlaceholder: string;
44
50
  cvvLabel: string;
45
51
  /**
46
52
  * The expiry field holds something that is not yet `MM/AA` at all — too few
@@ -0,0 +1,61 @@
1
+ import type { CardCopy } from './copy';
2
+
3
+ /**
4
+ * The en-US pack for the card form — a NAMED constant a host passes by hand,
5
+ * never a default.
6
+ *
7
+ * `MM/AA` becomes `MM/YY` and `CVV` stays `CVV`, and the difference between
8
+ * those two is the rule: the expiry mask is what the buyer TYPES, so it follows
9
+ * their own notation, while CVV is what is PRINTED on the card and is the same
10
+ * three letters in either language. `CPF` stays too — it is Brazil's taxpayer
11
+ * number, and there is nothing else to call the field a Brazilian buyer is
12
+ * filling in.
13
+ */
14
+ export const EN_US_CARD_COPY: CardCopy = {
15
+ fields: {
16
+ unknownBrand: 'Card',
17
+ numberLabel: 'Card number',
18
+ numberRequired: 'Enter the card number.',
19
+ numberIncomplete: 'That card number is incomplete.',
20
+ numberInvalid: 'That card number is not valid.',
21
+ holderLabel: 'Name printed on the card',
22
+ holderRequired: 'Enter the name printed on the card.',
23
+ expiryLabel: 'Expiry (MM/YY)',
24
+ expiryPlaceholder: 'MM/YY',
25
+ cvvLabel: 'CVV',
26
+ expiryIncomplete: 'The expiry is incomplete (MM/YY).',
27
+ monthInvalid: 'That month is not valid.',
28
+ expired: 'That card has expired.',
29
+ expiryInvalid: 'That expiry is not valid, or has passed.',
30
+ cvvRequired: 'Enter the CVV.',
31
+ cvvInvalid: 'That CVV is not valid.',
32
+ // The length differs by brand (3 for most, 4 for Amex), so it is
33
+ // interpolated rather than written out.
34
+ cvvDigits: (length) => `The CVV must be ${length} digits.`,
35
+ cpfRequired: 'CPF is required.',
36
+ cpfInvalid: 'That CPF is not valid.',
37
+ savedCardsLabel: 'Card',
38
+ savedCardExpiry: (month, year) => `Expires ${month}/${year}`,
39
+ newCard: 'New card',
40
+ newCardDescription: 'Enter another card',
41
+ saveCard: 'Save this card for next time',
42
+ },
43
+ tokenize: {
44
+ sdkUnavailable: 'Could not load the payment method. Reload the page.',
45
+ cardNotProcessed: 'Could not process the card. Check the details and try again.',
46
+ providerUnreachable: 'Could not reach the card provider. Check your connection.',
47
+ providerTimedOut: 'The card provider did not answer in time.',
48
+ // The status and the raw response ride along because this is what a store
49
+ // owner forwards to support; a tidier sentence would drop the only part
50
+ // that identifies the failure.
51
+ providerRefused: (status, response) =>
52
+ `The card provider refused the card details (HTTP ${status}). ` + `Response: ${response}`,
53
+ noPublicKey:
54
+ 'The card public key is not available for this store. ' +
55
+ 'Reconnect the provider and try again.',
56
+ // Ends with what the BUYER can do, in order of how likely each is to work:
57
+ // this is the one message here a shopper reads mid-purchase.
58
+ cardUnavailable:
59
+ 'Card payment is unavailable in this store right now. Reload the page and try again, choose another payment method, or arrange it with the store directly.',
60
+ },
61
+ };
@@ -115,7 +115,7 @@ function ExpiryCvvFields({
115
115
  label={copy.expiryLabel}
116
116
  type="text"
117
117
  inputMode="numeric"
118
- placeholder="MM/AA"
118
+ placeholder={copy.expiryPlaceholder}
119
119
  variant="outlined"
120
120
  size="md"
121
121
  fullWidth
package/src/card/index.ts CHANGED
@@ -42,3 +42,4 @@ export {
42
42
  export { NEW_CARD, type CardDetails, type CardFieldErrors, type CardToken, type SavedCard } from "./types";
43
43
  export type { CardCopy, CardFieldCopy, CardTokenizeCopy } from "./copy";
44
44
  export { PT_BR_CARD_COPY } from "./pt-BR";
45
+ export { EN_US_CARD_COPY } from "./en-US";
package/src/card/pt-BR.ts CHANGED
@@ -19,6 +19,7 @@ export const PT_BR_CARD_COPY: CardCopy = {
19
19
  holderLabel: 'Nome impresso no cartão',
20
20
  holderRequired: 'Informe o nome impresso no cartão.',
21
21
  expiryLabel: 'Validade (MM/AA)',
22
+ expiryPlaceholder: 'MM/AA',
22
23
  cvvLabel: 'CVV',
23
24
  expiryIncomplete: 'Validade incompleta (MM/AA).',
24
25
  monthInvalid: 'Mês inválido.',
@@ -159,9 +159,10 @@ function StepText({ text, link }: { text?: string; link?: SetupStep['link'] }) {
159
159
  * button gives them nothing to weigh that against.
160
160
  */
161
161
  function ConfirmBar({ action }: { action: { label: string; run: () => void } }) {
162
+ const copy = usePaymentsSettingsCopy().setupGuide;
162
163
  return (
163
164
  <Box sx={BAR_SX} data-testid="payments-setup-confirm-bar">
164
- <Typography sx={BAR_MSG_SX}>Confirme quando terminar do lado do provedor.</Typography>
165
+ <Typography sx={BAR_MSG_SX}>{copy.confirmPrompt}</Typography>
165
166
  <Button variant="contained" disableElevation sx={BTN_PRIMARY_SX} onClick={() => action.run()}>
166
167
  {action.label}
167
168
  </Button>
@@ -0,0 +1,95 @@
1
+ import { EN_US_CARD_COPY } from "../../card/en-US";
2
+ import { EN_US_CHECKOUT_SCREENS_COPY } from "./screens-en-US";
3
+ import type { CheckoutCopy } from "./copy-context";
4
+ import type { CheckoutViewCopy, PaymentStatusCopy } from "./view-copy";
5
+
6
+ /**
7
+ * The en-US packs for the checkout views — NAMED exports a host passes by hand,
8
+ * never defaults.
9
+ *
10
+ * Four of these sentences carry a promise rather than a description, and the
11
+ * translation keeps each one FIRST in its block, because the fear on this
12
+ * screen is having been charged for an order that failed:
13
+ *
14
+ * - `failed.support` and `expired.support` both open by saying nothing was
15
+ * charged;
16
+ * - `awaitingTimedOut.support` says "do not pay again" before anything else it
17
+ * has to say, because a second payment is the expensive mistake here;
18
+ * - `dados.secureNotice` is the one reassurance on the details step.
19
+ */
20
+ export const EN_US_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
21
+ paid: {
22
+ heading: "Order confirmed",
23
+ support: "We have your payment and the order is recorded.",
24
+ },
25
+ awaiting: {
26
+ heading: "Confirming your payment",
27
+ support: "This usually takes a few seconds. You can leave this screen open.",
28
+ },
29
+ failed: {
30
+ heading: "Payment not completed",
31
+ support: "Nothing was charged. You can try again.",
32
+ },
33
+ expired: {
34
+ heading: "The code expired",
35
+ support: "Nothing was charged. Generate a new code to carry on.",
36
+ },
37
+ awaitingTimedOut: {
38
+ heading: "We have not had the confirmation yet",
39
+ support:
40
+ "If you have already paid, the order is confirmed as soon as the provider tells us — " +
41
+ "do not pay again. You can close this screen.",
42
+ },
43
+ retryAction: "Try again",
44
+ regenerateAction: "Generate a new code",
45
+ backAction: "Back to the menu",
46
+ amountLabel: "Amount paid",
47
+ referenceLabel: "Order",
48
+ receiptEmailLabel: "Receipt sent to",
49
+ };
50
+
51
+ /**
52
+ * The words the checkout's deeper screens read from context — the card form,
53
+ * its tokenizers, and the buyer-details step's own fields.
54
+ */
55
+ export const EN_US_CHECKOUT_COPY: CheckoutCopy = {
56
+ card: EN_US_CARD_COPY,
57
+ screens: EN_US_CHECKOUT_SCREENS_COPY,
58
+ buyer: {
59
+ emailInvalid: "That e-mail address is not valid.",
60
+ emailRequired: "E-mail is required.",
61
+ nameRequired: "Name is required.",
62
+ phoneRequired: "Phone is required.",
63
+ // The list of REQUIRED field names is the host's configuration, so the
64
+ // sentence is built around it — and it inflects on how many there are,
65
+ // which is why this is a function rather than a template.
66
+ fieldsHint: (names) =>
67
+ names.length === 0
68
+ ? "Name, e-mail and phone are optional — they are only used for the receipt."
69
+ : `Enter your ${names.join(", ")} (${names.length === 1 ? "required" : "required"} ` +
70
+ "for payment). The other fields are optional — they are only used for the receipt.",
71
+ },
72
+ };
73
+
74
+ export const EN_US_CHECKOUT_VIEW_COPY: CheckoutViewCopy = {
75
+ screens: EN_US_CHECKOUT_COPY,
76
+ // The step KEYS are the package's own ids; only the labels are words.
77
+ steps: {
78
+ dados: "Details",
79
+ payment: "Payment",
80
+ status: "Confirmation",
81
+ },
82
+ dados: {
83
+ saveProfile: "Save my details for next time",
84
+ cannotContinueTitle: "Could not continue",
85
+ continueAction: "Continue",
86
+ secureNotice: "Secure payment",
87
+ keepShopping: "Keep shopping",
88
+ back: "Back",
89
+ },
90
+ emptyCart: {
91
+ title: "Your cart is empty.",
92
+ action: "See the menu",
93
+ },
94
+ status: EN_US_PAYMENT_STATUS_COPY,
95
+ };
@@ -0,0 +1,113 @@
1
+ import type { CheckoutScreensCopy } from './screens-copy';
2
+
3
+ /**
4
+ * The en-US pack for the buyer's checkout screens — a NAMED constant a host
5
+ * passes by hand, never a default.
6
+ *
7
+ * Two entries here are LOCALE TAGS rather than words, and they move with the
8
+ * pack because they decide how the same screen renders a time and a wallet
9
+ * button: `pix.expiryLocale` formats the "valid until" clock, and
10
+ * `wallet.googlePay.buttonLocale` is the language Google's own button renders
11
+ * itself in. A pack whose sentences were English and whose clock was
12
+ * Portuguese would be a screen written for nobody.
13
+ *
14
+ * PIX, CPF, Apple Pay and Google Pay all keep their names: the first two are
15
+ * Brazilian schemes the buyer will look for by name in their banking app, and
16
+ * the last two are the vendors' own product names, rendered by the vendors'
17
+ * own buttons.
18
+ */
19
+ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
20
+ method: {
21
+ groupLabel: 'Payment method',
22
+ pixLabel: 'PIX',
23
+ cardLabel: 'Card',
24
+ pixDescription: 'Approved instantly',
25
+ cardDescription: 'Credit, paid in full',
26
+ unavailableHere: 'Unavailable in this store',
27
+ },
28
+ settling: {
29
+ cannotConfirm: 'Could not confirm the payment',
30
+ takingLonger: 'The payment is taking longer than expected',
31
+ // "do not make another payment" is the load-bearing half: a second payment
32
+ // is the expensive mistake on this screen.
33
+ takingLongerHelp:
34
+ 'You can wait, or check your order again shortly — do not make another payment.',
35
+ processing: 'Processing payment…',
36
+ confirming: 'We are confirming your payment',
37
+ cannotPay: 'Could not pay',
38
+ },
39
+ pix: {
40
+ heading: 'Pay with PIX',
41
+ instructions: (totalLabel) =>
42
+ `Scan the QR code in your banking app, or copy the code. Total ${totalLabel}.`,
43
+ qrAlt: 'PIX QR code for payment',
44
+ copyAction: 'Copy',
45
+ copiedAction: 'Copied!',
46
+ validUntil: (time) => `Valid until ${time}. Confirmation is automatic.`,
47
+ expiryLocale: 'en-US',
48
+ awaiting: 'Waiting for payment…',
49
+ chargeMissing: 'Could not generate the PIX code.',
50
+ },
51
+ card: {
52
+ heading: 'Pay by card',
53
+ },
54
+ payer: {
55
+ taxId: (formatted) => `CPF ${formatted}`,
56
+ taxIdAlreadyKnown: 'CPF already on file',
57
+ payingAs: (name) => `Paying as ${name}`,
58
+ payingWithSavedDetails: 'Paying with your saved details',
59
+ changeAction: 'Change',
60
+ },
61
+ error: {
62
+ confirming: 'We are confirming your payment',
63
+ cannotContinue: 'Could not continue',
64
+ retryAction: 'Try again',
65
+ emailLabel: 'E-mail for the payment',
66
+ // Lower-case and fragmentary: it renders as a hint under the field.
67
+ emailMustDifferHint: "use an e-mail address different from the store's",
68
+ useEmailAction: 'Use this e-mail and continue',
69
+ },
70
+ wallet: {
71
+ applePay: {
72
+ orderTotal: 'Order total',
73
+ cannotStart: 'Could not start Apple Pay in this store. Pay by card instead.',
74
+ cannotComplete: 'Could not start Apple Pay. Try again, or pay by card.',
75
+ payAction: 'Pay with Apple Pay',
76
+ },
77
+ googlePay: {
78
+ cannotComplete: 'Could not complete the payment with Google Pay. Try again, or pay by card.',
79
+ // Google's button renders itself in this language; it takes a bare
80
+ // language subtag, not a full BCP-47 tag.
81
+ buttonLocale: 'en',
82
+ },
83
+ orPayWithCard: 'or pay by card',
84
+ },
85
+ hosted: {
86
+ // Five fragments the screen composes into one sentence, so each keeps its
87
+ // leading preposition and none reads as a sentence on its own.
88
+ destinationNamed: (displayName) => `to ${displayName}'s payment page`,
89
+ destinationGeneric: "to the provider's secure payment page",
90
+ methodsChoice: (methods) => `, where you choose to pay with ${methods}`,
91
+ pixAndCard: 'PIX or card',
92
+ pixOnly: 'PIX',
93
+ cardOnly: 'card',
94
+ handoff: (destination, choice) => `You will be taken ${destination}${choice}.`,
95
+ afterwards:
96
+ 'Once the payment goes through, you come back here and we confirm the order.',
97
+ startAction: 'Continue to payment',
98
+ preparing: 'Preparing the payment',
99
+ },
100
+ transport: {
101
+ failed: 'Could not complete the operation. Try again.',
102
+ invalidResponse: 'Invalid response from the server.',
103
+ offline: 'Could not connect. Check your connection and try again.',
104
+ },
105
+ validation: {
106
+ taxIdInvalid: 'That CPF is not valid.',
107
+ nameRequired: 'Enter your name.',
108
+ emailInvalid: 'That e-mail address is not valid.',
109
+ phoneInvalid: 'That phone number is not valid.',
110
+ required: 'This field is required.',
111
+ },
112
+ generatingPayment: 'Generating payment…',
113
+ };
@@ -0,0 +1,53 @@
1
+ import type { CheckoutPaymentCopy } from './checkout-payment-copy';
2
+
3
+ /**
4
+ * The en-US pack for the legacy `CheckoutPayment` step — a NAMED constant a
5
+ * host passes by hand, never a default.
6
+ *
7
+ * `money.amountLocale` is part of the pack and moves with it: it is what the
8
+ * step formats the total with, so the words around a price and the price
9
+ * itself are written for the same reader. It does NOT change the CURRENCY —
10
+ * an English-reading buyer of a Brazilian store still pays in BRL.
11
+ *
12
+ * PIX keeps its name. It is Brazil's instant-payment scheme, and the buyer will
13
+ * look for those three letters in their banking app.
14
+ */
15
+ export const EN_US_CHECKOUT_PAYMENT_COPY: CheckoutPaymentCopy = {
16
+ money: {
17
+ totalLabel: (formattedAmount) => `Total: ${formattedAmount}`,
18
+ payAction: (formattedAmount) => `Pay ${formattedAmount}`,
19
+ amountLocale: 'en-US',
20
+ },
21
+ method: {
22
+ groupLabel: 'Payment method',
23
+ pixTitle: 'PIX',
24
+ pixSubtitle: 'Approved instantly',
25
+ cardTitle: 'Card',
26
+ cardSubtitle: 'Credit, paid in full',
27
+ generatePixAction: 'Generate a PIX QR code',
28
+ continueToPaymentAction: 'Continue to payment',
29
+ },
30
+ pix: {
31
+ qrAlt: 'PIX QR code',
32
+ copyPasteLabel: 'PIX copy-and-paste code',
33
+ copyAction: 'Copy the code',
34
+ copiedAction: 'Copied!',
35
+ awaiting: 'Waiting for payment…',
36
+ },
37
+ card: {
38
+ heading: 'Pay by card',
39
+ numberLabel: 'Card number',
40
+ holderLabel: 'Name printed on the card',
41
+ expiryLabel: 'Expiry (MM/YY)',
42
+ cvvLabel: 'CVV',
43
+ payAction: 'Pay by card',
44
+ newCard: 'New card — enter another card',
45
+ savedCard: (brand, last4, expiry) =>
46
+ `${brand} •••• ${last4}${expiry ? ` — expires ${expiry}` : ''}`,
47
+ },
48
+ refusal: {
49
+ paymentsOff: 'This store does not accept online payments yet.',
50
+ cardUnavailable: 'Card payment is unavailable.',
51
+ redirectNotice: 'You will be taken somewhere secure to finish the payment.',
52
+ },
53
+ };
@@ -6,6 +6,8 @@ import type { ReactNode } from 'react';
6
6
  import type { ConnectApplicationReport, PaymentEnvironment } from '@12-apps/payments-backend';
7
7
 
8
8
  import { CARD_SX, ConnectEnvironmentCard } from './ConnectEnvironmentCard';
9
+ import type { PlatformHomologacaoCopy } from './copy';
10
+ import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
9
11
 
10
12
  /**
11
13
  * The platform's PagBank Connect application, per environment (FUT-479,
@@ -25,9 +27,11 @@ import { CARD_SX, ConnectEnvironmentCard } from './ConnectEnvironmentCard';
25
27
  * here, so the host page is a thin mount — page chrome, auth and loading
26
28
  * belong to the host; the screen itself lives in this package.
27
29
  *
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.
30
+ * Its words are the HOST's (FUT-760), passed as one required `copy` object.
31
+ * The reader is whoever operates the platform — and a second platform adopting
32
+ * this package has its own operator, who reads whatever it was handed. The
33
+ * literals this screen used to carry were English because they were written
34
+ * for whoever was reading the code, which is a different person.
31
35
  */
32
36
  export interface ConnectApplicationPanelProps {
33
37
  /** The consult report, as the backend's `consultConnectApplications` answers. */
@@ -40,15 +44,26 @@ export interface ConnectApplicationPanelProps {
40
44
  * when provided.
41
45
  */
42
46
  configVarsFor?: (environment: PaymentEnvironment) => string[];
47
+ /** Every word this panel and its environment cards render. REQUIRED. */
48
+ copy: PlatformHomologacaoCopy;
43
49
  }
44
50
 
45
51
  export function ConnectApplicationPanel(props: ConnectApplicationPanelProps): ReactNode {
52
+ return (
53
+ <PlatformCopyProvider copy={props.copy}>
54
+ <ConnectApplicationBody {...props} />
55
+ </PlatformCopyProvider>
56
+ );
57
+ }
58
+
59
+ function ConnectApplicationBody(props: ConnectApplicationPanelProps): ReactNode {
46
60
  const { report, onRefresh, configVarsFor } = props;
61
+ const copy = usePlatformCopy().connect;
47
62
  return (
48
63
  <Stack spacing={2} data-testid="connect-application-panel">
49
64
  <Stack spacing={0.5} data-testid="connect-expected-redirect" sx={CARD_SX}>
50
65
  <Typography variant="caption" color="text.secondary" fontWeight={600}>
51
- Callback this deployment uses (the value that must be registered)
66
+ {copy.expectedRedirectHeading}
52
67
  </Typography>
53
68
  <Box
54
69
  component="code"
@@ -72,7 +87,7 @@ export function ConnectApplicationPanel(props: ConnectApplicationPanelProps): Re
72
87
  onClick={() => onRefresh()}
73
88
  data-testid="connect-refresh"
74
89
  >
75
- Consult again
90
+ {copy.consultAgain}
76
91
  </Button>
77
92
  </Box>
78
93
  ) : null}
@@ -4,6 +4,7 @@ import { Alert, Box, Button, Stack, Typography } from '@mui/material';
4
4
  import { useState, type ReactNode } from 'react';
5
5
 
6
6
  import type { ConnectApplicationStatus, PaymentEnvironment } from '@12-apps/payments-backend';
7
+ import { usePlatformCopy } from './copy-context';
7
8
 
8
9
  /**
9
10
  * One environment's Connect application (FUT-479, packaged by FUT-573).
@@ -46,49 +47,50 @@ function Field({ label, children }: { label: string; children: ReactNode }): Rea
46
47
 
47
48
  /** The verdict the screen exists for: does the registered callback match ours? */
48
49
  function MismatchAlert({ status }: { status: ConnectApplicationStatus }): ReactNode {
50
+ const copy = usePlatformCopy().connect;
49
51
  if (status.application === null) return null;
50
52
  if (status.redirectUriMismatch === true) {
51
53
  return (
52
54
  <Alert severity="error" data-testid={`connect-mismatch-${status.environment}`}>
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.
55
+ {copy.redirectDiffers}
56
56
  </Alert>
57
57
  );
58
58
  }
59
59
  if (status.redirectUriMismatch === false) {
60
60
  return (
61
61
  <Alert severity="success" data-testid={`connect-match-${status.environment}`}>
62
- The registered redirect_uri matches the callback this deployment uses.
62
+ {copy.redirectMatches}
63
63
  </Alert>
64
64
  );
65
65
  }
66
66
  return (
67
67
  <Alert severity="warning" data-testid={`connect-unknown-${status.environment}`}>
68
- The PagBank response carried no redirect_uri, so it could not be compared with the
69
- callback this deployment uses.
68
+ {copy.redirectUnreported}
70
69
  </Alert>
71
70
  );
72
71
  }
73
72
 
74
73
  /** What PagBank reports as registered, plus whatever extra keys came back. */
75
74
  function ApplicationFields({ status }: { status: ConnectApplicationStatus }): ReactNode {
75
+ const copy = usePlatformCopy().connect;
76
76
  const app = status.application;
77
77
  if (app === null) return null;
78
78
  const extraKeys = Object.keys(app.extra);
79
79
  return (
80
80
  <Stack spacing={1.5}>
81
81
  <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2.5 }}>
82
- <Field label="Name (shown to the merchant)">{app.name ?? '—'}</Field>
83
- <Field label="Site">{app.site ?? '—'}</Field>
84
- <Field label="Description">{app.description ?? '—'}</Field>
85
- <Field label="Logo">{app.logo ?? '—'}</Field>
86
- <Field label="Registered redirect_uri">{app.redirectUri ?? 'not reported'}</Field>
82
+ <Field label={copy.fields.name}>{app.name ?? copy.fieldEmpty}</Field>
83
+ <Field label={copy.fields.site}>{app.site ?? copy.fieldEmpty}</Field>
84
+ <Field label={copy.fields.description}>{app.description ?? copy.fieldEmpty}</Field>
85
+ <Field label={copy.fields.logo}>{app.logo ?? copy.fieldEmpty}</Field>
86
+ <Field label={copy.fields.redirectUri}>
87
+ {app.redirectUri ?? copy.redirectNotReported}
88
+ </Field>
87
89
  </Box>
88
90
  {extraKeys.length > 0 ? (
89
91
  <Box data-testid={`connect-extra-${status.environment}`}>
90
92
  <Typography variant="caption" color="text.secondary" fontWeight={600}>
91
- Other fields returned (undocumented schema)
93
+ {copy.extraKeys}
92
94
  </Typography>
93
95
  <Box component="pre" sx={{ m: 0, fontSize: 12, overflowX: 'auto' }}>
94
96
  {JSON.stringify(app.extra, null, 2)}
@@ -111,6 +113,7 @@ function ConfigHelp({
111
113
  environment: PaymentEnvironment;
112
114
  configVars?: string[];
113
115
  }): ReactNode {
116
+ const copy = usePlatformCopy().connect;
114
117
  const [open, setOpen] = useState(false);
115
118
  if (!configVars || configVars.length === 0) return null;
116
119
  return (
@@ -121,13 +124,12 @@ function ConfigHelp({
121
124
  onClick={() => setOpen((value) => !value)}
122
125
  data-testid={`connect-config-toggle-${environment}`}
123
126
  >
124
- {open ? 'Hide environment variables' : 'Show environment variables'}
127
+ {open ? copy.hideConfig : copy.showConfig}
125
128
  </Button>
126
129
  {open ? (
127
130
  <Box data-testid={`connect-config-details-${environment}`}>
128
131
  <Typography variant="caption" color="text.secondary" component="p">
129
- This environment's application is resolved strictly from these variables, with no
130
- fallback between environments:
132
+ {copy.resolvedFrom}
131
133
  </Typography>
132
134
  <Box component="ul" sx={{ m: 0, pl: 2.5 }}>
133
135
  {configVars.map((name) => (
@@ -151,6 +153,7 @@ export function ConnectEnvironmentCard({
151
153
  status: ConnectApplicationStatus;
152
154
  configVars?: string[];
153
155
  }): ReactNode {
156
+ const copy = usePlatformCopy().connect;
154
157
  return (
155
158
  <Stack spacing={1.5} data-testid={`connect-env-${status.environment}`} sx={CARD_SX}>
156
159
  <Box sx={{ display: 'flex', alignItems: 'baseline', gap: 1.5, wordBreak: 'break-all' }}>
@@ -162,9 +165,7 @@ export function ConnectEnvironmentCard({
162
165
  </Typography>
163
166
  </Box>
164
167
  {!status.configured ? (
165
- <Typography variant="body2" color="text.secondary">
166
- No application configured in this environment.
167
- </Typography>
168
+ <Typography variant="body2" color="text.secondary">{copy.noApplication}</Typography>
168
169
  ) : null}
169
170
  {status.error !== null ? (
170
171
  <Alert severity="warning" data-testid={`connect-error-${status.environment}`}>
@@ -6,6 +6,7 @@ import type { ReactNode } from 'react';
6
6
  import type { HomologacaoGuide } from '@12-apps/payments-backend';
7
7
 
8
8
  import { CARD_SX } from './ConnectEnvironmentCard';
9
+ import { usePlatformCopy } from './copy-context';
9
10
 
10
11
  /**
11
12
  * The paste-ready homologação answers (FUT-483, packaged by FUT-573) — the
@@ -49,31 +50,31 @@ function Answer({ label, children }: { label: string; children: ReactNode }): Re
49
50
  }
50
51
 
51
52
  export function HomologacaoGuideCard({ guide }: { guide: HomologacaoGuide }): ReactNode {
53
+ const copy = usePlatformCopy().guide;
52
54
  return (
53
55
  <Stack spacing={1.5} data-testid="homologacao-guide-card" sx={CARD_SX}>
54
56
  <Typography variant="body2" fontWeight={600}>
55
- Homologation form — answers ready to paste
57
+ {copy.heading}
56
58
  </Typography>
57
59
  <Typography variant="body2" color="text.secondary" component="p">
58
- Open the{' '}
60
+ {copy.ledeBeforeForm}
59
61
  <Link
60
62
  href={guide.formUrl}
61
63
  target="_blank"
62
64
  rel="noreferrer"
63
65
  data-testid="homologacao-form-link"
64
66
  >
65
- official homologation form
66
- </Link>{' '}
67
- and fill it in with the values below. In parallel, open a ticket with{' '}
67
+ {copy.formLink}
68
+ </Link>
69
+ {copy.ledeBeforeSupport}
68
70
  <Link href={guide.supportFormUrl} target="_blank" rel="noreferrer">
69
- SIP — PagBank integration support
70
- </Link>{' '}
71
- quoting the 403 ACCESS_DENIED: whichever answers first settles whether the form
72
- covers Connect. Documentation:{' '}
71
+ {copy.supportLink}
72
+ </Link>
73
+ {copy.ledeBeforeDocs}
73
74
  <Link href={guide.docsUrl} target="_blank" rel="noreferrer">
74
- requesting homologation
75
+ {copy.docsLink}
75
76
  </Link>
76
- .
77
+ {copy.ledeAfterDocs}
77
78
  </Typography>
78
79
  <Answer label={guide.fieldLabels.integrationType}>{guide.integrationType}</Answer>
79
80
  <Box data-testid="homologacao-services">