@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 +2 -2
- package/src/card/pagbank-test-cards.ts +50 -0
- package/src/card/tokenize.ts +26 -0
- package/src/components/checkout/poll-live.ts +7 -1
- package/src/components/platform/HomologacaoEvidenceCard.tsx +277 -0
- package/src/components/platform/copy.ts +25 -0
- package/src/components/platform/en-US.ts +39 -0
- package/src/components/platform/pt-BR.ts +44 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
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.
|
|
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
|
+
}
|
package/src/card/tokenize.ts
CHANGED
|
@@ -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,
|