@12-apps/payments-frontend 3.47.2 → 3.48.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.47.2",
3
+ "version": "3.48.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.38.0",
26
+ "@12-apps/payments-backend": "^4.39.0",
27
27
  "react-qr-code": "^2.2.0"
28
28
  },
29
29
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Alert, Box, Button, LinearProgress, Stack, Typography } from '@mui/material';
3
+ import { Alert, Box, Button, Stack, Typography } from '@mui/material';
4
4
  import { useState, type ReactNode } from 'react';
5
5
 
6
6
  import type {
@@ -8,6 +8,7 @@ import type {
8
8
  BrowserCardResult,
9
9
  BrowserWalletResult,
10
10
  DebitTestData,
11
+ EvidenceRunSection,
11
12
  EvidenceSummary,
12
13
  EvidenceWalletsConfig,
13
14
  PaymentEnvironment,
@@ -21,6 +22,7 @@ import type { GooglePayApi } from '../checkout/google-pay-button';
21
22
  import { CARD_SX, TAP_SX } from './ConnectEnvironmentCard';
22
23
  import type { PlatformHomologacaoCopy } from './copy';
23
24
  import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
25
+ import { EvidenceProgress, type EvidencePhase } from './HomologacaoEvidenceProgress';
24
26
  import { EvidenceWalletBlock, useEvidenceWallets, type EvidenceWallets } from './HomologacaoEvidenceWallets';
25
27
 
26
28
  /**
@@ -78,9 +80,15 @@ export interface HomologacaoEvidenceCardProps {
78
80
  }>;
79
81
  /**
80
82
  * Phase 2 — raise the orders with the encrypted cards and, for each wallet,
81
- * the token its sheet gave (or the code saying why there is none).
83
+ * the token its sheet gave (or the code saying why there is none). The host
84
+ * calls `onSection` as the server starts each section, for the step list.
82
85
  */
83
- onRun: (runId: string, cards: BrowserCardResult[], wallets: BrowserWalletResult[]) => Promise<void>;
86
+ onRun: (
87
+ runId: string,
88
+ cards: BrowserCardResult[],
89
+ wallets: BrowserWalletResult[],
90
+ onSection: (section: EvidenceRunSection) => void,
91
+ ) => Promise<void>;
84
92
  /**
85
93
  * The host's Apple Pay merchant validation (FUT-3755): exchange Apple's
86
94
  * `validationURL` for a merchant session, server-side. Absent, the Apple Pay
@@ -93,8 +101,8 @@ export interface HomologacaoEvidenceCardProps {
93
101
  onDownload: () => Promise<EvidenceSummary>;
94
102
  }
95
103
 
96
- type Phase = 'idle' | 'preparing' | 'encrypting' | 'running' | 'downloading';
97
- type ActivePhase = Exclude<Phase, 'idle'>;
104
+ type Phase = 'idle' | EvidencePhase;
105
+ type ActivePhase = EvidencePhase;
98
106
 
99
107
  type TestCard = typeof PAGBANK_3DS_TEST_CARD;
100
108
 
@@ -236,9 +244,17 @@ function useEvidenceRun(
236
244
  const [phase, setPhase] = useState<Phase>('idle');
237
245
  const [summary, setSummary] = useState<EvidenceSummary | null>(null);
238
246
  const [failed, setFailed] = useState<ActivePhase | null>(null);
247
+ // What the step list draws: the server's section, and the tokens this run took.
248
+ const [section, setSection] = useState<EvidenceRunSection | null>(null);
249
+ const [scope, setScope] = useState<{ full: boolean; walletResults: BrowserWalletResult[] | null }>({
250
+ full: false,
251
+ walletResults: null,
252
+ });
239
253
  const run = async (full: boolean): Promise<void> => {
240
254
  setFailed(null);
241
255
  setSummary(null);
256
+ setSection(null);
257
+ setScope({ full, walletResults: null });
242
258
  // The phase that throws is the one the operator is told about.
243
259
  let current: ActivePhase = full ? 'preparing' : 'downloading';
244
260
  const enter = (next: ActivePhase): void => {
@@ -251,11 +267,12 @@ function useEvidenceRun(
251
267
  // The sheets ran before this chain: what they answered, taken once.
252
268
  const walletResults = wallets.collect();
253
269
  wallets.reset();
270
+ setScope({ full, walletResults });
254
271
  const prepared = await onPrepare();
255
272
  enter('encrypting');
256
273
  const cards = await prepareTestCards(prepared);
257
274
  enter('running');
258
- await onRun(prepared.runId, cards, walletResults);
275
+ await onRun(prepared.runId, cards, walletResults, setSection);
259
276
  }
260
277
  enter('downloading');
261
278
  setSummary(await onDownload());
@@ -265,7 +282,7 @@ function useEvidenceRun(
265
282
  setPhase('idle');
266
283
  }
267
284
  };
268
- return { phase, busy: phase !== 'idle', summary, failed, run };
285
+ return { phase, section, scope, busy: phase !== 'idle', summary, failed, run };
269
286
  }
270
287
 
271
288
  /** How the last return from PagBank ended — with the recorded reason when it failed. */
@@ -290,7 +307,7 @@ function EvidenceBody(props: HomologacaoEvidenceCardProps): ReactNode {
290
307
  validateApplePayMerchant: props.onValidateApplePayMerchant,
291
308
  googlePayApi: props.googlePayApi,
292
309
  });
293
- const { phase, busy, summary, failed, run } = useEvidenceRun(props, wallets);
310
+ const { phase, section, scope, busy, summary, failed, run } = useEvidenceRun(props, wallets);
294
311
  return (
295
312
  <Stack spacing={1.5} data-testid="homologacao-evidence-card" sx={CARD_SX}>
296
313
  <Typography variant="body2" fontWeight={600}>
@@ -324,11 +341,8 @@ function EvidenceBody(props: HomologacaoEvidenceCardProps): ReactNode {
324
341
  {copy.downloadLatest}
325
342
  </Button>
326
343
  </Stack>
327
- {busy ? (
328
- <Stack spacing={0.5} role="status" aria-busy="true" data-testid="evidence-phase">
329
- <Typography variant="body2">{copy.phases[phase as ActivePhase]}</Typography>
330
- <LinearProgress />
331
- </Stack>
344
+ {phase !== 'idle' ? (
345
+ <EvidenceProgress phase={phase} section={section} full={scope.full} walletResults={scope.walletResults} />
332
346
  ) : null}
333
347
  <EvidenceResult summary={summary} failed={failed} />
334
348
  </Stack>
@@ -0,0 +1,135 @@
1
+ 'use client';
2
+
3
+ import { Box, CircularProgress, LinearProgress, Stack, SvgIcon, Typography } from '@mui/material';
4
+ import type { ReactNode } from 'react';
5
+
6
+ import type { BrowserWalletResult, EvidenceRunSection } from '@12-apps/payments-backend';
7
+
8
+ import { usePlatformCopy } from './copy-context';
9
+
10
+ /**
11
+ * The evidence run's progress, one line per step (FUT-3755): a run takes
12
+ * minutes — every cancel is retried while the sandbox answers 40008 — so the
13
+ * operator sees which step is done, which one is going, and which wallets this
14
+ * generation carries (their sheets were tapped BEFORE "generate", and the
15
+ * buttons reset once the tokens are taken).
16
+ *
17
+ * The sections come from the host as the server starts each one; the card
18
+ * never guesses one. A download-only press shows the file step alone.
19
+ */
20
+
21
+ export type EvidencePhase = 'preparing' | 'encrypting' | 'running' | 'downloading';
22
+ type EvidenceRunStep = 'prepare' | 'encrypt' | EvidenceRunSection | 'file';
23
+
24
+ // The server's section order (`EVIDENCE_RUN_SECTIONS`), spelled here: this
25
+ // package may import only types from the backend (ADOPTING.md §6).
26
+ const SECTIONS: readonly EvidenceRunSection[] = ['PIX', 'CARD', 'DEBIT', 'GOOGLE_PAY', 'APPLE_PAY'];
27
+ const FULL_RUN: readonly EvidenceRunStep[] = ['prepare', 'encrypt', ...SECTIONS, 'file'];
28
+ const DOWNLOAD_ONLY: readonly EvidenceRunStep[] = ['file'];
29
+
30
+ /** The step a phase — and, while running, the server's section — is on. */
31
+ function currentEvidenceStep(phase: EvidencePhase, section: EvidenceRunSection | null): EvidenceRunStep {
32
+ if (phase === 'preparing') return 'prepare';
33
+ if (phase === 'encrypting') return 'encrypt';
34
+ if (phase === 'running') return section ?? 'PIX';
35
+ return 'file';
36
+ }
37
+
38
+ interface EvidenceProgressProps {
39
+ phase: EvidencePhase;
40
+ section: EvidenceRunSection | null;
41
+ /** False for "download the latest recorded": nothing is run. */
42
+ full: boolean;
43
+ /** What the wallet sheets answered for this run; null on a download-only press. */
44
+ walletResults: BrowserWalletResult[] | null;
45
+ }
46
+
47
+ // Material Symbols' check_circle and radio_button_unchecked, inline: the
48
+ // package takes no icons dependency (see `../checkout/icons.tsx`).
49
+ const DONE_PATH = 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m-2 15-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8z';
50
+ const PENDING_PATH =
51
+ 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8';
52
+
53
+ /**
54
+ * Shape, not only colour, tells the three states apart. The spinner and the
55
+ * bar take the ink, never the theme's primary: on a staff screen whose primary
56
+ * is red they would read as a failure beside the green checks.
57
+ */
58
+ function StepMark({ state }: { state: 'done' | 'current' | 'pending' }): ReactNode {
59
+ return (
60
+ <Box sx={{ width: 20, height: 20, flexShrink: 0, display: 'grid', placeItems: 'center' }}>
61
+ {state === 'current' ? (
62
+ <CircularProgress size={18} thickness={5} color="inherit" sx={{ color: 'text.primary' }} />
63
+ ) : (
64
+ <SvgIcon fontSize="small" sx={{ color: state === 'done' ? 'success.main' : 'text.secondary' }}>
65
+ <path d={state === 'done' ? DONE_PATH : PENDING_PATH} />
66
+ </SvgIcon>
67
+ )}
68
+ </Box>
69
+ );
70
+ }
71
+
72
+ /** Which wallets this run carries a token for, and which stay pending. */
73
+ function WalletsLine({ results }: { results: BrowserWalletResult[] }): ReactNode {
74
+ const copy = usePlatformCopy().evidence;
75
+ const names = copy.wallets.names;
76
+ const included = (['GOOGLE_PAY', 'APPLE_PAY'] as const).filter((type) =>
77
+ results.some((result) => result.type === type && result.token),
78
+ );
79
+ const left = (['GOOGLE_PAY', 'APPLE_PAY'] as const).filter((type) => !included.includes(type));
80
+ return (
81
+ <Typography variant="caption" component="p" data-testid="evidence-run-wallets">
82
+ {copy.steps.walletsInRun(
83
+ included.map((type) => names[type]),
84
+ left.map((type) => names[type]),
85
+ )}
86
+ </Typography>
87
+ );
88
+ }
89
+
90
+ export function EvidenceProgress({ phase, section, full, walletResults }: EvidenceProgressProps): ReactNode {
91
+ const copy = usePlatformCopy().evidence;
92
+ const steps = full ? FULL_RUN : DOWNLOAD_ONLY;
93
+ const at = steps.indexOf(currentEvidenceStep(phase, section));
94
+ return (
95
+ <Stack spacing={1} role="status" aria-busy="true" data-testid="evidence-phase">
96
+ <Typography variant="body2">{copy.phases[phase]}</Typography>
97
+ <LinearProgress
98
+ variant="determinate"
99
+ value={(at / steps.length) * 100}
100
+ aria-label={copy.steps.label}
101
+ color="inherit"
102
+ sx={{ color: 'text.primary' }}
103
+ />
104
+ {full ? (
105
+ <Box component="ol" sx={{ m: 0, p: 0, listStyle: 'none', display: 'grid', gap: 0.5 }} data-testid="evidence-steps">
106
+ {steps.map((step, index) => {
107
+ const state = index < at ? 'done' : index === at ? 'current' : 'pending';
108
+ return (
109
+ <Stack
110
+ key={step}
111
+ component="li"
112
+ direction="row"
113
+ spacing={1}
114
+ sx={{ alignItems: 'center', minHeight: 24 }}
115
+ data-testid={`evidence-step-${step}`}
116
+ data-state={state}
117
+ aria-current={state === 'current' ? 'step' : undefined}
118
+ >
119
+ <StepMark state={state} />
120
+ <Typography
121
+ variant="body2"
122
+ fontWeight={state === 'current' ? 600 : 400}
123
+ color={state === 'pending' ? 'text.secondary' : 'text.primary'}
124
+ >
125
+ {copy.steps.names[step]}
126
+ </Typography>
127
+ </Stack>
128
+ );
129
+ })}
130
+ </Box>
131
+ ) : null}
132
+ {walletResults ? <WalletsLine results={walletResults} /> : null}
133
+ </Stack>
134
+ );
135
+ }
@@ -148,6 +148,14 @@ export interface HomologacaoEvidenceCardCopy {
148
148
  generate: string;
149
149
  downloadLatest: string;
150
150
  phases: { preparing: string; encrypting: string; running: string; downloading: string };
151
+ /** The run's step list (FUT-3755): which step is done, which one is going. */
152
+ steps: {
153
+ /** The bar's accessible name. */
154
+ label: string;
155
+ names: Record<'prepare' | 'encrypt' | 'PIX' | 'CARD' | 'DEBIT' | 'GOOGLE_PAY' | 'APPLE_PAY' | 'file', string>;
156
+ /** Which wallets this generation carries a token for, and which stay pending. */
157
+ walletsInRun: (included: string[], pending: string[]) => string;
158
+ };
151
159
  summary: (counts: { executed: number; failed: number; pending: number }) => string;
152
160
  incomplete: string;
153
161
  /** Which phase failed — and that the latest recorded file can still be downloaded. */
@@ -128,9 +128,29 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
128
128
  phases: {
129
129
  preparing: 'Consulting the application, renewing the token and minting the public key and the 3DS session…',
130
130
  encrypting: 'Encrypting the test cards and authenticating the debit card (3DS) in the browser…',
131
- running: 'Creating the Pix, credit, debit and wallet orders in the sandbox — this can take a few minutes…',
131
+ running: 'Creating the orders in the sandbox — this can take a few minutes…',
132
132
  downloading: 'Building the file…',
133
133
  },
134
+ steps: {
135
+ label: 'Generation progress',
136
+ names: {
137
+ prepare: 'Prepare (application, token, public key and 3DS session)',
138
+ encrypt: 'Encrypt the cards and authenticate the debit (3DS)',
139
+ PIX: 'Pix',
140
+ CARD: 'Credit card',
141
+ DEBIT: 'Debit with 3DS',
142
+ GOOGLE_PAY: 'Google Pay',
143
+ APPLE_PAY: 'Apple Pay',
144
+ file: 'Build the file',
145
+ },
146
+ walletsInRun: (included, pending) =>
147
+ [
148
+ included.length > 0 ? `In this generation: ${included.join(' and ')}.` : 'No wallet in this generation.',
149
+ pending.length > 0 ? `${pending.join(' and ')} ${pending.length === 1 ? 'stays' : 'stay'} pending.` : '',
150
+ ]
151
+ .filter(Boolean)
152
+ .join(' '),
153
+ },
134
154
  summary: ({ executed, failed, pending }) =>
135
155
  `✓\u00a0${executed}\u00a0executed · ✗\u00a0${failed}\u00a0failed · ⧗\u00a0${pending}\u00a0pending`,
136
156
  incomplete:
@@ -109,9 +109,29 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
109
109
  phases: {
110
110
  preparing: 'Consultando a aplicação, renovando o token e gerando a chave pública e a sessão 3DS…',
111
111
  encrypting: 'Criptografando os cartões de teste e autenticando o cartão de débito (3DS) no navegador…',
112
- running: 'Criando os pedidos Pix, crédito, débito e carteiras no sandbox — pode levar alguns minutos…',
112
+ running: 'Criando os pedidos no sandbox — pode levar alguns minutos…',
113
113
  downloading: 'Montando o arquivo…',
114
114
  },
115
+ steps: {
116
+ label: 'Progresso da geração',
117
+ names: {
118
+ prepare: 'Preparar (aplicação, token, chave pública e sessão 3DS)',
119
+ encrypt: 'Criptografar os cartões e autenticar o débito (3DS)',
120
+ PIX: 'Pix',
121
+ CARD: 'Cartão de crédito',
122
+ DEBIT: 'Débito com 3DS',
123
+ GOOGLE_PAY: 'Google Pay',
124
+ APPLE_PAY: 'Apple Pay',
125
+ file: 'Montar o arquivo',
126
+ },
127
+ walletsInRun: (included, pending) =>
128
+ [
129
+ included.length > 0 ? `Nesta geração: ${included.join(' e ')}.` : 'Nenhuma carteira nesta geração.',
130
+ pending.length > 0 ? `${pending.join(' e ')} ${pending.length === 1 ? 'fica pendente' : 'ficam pendentes'}.` : '',
131
+ ]
132
+ .filter(Boolean)
133
+ .join(' '),
134
+ },
115
135
  summary: ({ executed, failed, pending }) =>
116
136
  // Non-breaking inside each count, so only the separators may wrap.
117
137
  `✓\u00a0${executed}\u00a0${executed === 1 ? 'executado' : 'executados'} · ` +