@12-apps/payments-frontend 3.42.0 → 3.43.1

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.42.0",
3
+ "version": "3.43.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "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.",
@@ -23,7 +23,7 @@
23
23
  "storybook:build": "storybook build"
24
24
  },
25
25
  "dependencies": {
26
- "@12-apps/payments-backend": "^4.32.0",
26
+ "@12-apps/payments-backend": "^4.33.1",
27
27
  "react-qr-code": "^2.2.0"
28
28
  },
29
29
  "peerDependencies": {
@@ -0,0 +1,50 @@
1
+ /**
2
+ * PagBank's OFFICIAL sandbox test cards (developer.pagbank.com.br, "Cartões de
3
+ * teste") — the only cards the homologação evidence may use (FUT-3696).
4
+ *
5
+ * Public test data, published by PagBank for exactly this: they authorize
6
+ * nothing outside the sandbox. They are never sent to our server — the browser
7
+ * encrypts them and only the encrypted blob leaves it.
8
+ */
9
+ interface PagBankTestCard {
10
+ scenario: "approved" | "declined";
11
+ brand: string;
12
+ number: string;
13
+ holder: string;
14
+ expMonth: string;
15
+ expYear: string;
16
+ securityCode: string;
17
+ }
18
+
19
+ /**
20
+ * December of NEXT year. PagBank's page prints 12/2026 as the example expiry,
21
+ * and the sandbox accepts any future date; a literal would turn every run
22
+ * after that month into a "card expired" evidence for a reason nobody chose.
23
+ */
24
+ const TEST_EXPIRY_YEAR = String(new Date().getFullYear() + 1);
25
+
26
+ export const PAGBANK_TEST_CARDS: readonly PagBankTestCard[] = [
27
+ {
28
+ scenario: "approved",
29
+ brand: "VISA",
30
+ number: "4539620659922097",
31
+ holder: "Jose da Silva",
32
+ expMonth: "12",
33
+ expYear: TEST_EXPIRY_YEAR,
34
+ securityCode: "123",
35
+ },
36
+ {
37
+ scenario: "declined",
38
+ brand: "VISA",
39
+ number: "4929291898380766",
40
+ holder: "Jose da Silva",
41
+ expMonth: "12",
42
+ expYear: TEST_EXPIRY_YEAR,
43
+ securityCode: "123",
44
+ },
45
+ ];
46
+
47
+ /** `4539 62** **** 2097` — the first six and last four, as a receipt prints them. */
48
+ export function maskTestCard(number: string): string {
49
+ return `${number.slice(0, 4)} ${number.slice(4, 6)}** **** ${number.slice(-4)}`;
50
+ }
@@ -335,3 +335,29 @@ function mintToken(prefix: string): string {
335
335
  tokenCounter += 1;
336
336
  return `${prefix}_${Date.now().toString(36)}${tokenCounter.toString(36)}`;
337
337
  }
338
+
339
+ /** Why {@link encryptPagBankCard} produced no blob. */
340
+ type PagBankEncryptFailure = "SDK_UNAVAILABLE" | "NOT_PROCESSED";
341
+
342
+ /**
343
+ * Encrypt a card with PagBank's own browser SDK — the same script and the same
344
+ * `PagSeguro.encryptCard` call the checkout's {@link tokenizeCard} makes, minus
345
+ * the buyer-form validation and copy. For the homologação evidence (FUT-3696),
346
+ * which encrypts PagBank's official TEST cards and must prove the flow the
347
+ * checkout really uses: the PAN never leaves the browser, only the blob does.
348
+ */
349
+ export async function encryptPagBankCard(params: {
350
+ publicKey: string;
351
+ holder: string;
352
+ number: string;
353
+ expMonth: string;
354
+ expYear: string;
355
+ securityCode: string;
356
+ }): Promise<{ encrypted: string } | { failure: PagBankEncryptFailure }> {
357
+ if (!(await ensurePagBankSdk()) || !window.PagSeguro?.encryptCard) {
358
+ return { failure: "SDK_UNAVAILABLE" };
359
+ }
360
+ const result = window.PagSeguro.encryptCard(params);
361
+ if (result.hasErrors || !result.encryptedCard) return { failure: "NOT_PROCESSED" };
362
+ return { encrypted: result.encryptedCard };
363
+ }
@@ -23,7 +23,13 @@
23
23
  * - A wait given {@link LiveCadence.liveIntervalMs} keeps that floor. The hook
24
24
  * gives it one only on a hosted-checkout return carrying the settlement pair
25
25
  * (`carriesSettlementPair`), where the buyer's own read is what confirms the
26
- * payment and no server sweep can stand in for it.
26
+ * payment and no server sweep can stand in for it. That floor is a timed
27
+ * read on a live channel — a bounded wait on a THIRD PARTY, the provider the
28
+ * buyer just paid — and it is recorded as one: the origin host's realtime
29
+ * doc lists it under "Allowed exceptions", beside the redirect activation's
30
+ * verify (`../../activation/use-redirect-activation.ts`), with the decision
31
+ * that admits the class. If the provider's webhook ever settles every
32
+ * return, the floor goes and the return waits on the channel like the rest.
27
33
  *
28
34
  * Split out of `poll-loop.ts` so the loop stays one question: WHEN to ask. This
29
35
  * file answers only how being live changes that answer.
@@ -0,0 +1,277 @@
1
+ 'use client';
2
+
3
+ import { Alert, Box, Button, LinearProgress, Stack, Typography } from '@mui/material';
4
+ import { useState, type ReactNode } from 'react';
5
+
6
+ import type {
7
+ BrowserCardResult,
8
+ EvidenceSummary,
9
+ PaymentEnvironment,
10
+ } from '@12-apps/payments-backend';
11
+
12
+ import { maskTestCard, PAGBANK_TEST_CARDS } from '../../card/pagbank-test-cards';
13
+ import { encryptPagBankCard } from '../../card/tokenize';
14
+ import { CARD_SX, TAP_SX } from './ConnectEnvironmentCard';
15
+ import type { PlatformHomologacaoCopy } from './copy';
16
+ import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
17
+
18
+ /**
19
+ * The homologação EVIDENCE card (FUT-3696): connect a sandbox test seller,
20
+ * then run the real scenarios and download the TXT PagBank's form takes.
21
+ *
22
+ * The one thing this card does itself is the part that has to happen in a
23
+ * browser: encrypting PagBank's official TEST cards with the seller's public
24
+ * key, through the same SDK call the checkout makes. Everything else is the
25
+ * host's — each callback is a call to its own mounted routes — and what the
26
+ * file says is decided by the server, from what was recorded. This card shows
27
+ * the counts it answered and never infers a result of its own.
28
+ */
29
+
30
+ /** The test seller's connection, as the host reads it back. */
31
+ export interface HomologacaoSellerView {
32
+ connected: boolean;
33
+ environment: PaymentEnvironment | null;
34
+ /** UNVERIFIED | VERIFIED | FAILED | RECONNECT_REQUIRED. */
35
+ status: string | null;
36
+ connectedAt: string | null;
37
+ accountId: string | null;
38
+ /** Why the latest authorization did not complete, as the evidence recorded it. */
39
+ lastConnectProblem?: string | null;
40
+ }
41
+
42
+ export interface HomologacaoEvidenceCardProps {
43
+ copy: PlatformHomologacaoCopy;
44
+ /** Null while the host is still reading it. */
45
+ seller: HomologacaoSellerView | null;
46
+ /** How the last return from PagBank ended, when the page was opened by one. */
47
+ connectOutcome?: 'connected' | 'failed' | null;
48
+ /** Start the authorization: the host mints the state and navigates away. */
49
+ onConnect: () => Promise<void>;
50
+ /** Phase 1 — the seller's public key, null when it could not be minted. */
51
+ onPrepare: () => Promise<{ runId: string; publicKey: string | null }>;
52
+ /** Phase 2 — raise the orders with the encrypted cards. */
53
+ onRun: (runId: string, cards: BrowserCardResult[]) => Promise<void>;
54
+ /** Build and deliver the file; resolves to the counts it holds. */
55
+ onDownload: () => Promise<EvidenceSummary>;
56
+ }
57
+
58
+ type Phase = 'idle' | 'preparing' | 'encrypting' | 'running' | 'downloading';
59
+ type ActivePhase = Exclude<Phase, 'idle'>;
60
+
61
+ /** Each official test card, encrypted — or the code saying why it is not. */
62
+ async function encryptTestCards(publicKey: string | null): Promise<BrowserCardResult[]> {
63
+ const results: BrowserCardResult[] = [];
64
+ for (const card of PAGBANK_TEST_CARDS) {
65
+ const base = { scenario: card.scenario, brand: card.brand, masked: maskTestCard(card.number) };
66
+ if (!publicKey) {
67
+ results.push({ ...base, encrypted: null, failure: 'NO_PUBLIC_KEY' });
68
+ continue;
69
+ }
70
+ const encrypted = await encryptPagBankCard({
71
+ publicKey,
72
+ holder: card.holder,
73
+ number: card.number,
74
+ expMonth: card.expMonth,
75
+ expYear: card.expYear,
76
+ securityCode: card.securityCode,
77
+ });
78
+ results.push(
79
+ 'encrypted' in encrypted
80
+ ? { ...base, encrypted: encrypted.encrypted }
81
+ : { ...base, encrypted: null, failure: encrypted.failure },
82
+ );
83
+ }
84
+ return results;
85
+ }
86
+
87
+ export function HomologacaoEvidenceCard(props: HomologacaoEvidenceCardProps): ReactNode {
88
+ return (
89
+ <PlatformCopyProvider copy={props.copy}>
90
+ <EvidenceBody {...props} />
91
+ </PlatformCopyProvider>
92
+ );
93
+ }
94
+
95
+ /** The seller's one-line state — null while the host is still reading it. */
96
+ function sellerLine(copy: PlatformHomologacaoCopy['evidence'], seller: HomologacaoSellerView | null): string | null {
97
+ if (!seller) return null;
98
+ if (seller.status === 'RECONNECT_REQUIRED') return copy.sellerReconnect;
99
+ return seller.connected ? copy.sellerConnected(seller.connectedAt, seller.accountId) : copy.sellerNotConnected;
100
+ }
101
+
102
+ function SellerBlock({
103
+ seller,
104
+ onConnect,
105
+ }: Pick<HomologacaoEvidenceCardProps, 'seller' | 'onConnect'>): ReactNode {
106
+ const copy = usePlatformCopy().evidence;
107
+ const [failed, setFailed] = useState(false);
108
+ const [busy, setBusy] = useState(false);
109
+ const line = sellerLine(copy, seller);
110
+ const offerReconnect = Boolean(seller?.connected) && seller?.status !== 'RECONNECT_REQUIRED';
111
+ const connect = (): void => {
112
+ setBusy(true);
113
+ setFailed(false);
114
+ onConnect()
115
+ .catch(() => setFailed(true))
116
+ .finally(() => setBusy(false));
117
+ };
118
+ return (
119
+ <Stack spacing={1} data-testid="evidence-seller">
120
+ <Typography variant="body2" fontWeight={600}>
121
+ {copy.sellerHeading}
122
+ </Typography>
123
+ {line ? (
124
+ <Typography variant="body2" data-testid="evidence-seller-state">
125
+ {line}
126
+ </Typography>
127
+ ) : null}
128
+ <Typography variant="caption" component="p" color="text.secondary">
129
+ {copy.connectHint}
130
+ </Typography>
131
+ <Box>
132
+ <Button
133
+ // The next step until a seller is connected; secondary once one is.
134
+ variant={offerReconnect ? 'outlined' : 'contained'}
135
+ size="small"
136
+ disabled={!seller || busy}
137
+ onClick={connect}
138
+ data-testid="evidence-connect"
139
+ sx={TAP_SX}
140
+ >
141
+ {offerReconnect ? copy.reconnect : copy.connect}
142
+ </Button>
143
+ </Box>
144
+ {failed ? (
145
+ <Alert severity="error" data-testid="evidence-connect-failed">
146
+ {copy.connectFailed}
147
+ </Alert>
148
+ ) : null}
149
+ </Stack>
150
+ );
151
+ }
152
+
153
+ /** One run at a time: which phase it is in, what it answered, where it failed. */
154
+ function useEvidenceRun({ onPrepare, onRun, onDownload }: Pick<HomologacaoEvidenceCardProps, 'onPrepare' | 'onRun' | 'onDownload'>) {
155
+ const [phase, setPhase] = useState<Phase>('idle');
156
+ const [summary, setSummary] = useState<EvidenceSummary | null>(null);
157
+ const [failed, setFailed] = useState<ActivePhase | null>(null);
158
+ const run = async (full: boolean): Promise<void> => {
159
+ setFailed(null);
160
+ setSummary(null);
161
+ // The phase that throws is the one the operator is told about.
162
+ let current: ActivePhase = full ? 'preparing' : 'downloading';
163
+ const enter = (next: ActivePhase): void => {
164
+ current = next;
165
+ setPhase(next);
166
+ };
167
+ try {
168
+ if (full) {
169
+ enter('preparing');
170
+ const prepared = await onPrepare();
171
+ enter('encrypting');
172
+ const cards = await encryptTestCards(prepared.publicKey);
173
+ enter('running');
174
+ await onRun(prepared.runId, cards);
175
+ }
176
+ enter('downloading');
177
+ setSummary(await onDownload());
178
+ } catch {
179
+ setFailed(current);
180
+ } finally {
181
+ setPhase('idle');
182
+ }
183
+ };
184
+ return { phase, busy: phase !== 'idle', summary, failed, run };
185
+ }
186
+
187
+ /** How the last return from PagBank ended — with the recorded reason when it failed. */
188
+ function ConnectOutcome({ seller, connectOutcome }: Pick<HomologacaoEvidenceCardProps, 'seller' | 'connectOutcome'>): ReactNode {
189
+ const copy = usePlatformCopy().evidence;
190
+ // The URL only says PagBank came back; the seller's own row says whether it
191
+ // connected. A "connected" the row does not confirm is not announced.
192
+ const outcome = connectOutcome === 'connected' && !seller?.connected ? null : connectOutcome;
193
+ if (!outcome) return null;
194
+ return (
195
+ <Alert severity={outcome === 'connected' ? 'success' : 'warning'} data-testid="evidence-connect-outcome">
196
+ {outcome === 'connected' ? copy.returnedConnected : (seller?.lastConnectProblem ?? copy.returnedFailed)}
197
+ </Alert>
198
+ );
199
+ }
200
+
201
+ function EvidenceBody(props: HomologacaoEvidenceCardProps): ReactNode {
202
+ const { seller, onConnect } = props;
203
+ const copy = usePlatformCopy().evidence;
204
+ const { phase, busy, summary, failed, run } = useEvidenceRun(props);
205
+ return (
206
+ <Stack spacing={1.5} data-testid="homologacao-evidence-card" sx={CARD_SX}>
207
+ <Typography variant="body2" fontWeight={600}>
208
+ {copy.heading}
209
+ </Typography>
210
+ <Typography variant="caption" component="p">
211
+ {copy.lede}
212
+ </Typography>
213
+ <ConnectOutcome seller={seller} connectOutcome={props.connectOutcome} />
214
+ <SellerBlock seller={seller} onConnect={onConnect} />
215
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'center' } }}>
216
+ <Button
217
+ variant={seller?.connected ? 'contained' : 'outlined'}
218
+ size="small"
219
+ disabled={busy}
220
+ onClick={() => void run(true)}
221
+ data-testid="evidence-generate"
222
+ sx={TAP_SX}
223
+ >
224
+ {copy.generate}
225
+ </Button>
226
+ <Button
227
+ variant="text"
228
+ size="small"
229
+ disabled={busy}
230
+ onClick={() => void run(false)}
231
+ data-testid="evidence-download-latest"
232
+ sx={TAP_SX}
233
+ >
234
+ {copy.downloadLatest}
235
+ </Button>
236
+ </Stack>
237
+ {busy ? (
238
+ <Stack spacing={0.5} role="status" aria-busy="true" data-testid="evidence-phase">
239
+ <Typography variant="body2">{copy.phases[phase as ActivePhase]}</Typography>
240
+ <LinearProgress />
241
+ </Stack>
242
+ ) : null}
243
+ <EvidenceResult summary={summary} failed={failed} />
244
+ </Stack>
245
+ );
246
+ }
247
+
248
+ /** The server's counts — never inferred here — or the failure line. */
249
+ function EvidenceResult({
250
+ summary,
251
+ failed,
252
+ }: {
253
+ summary: EvidenceSummary | null;
254
+ failed: ActivePhase | null;
255
+ }): ReactNode {
256
+ const copy = usePlatformCopy().evidence;
257
+ if (failed) {
258
+ return (
259
+ <Alert severity="error" data-testid="evidence-generate-failed">
260
+ {copy.generateFailed(failed)}
261
+ </Alert>
262
+ );
263
+ }
264
+ if (!summary) return null;
265
+ return (
266
+ <Stack spacing={0.5} role="status" data-testid="evidence-summary">
267
+ <Typography variant="body2" fontWeight={600} data-testid="evidence-summary-counts">
268
+ {copy.summary(summary)}
269
+ </Typography>
270
+ {summary.failed + summary.pending > 0 ? (
271
+ <Typography variant="caption" component="p" data-testid="evidence-incomplete">
272
+ {copy.incomplete}
273
+ </Typography>
274
+ ) : null}
275
+ </Stack>
276
+ );
277
+ }
@@ -130,10 +130,35 @@ export interface ConnectApplicationCopy {
130
130
  };
