@12-apps/payments-frontend 3.12.0 → 3.13.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.12.0",
3
+ "version": "3.13.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": {
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
@@ -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/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>
@@ -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">
@@ -6,6 +6,7 @@ import { useState, type ReactNode } from 'react';
6
6
  import type { PlatformHomologationStatus } 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 homologação outcome record (FUT-483, packaged by FUT-573) — the durable
@@ -44,12 +45,6 @@ export interface HomologacaoSaveState {
44
45
  success: boolean;
45
46
  }
46
47
 
47
- const STATUS_LABEL: Record<PlatformHomologationStatus, string> = {
48
- SUBMITTED: 'Submitted',
49
- APPROVED: 'Approved',
50
- REJECTED: 'Rejected',
51
- };
52
-
53
48
  const STATUS_COLOR: Record<PlatformHomologationStatus, 'warning' | 'success' | 'error'> = {
54
49
  SUBMITTED: 'warning',
55
50
  APPROVED: 'success',
@@ -57,10 +52,11 @@ const STATUS_COLOR: Record<PlatformHomologationStatus, 'warning' | 'success' | '
57
52
  };
58
53
 
59
54
  function StatusChip({ record }: { record: PlatformHomologationRecordView | null }): ReactNode {
55
+ const copy = usePlatformCopy().outcome;
60
56
  if (record === null) {
61
57
  return (
62
58
  <Chip
63
- label="Not submitted"
59
+ label={copy.notSubmitted}
64
60
  size="small"
65
61
  variant="outlined"
66
62
  data-testid="homologacao-status-chip"
@@ -69,7 +65,7 @@ function StatusChip({ record }: { record: PlatformHomologationRecordView | null
69
65
  }
70
66
  return (
71
67
  <Chip
72
- label={STATUS_LABEL[record.status]}
68
+ label={copy.statuses[record.status] ?? record.status}
73
69
  size="small"
74
70
  color={STATUS_COLOR[record.status]}
75
71
  data-testid="homologacao-status-chip"
@@ -88,11 +84,12 @@ function StatusChip({ record }: { record: PlatformHomologationRecordView | null
88
84
  const formatDateTime = (iso: string): string => new Date(iso).toLocaleString();
89
85
 
90
86
  function RecordTrail({ record }: { record: PlatformHomologationRecordView }): ReactNode {
87
+ const copy = usePlatformCopy().outcome;
91
88
  return (
92
89
  <Typography variant="caption" color="text.secondary" component="p">
93
- {record.submittedAt ? `Submitted ${formatDateTime(record.submittedAt)}. ` : ''}
94
- {record.decidedAt ? `Decided ${formatDateTime(record.decidedAt)}. ` : ''}
95
- {record.updatedBy ? `Recorded by ${record.updatedBy}.` : ''}
90
+ {record.submittedAt ? copy.submittedAt(formatDateTime(record.submittedAt)) : ''}
91
+ {record.decidedAt ? copy.decidedAt(formatDateTime(record.decidedAt)) : ''}
92
+ {record.updatedBy ? copy.recordedBy(record.updatedBy) : ''}
96
93
  </Typography>
97
94
  );
98
95
  }
@@ -106,6 +103,7 @@ interface HomologacaoOutcomeCardProps {
106
103
 
107
104
  export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): ReactNode {
108
105
  const { record, onSave, save } = props;
106
+ const copy = usePlatformCopy().outcome;
109
107
  const [status, setStatus] = useState<PlatformHomologationStatus>(record?.status ?? 'SUBMITTED');
110
108
  const [protocol, setProtocol] = useState(record?.protocol ?? '');
111
109
  const [notes, setNotes] = useState(record?.notes ?? '');
@@ -114,7 +112,7 @@ export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): Reac
114
112
  <Stack spacing={1.5} data-testid="homologacao-outcome-card" sx={CARD_SX}>
115
113
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
116
114
  <Typography variant="body2" fontWeight={600}>
117
- Homologation status
115
+ {copy.heading}
118
116
  </Typography>
119
117
  <StatusChip record={record} />
120
118
  </Box>
@@ -123,7 +121,7 @@ export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): Reac
123
121
  <TextField
124
122
  select
125
123
  size="small"
126
- label="Status"
124
+ label={copy.statusLabel}
127
125
  value={status}
128
126
  onChange={(event) => setStatus(event.target.value as PlatformHomologationStatus)}
129
127
  slotProps={{
@@ -131,24 +129,24 @@ export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): Reac
131
129
  htmlInput: { 'data-testid': 'homologacao-status-select' },
132
130
  }}
133
131
  >
134
- {(Object.keys(STATUS_LABEL) as PlatformHomologationStatus[]).map((key) => (
132
+ {(Object.keys(STATUS_COLOR) as PlatformHomologationStatus[]).map((key) => (
135
133
  <option key={key} value={key}>
136
- {STATUS_LABEL[key]}
134
+ {copy.statuses[key] ?? key}
137
135
  </option>
138
136
  ))}
139
137
  </TextField>
140
138
  <TextField
141
139
  size="small"
142
- aria-label="Protocol"
143
- placeholder="Protocol (Pipefy card / ticket)"
140
+ aria-label={copy.protocolLabel}
141
+ placeholder={copy.protocolPlaceholder}
144
142
  value={protocol}
145
143
  onChange={(event) => setProtocol(event.target.value)}
146
144
  slotProps={{ htmlInput: { 'data-testid': 'homologacao-protocol' } }}
147
145
  />
148
146
  <TextField
149
147
  size="small"
150
- aria-label="Notes"
151
- placeholder="Notes (PagBank's reply, context…)"
148
+ aria-label={copy.notesLabel}
149
+ placeholder={copy.notesPlaceholder}
152
150
  value={notes}
153
151
  onChange={(event) => setNotes(event.target.value)}
154
152
  slotProps={{ htmlInput: { 'data-testid': 'homologacao-notes' } }}
@@ -160,7 +158,7 @@ export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): Reac
160
158
  onClick={() => onSave({ status, protocol, notes })}
161
159
  data-testid="homologacao-save"
162
160
  >
163
- Record
161
+ {copy.save}
164
162
  </Button>
165
163
  </Box>
166
164
  {save.error !== null ? (
@@ -170,7 +168,7 @@ export function HomologacaoOutcomeCard(props: HomologacaoOutcomeCardProps): Reac
170
168
  ) : null}
171
169
  {save.success ? (
172
170
  <Alert severity="success" data-testid="homologacao-save-ok">
173
- Record updated.
171
+ {copy.saved}
174
172
  </Alert>
175
173
  ) : null}
176
174
  </Stack>
@@ -13,6 +13,8 @@ import {
13
13
  type HomologacaoSaveState,
14
14
  type PlatformHomologationRecordView,
15
15
  } from './HomologacaoOutcomeCard';
16
+ import type { PlatformHomologacaoCopy } from './copy';
17
+ import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
16
18
 
17
19
  /**
18
20
  * The PLATFORM's PagBank homologação screen (FUT-483, packaged by FUT-573).
@@ -49,10 +51,13 @@ export interface PlatformHomologacaoProps {
49
51
  * card shows it verbatim.
50
52
  */
51
53
  onGenerateAnexo: () => Promise<void>;
54
+ /** Every word these three cards render. REQUIRED — no default copy. */
55
+ copy: PlatformHomologacaoCopy;
52
56
  }
53
57
 
54
58
  /** The evidence-file half: real sandbox calls, downloaded as a text file. */
55
59
  function AnexoCard({ onGenerate }: { onGenerate: () => Promise<void> }): ReactNode {
60
+ const copy = usePlatformCopy().anexo;
56
61
  const [error, setError] = useState<string | null>(null);
57
62
  const [busy, setBusy] = useState(false);
58
63
 
@@ -62,7 +67,7 @@ function AnexoCard({ onGenerate }: { onGenerate: () => Promise<void> }): ReactNo
62
67
  try {
63
68
  await onGenerate();
64
69
  } catch (cause) {
65
- setError(cause instanceof Error ? cause.message : 'Could not generate the attachment.');
70
+ setError(cause instanceof Error ? cause.message : copy.generateFailed);
66
71
  } finally {
67
72
  setBusy(false);
68
73
  }
@@ -71,13 +76,10 @@ function AnexoCard({ onGenerate }: { onGenerate: () => Promise<void> }): ReactNo
71
76
  return (
72
77
  <Stack spacing={1.5} data-testid="homologacao-anexo-card" sx={CARD_SX}>
73
78
  <Typography variant="body2" fontWeight={600}>
74
- Evidence attachment
79
+ {copy.heading}
75
80
  </Typography>
76
81
  <Typography variant="body2" color="text.secondary" component="p">
77
- The form demands the requests and responses of the calls sent to PagBank's APIs. The
78
- button below makes those calls for real against the test environment (Sandbox), on the
79
- platform's own token — nothing is actually charged — and downloads the file ready to
80
- attach, with the token redacted.
82
+ {copy.body}
81
83
  </Typography>
82
84
  <Box>
83
85
  <Button
@@ -87,7 +89,7 @@ function AnexoCard({ onGenerate }: { onGenerate: () => Promise<void> }): ReactNo
87
89
  onClick={() => void generate()}
88
90
  data-testid="homologacao-anexo-button"
89
91
  >
90
- Generate attachment
92
+ {copy.generate}
91
93
  </Button>
92
94
  </Box>
93
95
  {error !== null ? (
@@ -102,10 +104,12 @@ function AnexoCard({ onGenerate }: { onGenerate: () => Promise<void> }): ReactNo
102
104
  export function PlatformHomologacao(props: PlatformHomologacaoProps): ReactNode {
103
105
  const { record, guide, onSaveRecord, save, onGenerateAnexo } = props;
104
106
  return (
105
- <Stack spacing={2} data-testid="platform-homologacao">
106
- <HomologacaoOutcomeCard record={record} onSave={onSaveRecord} save={save} />
107
- <HomologacaoGuideCard guide={guide} />
108
- <AnexoCard onGenerate={onGenerateAnexo} />
109
- </Stack>
107
+ <PlatformCopyProvider copy={props.copy}>
108
+ <Stack spacing={2} data-testid="platform-homologacao">
109
+ <HomologacaoOutcomeCard record={record} onSave={onSaveRecord} save={save} />
110
+ <HomologacaoGuideCard guide={guide} />
111
+ <AnexoCard onGenerate={onGenerateAnexo} />
112
+ </Stack>
113
+ </PlatformCopyProvider>
110
114
  );
111
115
  }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, type JSX, type ReactNode } from 'react';
4
+
5
+ import type { PlatformHomologacaoCopy } from './copy';
6
+
7
+ /**
8
+ * The platform screens' words, for the eight components that render them.
9
+ *
10
+ * A CONTEXT rather than a prop, for the same reason the settings surface uses
11
+ * one: this is a tree of small cards — the outcome form, the paste-ready
12
+ * answers, the anexo, the environment cards and the fields inside them — and
13
+ * threading one object through all of them as props is how a copy port comes
14
+ * to exist, be required, and go unread.
15
+ *
16
+ * It stays a single REQUIRED prop at the two mounts (`PlatformHomologacao` and
17
+ * `ConnectApplicationPanel`), which is the only place a host has to answer.
18
+ */
19
+ const PlatformCopyContext = createContext<PlatformHomologacaoCopy | null>(null);
20
+
21
+ export function PlatformCopyProvider({
22
+ copy,
23
+ children,
24
+ }: {
25
+ copy: PlatformHomologacaoCopy;
26
+ children: ReactNode;
27
+ }): JSX.Element {
28
+ return <PlatformCopyContext.Provider value={copy}>{children}</PlatformCopyContext.Provider>;
29
+ }
30
+
31
+ /**
32
+ * The words these screens render — THROWS outside a provider rather than
33
+ * falling back.
34
+ *
35
+ * A fallback could only be this package's own answer, handed silently to the
36
+ * next platform's operator. Failing at the mount is the point: it is the one
37
+ * moment a host can still be told it forgot.
38
+ */
39
+ export function usePlatformCopy(): PlatformHomologacaoCopy {
40
+ const copy = useContext(PlatformCopyContext);
41
+ if (!copy) {
42
+ throw new Error('usePlatformCopy must be rendered inside a <PlatformCopyProvider>');
43
+ }
44
+ return copy;
45
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Every word the PLATFORM's homologação screens render.
3
+ *
4
+ * The reader here is not a buyer and not a merchant: it is whoever operates
5
+ * the platform, submitting a vendor homologação on behalf of every tenant on
6
+ * it. That made this surface look internal enough to write literals into for a
7
+ * long time, and it is not — a second platform adopting
8
+ * `@12-apps/payments-frontend` mounts these same four cards, and its operator
9
+ * reads whatever this package decided, in whatever language it decided.
10
+ *
11
+ * So the same rule as every other surface here: REQUIRED, no defaults. A pack
12
+ * for one language ships as `PT_BR_PLATFORM_HOMOLOGACAO_COPY` and a host
13
+ * passes it by hand.
14
+ *
15
+ * What is NOT here, deliberately: `HomologacaoGuide.fieldLabels` and the guide's
16
+ * three URLs. Those are PagBank's own form field names and pages, answered by
17
+ * the backend adapter, and a translation of them produces a submission their
18
+ * form does not accept.
19
+ */
20
+
21
+ /** The outcome card: what was submitted, and what came back. */
22
+ export interface HomologacaoOutcomeCopy {
23
+ heading: string;
24
+ /** The status a record carries — and what "no record at all" reads as. */
25
+ statusLabel: string;
26
+ notSubmitted: string;
27
+ /** The two free-text fields, each with a visible-less input. */
28
+ protocolLabel: string;
29
+ protocolPlaceholder: string;
30
+ notesLabel: string;
31
+ notesPlaceholder: string;
32
+ /** Save, and the confirmation after it. */
33
+ save: string;
34
+ saved: string;
35
+ /** The three statuses a record can carry, keyed by the stored value. */
36
+ statuses: Readonly<Record<string, string>>;
37
+ /**
38
+ * The trail under the heading, one clause per timestamp the record has. Each
39
+ * takes its own value because the ORDER of word and date differs by language
40
+ * and this line concatenates whichever clauses exist.
41
+ */
42
+ submittedAt(when: string): string;
43
+ decidedAt(when: string): string;
44
+ recordedBy(who: string): string;
45
+ }
46
+
47
+ /** The paste-ready answers card, and the three links woven through its lede. */
48
+ export interface HomologacaoGuideCopy {
49
+ heading: string;
50
+ /**
51
+ * The instruction paragraph, in the runs it renders as: text, link, text,
52
+ * link, text, link, text. Split because two of the links sit MID-SENTENCE,
53
+ * so a single string with placeholders could not carry them.
54
+ */
55
+ ledeBeforeForm: string;
56
+ formLink: string;
57
+ ledeBeforeSupport: string;
58
+ supportLink: string;
59
+ ledeBeforeDocs: string;
60
+ docsLink: string;
61
+ ledeAfterDocs: string;
62
+ }
63
+
64
+ /** The evidence-file card. */
65
+ export interface HomologacaoAnexoCopy {
66
+ heading: string;
67
+ body: string;
68
+ generate: string;
69
+ /** A generate that threw something carrying no message of its own. */
70
+ generateFailed: string;
71
+ }
72
+
73
+ /** The Connect application panel, and the environment card under it. */
74
+ export interface ConnectApplicationCopy {
75
+ /** The callback the deployment uses — the value that must be registered. */
76
+ expectedRedirectHeading: string;
77
+ consultAgain: string;
78
+ /** No application resolved for this environment at all. */
79
+ noApplication: string;
80
+ /**
81
+ * The three verdicts on the registered `redirect_uri`: it matches, it does
82
+ * not, or PagBank reported none to compare against. A mismatch is the whole
83
+ * reason this card exists, so all three read as findings rather than states.
84
+ */
85
+ redirectMatches: string;
86
+ redirectDiffers: string;
87
+ redirectUnreported: string;
88
+ /** The application's own fields, as PagBank reports them. */
89
+ fields: {
90
+ name: string;
91
+ site: string;
92
+ description: string;
93
+ logo: string;
94
+ redirectUri: string;
95
+ };
96
+ /** What a field with no value reads as, and an unreported redirect_uri. */
97
+ fieldEmpty: string;
98
+ redirectNotReported: string;
99
+ /** Where the environment's application comes from, and its disclosure. */
100
+ resolvedFrom: string;
101
+ showConfig: string;
102
+ hideConfig: string;
103
+ /** The heading above whatever extra keys PagBank's response carried. */
104
+ extraKeys: string;
105
+ }
106
+
107
+ /** The whole platform surface, in one object a host passes at the mount. */
108
+ export interface PlatformHomologacaoCopy {
109
+ outcome: HomologacaoOutcomeCopy;
110
+ guide: HomologacaoGuideCopy;
111
+ anexo: HomologacaoAnexoCopy;
112
+ connect: ConnectApplicationCopy;
113
+ }
@@ -0,0 +1,73 @@
1
+ import type { PlatformHomologacaoCopy } from './copy';
2
+
3
+ /**
4
+ * The pt-BR pack for the platform's homologação screens.
5
+ *
6
+ * A NAMED pack, which is how this repo ships a language: a host imports it and
7
+ * passes it by hand, so choosing Portuguese is a line in that host's diff
8
+ * rather than a silence in this package.
9
+ *
10
+ * The English these screens rendered as literals is deliberately NOT what this
11
+ * pack says. Those sentences were written for a developer reading the code;
12
+ * these are written for the operator filling PagBank's form, which is the
13
+ * register the surface is actually in.
14
+ */
15
+ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
16
+ outcome: {
17
+ heading: 'Situação da homologação',
18
+ statusLabel: 'Situação',
19
+ notSubmitted: 'Não enviada',
20
+ protocolLabel: 'Protocolo',
21
+ protocolPlaceholder: 'Protocolo (card do Pipefy / chamado)',
22
+ notesLabel: 'Observações',
23
+ notesPlaceholder: 'Observações (resposta do PagBank, contexto…)',
24
+ save: 'Registrar',
25
+ saved: 'Registro atualizado.',
26
+ statuses: { SUBMITTED: 'Enviada', APPROVED: 'Aprovada', REJECTED: 'Recusada' },
27
+ submittedAt: (when) => `Enviada em ${when}. `,
28
+ decidedAt: (when) => `Decidida em ${when}. `,
29
+ recordedBy: (who) => `Registrado por ${who}.`,
30
+ },
31
+ guide: {
32
+ heading: 'Formulário de homologação — respostas prontas para colar',
33
+ ledeBeforeForm: 'Abra o ',
34
+ formLink: 'formulário oficial de homologação',
35
+ ledeBeforeSupport: ' e preencha com os valores abaixo. Em paralelo, abra um chamado no ',
36
+ supportLink: 'SIP — suporte de integração PagBank',
37
+ ledeBeforeDocs:
38
+ ' citando o 403 ACCESS_DENIED: o que responder primeiro define se o formulário cobre o Connect. Documentação: ',
39
+ docsLink: 'solicitar homologação',
40
+ ledeAfterDocs: '.',
41
+ },
42
+ anexo: {
43
+ heading: 'Anexo de evidências',
44
+ body:
45
+ 'O formulário pede as requisições e as respostas das chamadas enviadas às APIs do PagBank. O arquivo é gerado a partir das chamadas reais desta plataforma, com o token redigido.',
46
+ generate: 'Gerar anexo',
47
+ generateFailed: 'Não foi possível gerar o anexo.',
48
+ },
49
+ connect: {
50
+ expectedRedirectHeading: 'Callback que este deploy usa (o valor que precisa estar registrado)',
51
+ consultAgain: 'Consultar de novo',
52
+ noApplication: 'Nenhuma aplicação configurada neste ambiente.',
53
+ redirectMatches: 'O redirect_uri registrado confere com o callback que este deploy usa.',
54
+ redirectDiffers:
55
+ 'O redirect_uri registrado no PagBank é diferente do callback que este deploy usa.',
56
+ redirectUnreported:
57
+ 'A resposta do PagBank não trouxe redirect_uri, então não foi possível comparar com o callback.',
58
+ fields: {
59
+ name: 'Nome (exibido ao lojista)',
60
+ site: 'Site',
61
+ description: 'Descrição',
62
+ logo: 'Logo',
63
+ redirectUri: 'redirect_uri registrado',
64
+ },
65
+ fieldEmpty: '—',
66
+ redirectNotReported: 'não informado',
67
+ resolvedFrom:
68
+ 'A aplicação deste ambiente é resolvida estritamente a partir destas variáveis, sem fallback entre ambientes:',
69
+ showConfig: 'Mostrar variáveis de ambiente',
70
+ hideConfig: 'Ocultar variáveis de ambiente',
71
+ extraKeys: 'Outros campos retornados',
72
+ },
73
+ };
@@ -289,6 +289,12 @@ export interface SetupGuideCopy {
289
289
  confirmedByYou: string;
290
290
  /** Reopen a section the owner already confirmed. */
291
291
  reviewAction: string;
292
+ /**
293
+ * The sentence beside the confirm button. It exists because the owner is
294
+ * vouching for work done in the PROVIDER's dashboard, not here — a bare
295
+ * button gives them nothing to weigh that against.
296
+ */
297
+ confirmPrompt: string;
292
298
  /**
293
299
  * The copy-to-clipboard button beside a reference value, and what it says
294
300
  * for the two seconds after a successful copy.
@@ -166,6 +166,7 @@ export const PT_BR_PAYMENTS_SETTINGS_COPY: PaymentsSettingsCopy = {
166
166
  defaultConfirmLabel: 'Já habilitei o Checkout Integrado',
167
167
  confirmedByYou: 'Confirmado por você',
168
168
  reviewAction: 'Revisar',
169
+ confirmPrompt: 'Confirme quando terminar do lado do provedor.',
169
170
  copyValue: (fieldLabel) => `Copiar ${fieldLabel}`,
170
171
  copied: 'Copiado',
171
172
  },
package/src/index.ts CHANGED
@@ -306,23 +306,23 @@ export {
306
306
  // The PLATFORM operations screens (FUT-479 / FUT-483, packaged by FUT-573) —
307
307
  // the Connect-application consult and the homologação, as dumb components a
308
308
  // host page mounts with data + callbacks from its own routes. Their backend
309
- // halves live in `@12-apps/payments-backend` (`consultConnectApplications`,
310
- // `platformHomologacaoGuide`, `createHomologationRecordService`,
311
- // `buildPlatformHomologacaoAnexo`).
309
+ // halves are `consultConnectApplications`, `platformHomologacaoGuide`,
310
+ // `createHomologationRecordService` and `buildPlatformHomologacaoAnexo`.
312
311
  // ---------------------------------------------------------------------------
313
312
  export {
314
313
  ConnectApplicationPanel,
315
314
  type ConnectApplicationPanelProps,
316
315
  } from './components/platform/ConnectApplicationPanel';
317
- export {
318
- PlatformHomologacao,
319
- type PlatformHomologacaoProps,
320
- } from './components/platform/PlatformHomologacao';
316
+ export { PlatformHomologacao, type PlatformHomologacaoProps } from './components/platform/PlatformHomologacao';
321
317
  export {
322
318
  type HomologacaoSaveInput,
323
319
  type HomologacaoSaveState,
324
320
  type PlatformHomologationRecordView,
325
321
  } from './components/platform/HomologacaoOutcomeCard';
322
+ // Both mounts require `copy`, so the contract and its pt-BR pack are part of
323
+ // the port — a required port a host cannot import is not one.
324
+ export type * from './components/platform/copy';
325
+ export { PT_BR_PLATFORM_HOMOLOGACAO_COPY } from './components/platform/pt-BR';
326
326
 
327
327
  /**
328
328
  * Re-exported because it appears in the `prepareConnect` prop a host must