131
131
  }
132
132
 
133
+ /** The homologação evidence card (FUT-3696) — the sandbox test seller and the TXT. */
134
+ export interface HomologacaoEvidenceCardCopy {
135
+ heading: string;
136
+ lede: string;
137
+ sellerHeading: string;
138
+ /** `connectedAt` is ISO-8601; the pack formats it in its own locale. */
139
+ sellerConnected: (connectedAt: string | null, accountId: string | null) => string;
140
+ sellerNotConnected: string;
141
+ sellerReconnect: string;
142
+ connectHint: string;
143
+ connect: string;
144
+ reconnect: string;
145
+ connectFailed: string;
146
+ returnedConnected: string;
147
+ returnedFailed: string;
148
+ generate: string;
149
+ downloadLatest: string;
150
+ phases: { preparing: string; encrypting: string; running: string; downloading: string };
151
+ summary: (counts: { executed: number; failed: number; pending: number }) => string;
152
+ incomplete: string;
153
+ /** Which phase failed — and that the latest recorded file can still be downloaded. */
154
+ generateFailed: (phase: 'preparing' | 'encrypting' | 'running' | 'downloading') => string;
155
+ }
156
+
133
157
  /** The whole platform surface, in one object a host passes at the mount. */
134
158
  export interface PlatformHomologacaoCopy {
135
159
  outcome: HomologacaoOutcomeCopy;
136
160
  guide: HomologacaoGuideCopy;
137
161
  anexo: HomologacaoAnexoCopy;
138
162
  connect: ConnectApplicationCopy;
163
+ evidence: HomologacaoEvidenceCardCopy;
139
164
  }
@@ -106,4 +106,43 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
106
106
  'PagBank does not check the client_secret in this lookup — only the client_id and the token. A wrong client_secret only shows at the first store authorization.',
107
107
  },
108
108
  },
109
+ evidence: {
110
+ heading: 'Homologation evidence (sandbox)',
111
+ lede: "Runs the real scenarios in PagBank's sandbox and builds the TXT for the form. Nothing is charged.",
112
+ sellerHeading: 'Test seller',
113
+ sellerConnected: (connectedAt, accountId) =>
114
+ `Connected${connectedAt ? ` on ${new Date(connectedAt).toLocaleString('en-US')}` : ''}` +
115
+ (accountId ? ` · account ${accountId}` : ''),
116
+ sellerNotConnected:
117
+ 'No test seller connected. Without one, the Connect, Pix and card scenarios stay pending in the file.',
118
+ sellerReconnect: "The test seller's connection must be authorized again.",
119
+ connectHint:
120
+ "You will sign in to PagBank's sandbox with a test seller account and approve the consent. Sandbox only.",
121
+ connect: 'Connect test seller',
122
+ reconnect: 'Reconnect test seller',
123
+ connectFailed: 'Could not start the test seller connection.',
124
+ returnedConnected: 'Test seller connected. The callback and the code exchange were recorded in the evidence.',
125
+ returnedFailed: 'The test seller connection did not complete. Try connecting again.',
126
+ generate: 'Generate evidence file\u00a0(TXT)',
127
+ downloadLatest: 'Download the latest recorded',
128
+ phases: {
129
+ preparing: 'Consulting the application, renewing the token and minting the public key…',
130
+ encrypting: 'Encrypting the test cards in the browser…',
131
+ running: 'Creating the Pix and card orders in the sandbox — this can take up to a minute…',
132
+ downloading: 'Building the file…',
133
+ },
134
+ summary: ({ executed, failed, pending }) =>
135
+ `✓\u00a0${executed}\u00a0executed · ✗\u00a0${failed}\u00a0failed · ⧗\u00a0${pending}\u00a0pending`,
136
+ incomplete:
137
+ 'The file lists what is missing under Pendências. Do not submit it as complete. Notifications that arrive later show up when you download the latest recorded.',
138
+ generateFailed: (phase) =>
139
+ `${
140
+ {
141
+ preparing: 'Consulting the application and preparing the test seller failed.',
142
+ encrypting: 'Encrypting the test cards in the browser failed.',
143
+ running: 'Creating the orders in the sandbox failed.',
144
+ downloading: 'Building the file failed.',
145
+ }[phase]
146
+ } Try again; "Download the latest recorded" still returns what was recorded.`,
147
+ },
109
148
  };
@@ -85,4 +85,48 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
85
85
  'O PagBank não verifica o client_secret nesta consulta — só o client_id e o token. Um client_secret errado só aparece na primeira autorização de loja.',
86
86
  },
87
87
  },
88
+ evidence: {
89
+ heading: 'Evidências para homologação (sandbox)',
90
+ lede: 'Executa os cenários reais no sandbox do PagBank e gera o TXT para o formulário. Nada é cobrado.',
91
+ sellerHeading: 'Lojista de teste',
92
+ sellerConnected: (connectedAt, accountId) =>
93
+ `Conectado${connectedAt ? ` em ${new Date(connectedAt).toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' })}` : ''}` +
94
+ (accountId ? ` · conta ${accountId}` : ''),
95
+ sellerNotConnected:
96
+ 'Nenhum lojista de teste conectado. Sem ele, os cenários de Connect, Pix e cartão ficam pendentes no arquivo.',
97
+ sellerReconnect: 'A conexão do lojista de teste precisa ser autorizada de novo.',
98
+ connectHint:
99
+ 'Você fará login no sandbox do PagBank com uma conta de vendedor de teste e aprovará o consentimento. Somente sandbox.',
100
+ connect: 'Conectar lojista de teste',
101
+ reconnect: 'Reconectar lojista de teste',
102
+ connectFailed: 'Não foi possível iniciar a conexão do lojista de teste.',
103
+ returnedConnected:
104
+ 'Lojista de teste conectado. O callback e a troca do código foram registrados nas evidências.',
105
+ returnedFailed:
106
+ 'A conexão do lojista de teste não foi concluída. Tente conectar de novo.',
107
+ generate: 'Gerar arquivo de evidências\u00a0(TXT)',
108
+ downloadLatest: 'Baixar o último registrado',
109
+ phases: {
110
+ preparing: 'Consultando a aplicação, renovando o token e gerando a chave pública…',
111
+ encrypting: 'Criptografando os cartões de teste no navegador…',
112
+ running: 'Criando os pedidos Pix e cartão no sandbox — pode levar até um minuto…',
113
+ downloading: 'Montando o arquivo…',
114
+ },
115
+ summary: ({ executed, failed, pending }) =>
116
+ // Non-breaking inside each count, so only the separators may wrap.
117
+ `✓\u00a0${executed}\u00a0${executed === 1 ? 'executado' : 'executados'} · ` +
118
+ `✗\u00a0${failed}\u00a0${failed === 1 ? 'falhou' : 'falharam'} · ` +
119
+ `⧗\u00a0${pending}\u00a0${pending === 1 ? 'pendente' : 'pendentes'}`,
120
+ incomplete:
121
+ 'O arquivo lista o que falta na seção Pendências. Não o envie como completo. Notificações que chegarem depois aparecem ao baixar o último registrado.',
122
+ generateFailed: (phase) =>
123
+ `${
124
+ {
125
+ preparing: 'Falhou ao consultar a aplicação e preparar o lojista de teste.',
126
+ encrypting: 'Falhou ao criptografar os cartões de teste no navegador.',
127
+ running: 'Falhou ao criar os pedidos no sandbox.',
128
+ downloading: 'Falhou ao montar o arquivo.',
129
+ }[phase]
130
+ } Tente de novo; "Baixar o último registrado" ainda traz o que já foi registrado.`,
131
+ },
88
132
  };
package/src/index.ts CHANGED
@@ -337,6 +337,7 @@ export {
337
337
  type ConnectApplicationPanelProps,
338
338
  } from './components/platform/ConnectApplicationPanel';
339
339
  export { PlatformHomologacao, type PlatformHomologacaoProps } from './components/platform/PlatformHomologacao';
340
+ export * from './components/platform/HomologacaoEvidenceCard';
340
341
  export {
341
342
  type HomologacaoSaveInput,
342
343
  type HomologacaoSaveState,