@12-apps/payments-frontend 3.46.1 → 3.47.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/components/checkout/apple-pay-button.tsx +15 -2
- package/src/components/checkout/google-pay-button.tsx +9 -2
- package/src/components/checkout/list/list-pix.tsx +19 -1
- package/src/components/checkout/list/pix-banks.tsx +6 -3
- package/src/components/checkout/pix-after-copy.tsx +20 -4
- package/src/components/platform/HomologacaoEvidenceCard.tsx +41 -6
- package/src/components/platform/HomologacaoEvidenceWallets.tsx +296 -0
- package/src/components/platform/copy.ts +27 -0
- package/src/components/platform/en-US.ts +19 -1
- package/src/components/platform/pt-BR.ts +19 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.47.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.38.0",
|
|
27
27
|
"react-qr-code": "^2.2.0"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
@@ -48,10 +48,18 @@ export interface ApplePayPaymentRequest {
|
|
|
48
48
|
total: { label: string; amount: string };
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** The card an authorized payment names, as Apple describes it — e.g. `{ network: 'Visa', displayName: 'Visa 1234' }`. */
|
|
52
|
+
export interface ApplePayPaymentMethod {
|
|
53
|
+
network?: string;
|
|
54
|
+
displayName?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
51
57
|
/** The session events/methods this button drives. */
|
|
52
58
|
export interface ApplePaySessionLike {
|
|
53
59
|
onvalidatemerchant: ((event: { validationURL: string }) => void) | null;
|
|
54
|
-
onpaymentauthorized:
|
|
60
|
+
onpaymentauthorized:
|
|
61
|
+
| ((event: { payment: { token: { paymentData: unknown; paymentMethod?: ApplePayPaymentMethod } } }) => void)
|
|
62
|
+
| null;
|
|
55
63
|
oncancel: (() => void) | null;
|
|
56
64
|
begin(): void;
|
|
57
65
|
abort(): void;
|
|
@@ -157,6 +165,8 @@ export function startApplePaySession(input: {
|
|
|
157
165
|
copy: ApplePayCopy;
|
|
158
166
|
validateMerchant: ((validationURL: string) => Promise<unknown>) | undefined;
|
|
159
167
|
onError: (message: string) => void;
|
|
168
|
+
/** The card the authorized payment names — told before the answer settles (FUT-3755). */
|
|
169
|
+
onPaymentMethod?: (method: ApplePayPaymentMethod | null) => void;
|
|
160
170
|
}): ApplePaySheet {
|
|
161
171
|
const { Session, copy } = input;
|
|
162
172
|
const session = new Session(APPLE_PAY_VERSION, paymentRequest(input.cents, copy));
|
|
@@ -177,7 +187,10 @@ export function startApplePaySession(input: {
|
|
|
177
187
|
.catch(() => fail(copy.cannotComplete));
|
|
178
188
|
};
|
|
179
189
|
// Apple's `token.paymentData`, serialized VERBATIM — PagBank's `key`.
|
|
180
|
-
session.onpaymentauthorized = (event) =>
|
|
190
|
+
session.onpaymentauthorized = (event) => {
|
|
191
|
+
input.onPaymentMethod?.(event.payment.token.paymentMethod ?? null);
|
|
192
|
+
settle(JSON.stringify(event.payment.token.paymentData));
|
|
193
|
+
};
|
|
181
194
|
// Closing the sheet is a choice, not a failure to report.
|
|
182
195
|
session.oncancel = () => settle(null);
|
|
183
196
|
try {
|
|
@@ -36,9 +36,16 @@ export interface GooglePaymentsClient {
|
|
|
36
36
|
loadPaymentData(request: Record<string, unknown>): Promise<GooglePaymentData>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/** The
|
|
39
|
+
/** The paths of the payment data this package reads. */
|
|
40
40
|
export interface GooglePaymentData {
|
|
41
|
-
paymentMethodData: {
|
|
41
|
+
paymentMethodData: {
|
|
42
|
+
tokenizationData: { token: string };
|
|
43
|
+
/**
|
|
44
|
+
* The card the buyer picked, as Google describes it: its network (`VISA`)
|
|
45
|
+
* and its last four digits — what a screen may show of it (FUT-3755).
|
|
46
|
+
*/
|
|
47
|
+
info?: { cardNetwork?: string; cardDetails?: string };
|
|
48
|
+
};
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/** The `google.payments.api` namespace, as far as this button needs it. */
|
|
@@ -300,6 +300,24 @@ function noPick(): void {
|
|
|
300
300
|
// No bank flow: nothing to remember.
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
/**
|
|
304
|
+
* The way to the bank app on the after-copy face, or `undefined` off a phone.
|
|
305
|
+
* With a bank chosen, one line says what to do there, in place of the
|
|
306
|
+
* checklist (FUT-3612) — unless the STORE confirms this Pix: its checklist's
|
|
307
|
+
* "A loja confirma o pagamento — até 14:32" is then the only place that says
|
|
308
|
+
* who confirms and by when, so it stays — under the six banks too. No line in
|
|
309
|
+
* the host's copy: the checklist stays too. Otherwise the six banks replace it.
|
|
310
|
+
*/
|
|
311
|
+
function bankFace(flow: BankFlow | null, pix: PixCharge, stale: boolean): { node: JSX.Element; replacesChecklist: boolean } | undefined {
|
|
312
|
+
if (!flow?.os) return undefined;
|
|
313
|
+
const storeConfirms = pix.confirmation === "MANUAL";
|
|
314
|
+
const oneLine = Boolean(flow.bank.copy.appHint) && !storeConfirms;
|
|
315
|
+
return {
|
|
316
|
+
node: <PixBanks bank={flow.bank} state={flow.state} stale={stale} hint={oneLine} />,
|
|
317
|
+
replacesChecklist: oneLine || (flow.state.chosen === null && !storeConfirms),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
303
321
|
/** Both faces of the list's Pix, and the bar under them. */
|
|
304
322
|
export function ListPixFace(props: ListPixFaceProps & { mode: PaymentListMode }): JSX.Element {
|
|
305
323
|
const { Text } = useCheckoutComponents();
|
|
@@ -311,7 +329,7 @@ export function ListPixFace(props: ListPixFaceProps & { mode: PaymentListMode })
|
|
|
311
329
|
// On a phone the bank flow takes the copy: "Copiar e abrir o Nubank", the
|
|
312
330
|
// toast, the app. Without it — or on a computer — the copy is today's.
|
|
313
331
|
const onCopy = flow ? copies.copy : props.onCopy;
|
|
314
|
-
const banks = flow
|
|
332
|
+
const banks = bankFace(flow, pix, stale);
|
|
315
333
|
return (
|
|
316
334
|
<Box data-testid="list-pix" sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
317
335
|
<Text variant="body" size="md" weight="bold" as="p" style={{ margin: 0 }} data-testid="list-pix-title">
|
|
@@ -182,13 +182,16 @@ function AppHint({ text }: { text: string }): JSX.Element {
|
|
|
182
182
|
);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
/**
|
|
186
|
-
|
|
185
|
+
/**
|
|
186
|
+
* The grid, or the bank being opened — with, when `hint`, the line that says
|
|
187
|
+
* what to do there (the face then drops its checklist; see `ListPixFace`).
|
|
188
|
+
*/
|
|
189
|
+
export function PixBanks({ bank, state, stale, hint }: { bank: CheckoutPixBank; state: PixBankState; stale: boolean; hint: boolean }): JSX.Element {
|
|
187
190
|
if (!state.chosen) return <Grid bank={bank} state={state} stale={stale} />;
|
|
188
191
|
return (
|
|
189
192
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
190
193
|
<Opening bank={bank} state={state} chosen={state.chosen} />
|
|
191
|
-
{bank.copy.appHint ? <AppHint text={bank.copy.appHint} /> : null}
|
|
194
|
+
{hint && bank.copy.appHint ? <AppHint text={bank.copy.appHint} /> : null}
|
|
192
195
|
</Box>
|
|
193
196
|
);
|
|
194
197
|
}
|
|
@@ -126,12 +126,28 @@ interface PixAfterCopyProps {
|
|
|
126
126
|
/**
|
|
127
127
|
* The way to the bank app, on a phone (FUT-3612): drawn where the heading
|
|
128
128
|
* was — the toast already said the code was copied and the bar says what it
|
|
129
|
-
* waits for — and,
|
|
129
|
+
* waits for — and, when `replacesChecklist`, in place of the checklist. The
|
|
130
|
+
* column then sits as tight as the prototype's (8px).
|
|
130
131
|
*/
|
|
131
132
|
banks?: { node: ReactNode; replacesChecklist: boolean };
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* The pair's leading icons. On a phone's card (the pair under 360px) they go,
|
|
137
|
+
* as in the prototype: beside the icon, "Copiar de novo" took two lines in its
|
|
138
|
+
* 159px column ("novo" alone). A computer's column is wider and keeps them.
|
|
139
|
+
* `button` in the selector outranks the library Button's own icon rule, which
|
|
140
|
+
* otherwise wins or loses on style insertion order.
|
|
141
|
+
*/
|
|
142
|
+
const ICON_SLOT = "& button .MuiButton-startIcon, & button .MuiButton-start";
|
|
143
|
+
/** The pair's own width, never the pane's. */
|
|
144
|
+
const ACTIONS_NARROW = "@container pixactions (max-width: 359.95px)";
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The narrow layout's two secondaries side by side — 140px columns, so the pair
|
|
148
|
+
* fits a 390px phone's card (two of 180px did not, and stacked) and stacks only
|
|
149
|
+
* at 320 — wide, the card link joins the row.
|
|
150
|
+
*/
|
|
135
151
|
function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAfterCopyProps, "justCopied" | "onCopyAgain" | "onShowQr" | "onPreferCard">): JSX.Element {
|
|
136
152
|
const { Button } = useCheckoutComponents();
|
|
137
153
|
const copy = useCheckoutCopy().screens.pix;
|
|
@@ -139,7 +155,7 @@ function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAf
|
|
|
139
155
|
// (FUT-3535): copying it again would only hand the buyer the wrong amount.
|
|
140
156
|
const stale = usePaymentStale();
|
|
141
157
|
return (
|
|
142
|
-
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(
|
|
158
|
+
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))", gap: 1.25, "& button": { minHeight: 48, height: "100%" }, "& button, & button *": { fontWeight: 700 }, containerType: "inline-size", containerName: "pixactions", [ACTIONS_NARROW]: { [ICON_SLOT]: { display: "none" } }, [PIX_WIDE]: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1.5, "& button": { height: "auto" } } }}>
|
|
143
159
|
{/* Narrow, a grid cell; wide, its own width beside the link (the prototype's row). */}
|
|
144
160
|
<Box sx={{ minWidth: 0, [PIX_WIDE]: { flex: "none", "& > *": { width: "auto" } } }}>
|
|
145
161
|
<Button variant="outline" color="neutral" size="md" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopyAgain} disabled={stale} dataTestId="pix-copy-again">
|
|
@@ -169,7 +185,7 @@ export function PixAfterCopy(props: PixAfterCopyProps): JSX.Element {
|
|
|
169
185
|
const stale = usePaymentStale();
|
|
170
186
|
return (
|
|
171
187
|
<Box data-testid="pix-after-copy" sx={{ display: "flex", flexWrap: "wrap", gap: 4, alignItems: "flex-start" }}>
|
|
172
|
-
<Box sx={{ flex: "999 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: 2.5 }}>
|
|
188
|
+
<Box sx={{ flex: "999 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: props.banks?.replacesChecklist ? 1 : 2.5 }}>
|
|
173
189
|
{props.banks ? props.banks.node : <Heading words={words} totalLabel={totalLabel} />}
|
|
174
190
|
{props.banks?.replacesChecklist ? null : <Checklist words={words} deadline={deadline} />}
|
|
175
191
|
<Actions {...props} />
|
|
@@ -6,8 +6,10 @@ import { useState, type ReactNode } from 'react';
|
|
|
6
6
|
import type {
|
|
7
7
|
BrowserCardFailure,
|
|
8
8
|
BrowserCardResult,
|
|
9
|
+
BrowserWalletResult,
|
|
9
10
|
DebitTestData,
|
|
10
11
|
EvidenceSummary,
|
|
12
|
+
EvidenceWalletsConfig,
|
|
11
13
|
PaymentEnvironment,
|
|
12
14
|
ThreeDSecureSession,
|
|
13
15
|
} from '@12-apps/payments-backend';
|
|
@@ -15,9 +17,11 @@ import type {
|
|
|
15
17
|
import { maskTestCard, PAGBANK_3DS_TEST_CARD, PAGBANK_TEST_CARDS } from '../../card/pagbank-test-cards';
|
|
16
18
|
import { authenticatePagBankDebit, type ThreeDSFailure } from '../../card/three-ds';
|
|
17
19
|
import { encryptPagBankCard } from '../../card/tokenize';
|
|
20
|
+
import type { GooglePayApi } from '../checkout/google-pay-button';
|
|
18
21
|
import { CARD_SX, TAP_SX } from './ConnectEnvironmentCard';
|
|
19
22
|
import type { PlatformHomologacaoCopy } from './copy';
|
|
20
23
|
import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
|
|
24
|
+
import { EvidenceWalletBlock, useEvidenceWallets, type EvidenceWallets } from './HomologacaoEvidenceWallets';
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* The homologação EVIDENCE card (FUT-3696): connect a sandbox test seller,
|
|
@@ -26,7 +30,9 @@ import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
|
|
|
26
30
|
* The one thing this card does itself is the part that has to happen in a
|
|
27
31
|
* browser: encrypting PagBank's official TEST cards with the seller's public
|
|
28
32
|
* key, through the same SDK call the checkout makes, and authenticating the
|
|
29
|
-
* 3DS test card with PagBank's SDK for the debit scenario (FUT-3755)
|
|
33
|
+
* 3DS test card with PagBank's SDK for the debit scenario (FUT-3755), and
|
|
34
|
+
* opening Google Pay's and Apple Pay's sheets on the operator's tap for the
|
|
35
|
+
* wallet scenarios (`HomologacaoEvidenceWallets.tsx`). Everything else is the
|
|
30
36
|
* host's — each callback is a call to its own mounted routes — and what the
|
|
31
37
|
* file says is decided by the server, from what was recorded. This card shows
|
|
32
38
|
* the counts it answered and never infers a result of its own.
|
|
@@ -42,6 +48,12 @@ export interface HomologacaoSellerView {
|
|
|
42
48
|
accountId: string | null;
|
|
43
49
|
/** Why the latest authorization did not complete, as the evidence recorded it. */
|
|
44
50
|
lastConnectProblem?: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* What each wallet's sheet needs (FUT-3755) — the package's seller view
|
|
53
|
+
* carries it. Absent, no wallet button is drawn and both wallet sections
|
|
54
|
+
* stay pending.
|
|
55
|
+
*/
|
|
56
|
+
wallets?: EvidenceWalletsConfig;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
export interface HomologacaoEvidenceCardProps {
|
|
@@ -64,8 +76,19 @@ export interface HomologacaoEvidenceCardProps {
|
|
|
64
76
|
/** What the debit card is authenticated with — the server's, so the amount matches what it charges. */
|
|
65
77
|
debitTest?: DebitTestData;
|
|
66
78
|
}>;
|
|
67
|
-
/**
|
|
68
|
-
|
|
79
|
+
/**
|
|
80
|
+
* 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).
|
|
82
|
+
*/
|
|
83
|
+
onRun: (runId: string, cards: BrowserCardResult[], wallets: BrowserWalletResult[]) => Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* The host's Apple Pay merchant validation (FUT-3755): exchange Apple's
|
|
86
|
+
* `validationURL` for a merchant session, server-side. Absent, the Apple Pay
|
|
87
|
+
* button is not drawn and its section stays pending.
|
|
88
|
+
*/
|
|
89
|
+
onValidateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
90
|
+
/** Injectable `google.payments.api` for tests and harnesses; omit to load pay.js. */
|
|
91
|
+
googlePayApi?: GooglePayApi | null;
|
|
69
92
|
/** Build and deliver the file; resolves to the counts it holds. */
|
|
70
93
|
onDownload: () => Promise<EvidenceSummary>;
|
|
71
94
|
}
|
|
@@ -206,7 +229,10 @@ function SellerBlock({
|
|
|
206
229
|
}
|
|
207
230
|
|
|
208
231
|
/** One run at a time: which phase it is in, what it answered, where it failed. */
|
|
209
|
-
function useEvidenceRun(
|
|
232
|
+
function useEvidenceRun(
|
|
233
|
+
{ onPrepare, onRun, onDownload }: Pick<HomologacaoEvidenceCardProps, 'onPrepare' | 'onRun' | 'onDownload'>,
|
|
234
|
+
wallets: Pick<EvidenceWallets, 'collect' | 'reset'>,
|
|
235
|
+
) {
|
|
210
236
|
const [phase, setPhase] = useState<Phase>('idle');
|
|
211
237
|
const [summary, setSummary] = useState<EvidenceSummary | null>(null);
|
|
212
238
|
const [failed, setFailed] = useState<ActivePhase | null>(null);
|
|
@@ -222,11 +248,14 @@ function useEvidenceRun({ onPrepare, onRun, onDownload }: Pick<HomologacaoEviden
|
|
|
222
248
|
try {
|
|
223
249
|
if (full) {
|
|
224
250
|
enter('preparing');
|
|
251
|
+
// The sheets ran before this chain: what they answered, taken once.
|
|
252
|
+
const walletResults = wallets.collect();
|
|
253
|
+
wallets.reset();
|
|
225
254
|
const prepared = await onPrepare();
|
|
226
255
|
enter('encrypting');
|
|
227
256
|
const cards = await prepareTestCards(prepared);
|
|
228
257
|
enter('running');
|
|
229
|
-
await onRun(prepared.runId, cards);
|
|
258
|
+
await onRun(prepared.runId, cards, walletResults);
|
|
230
259
|
}
|
|
231
260
|
enter('downloading');
|
|
232
261
|
setSummary(await onDownload());
|
|
@@ -256,7 +285,12 @@ function ConnectOutcome({ seller, connectOutcome }: Pick<HomologacaoEvidenceCard
|
|
|
256
285
|
function EvidenceBody(props: HomologacaoEvidenceCardProps): ReactNode {
|
|
257
286
|
const { seller, onConnect } = props;
|
|
258
287
|
const copy = usePlatformCopy().evidence;
|
|
259
|
-
const
|
|
288
|
+
const wallets = useEvidenceWallets({
|
|
289
|
+
config: seller?.connected ? (seller.wallets ?? null) : null,
|
|
290
|
+
validateApplePayMerchant: props.onValidateApplePayMerchant,
|
|
291
|
+
googlePayApi: props.googlePayApi,
|
|
292
|
+
});
|
|
293
|
+
const { phase, busy, summary, failed, run } = useEvidenceRun(props, wallets);
|
|
260
294
|
return (
|
|
261
295
|
<Stack spacing={1.5} data-testid="homologacao-evidence-card" sx={CARD_SX}>
|
|
262
296
|
<Typography variant="body2" fontWeight={600}>
|
|
@@ -267,6 +301,7 @@ function EvidenceBody(props: HomologacaoEvidenceCardProps): ReactNode {
|
|
|
267
301
|
</Typography>
|
|
268
302
|
<ConnectOutcome seller={seller} connectOutcome={props.connectOutcome} />
|
|
269
303
|
<SellerBlock seller={seller} onConnect={onConnect} />
|
|
304
|
+
<EvidenceWalletBlock wallets={wallets} />
|
|
270
305
|
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1} sx={{ alignItems: { sm: 'center' } }}>
|
|
271
306
|
<Button
|
|
272
307
|
variant={seller?.connected ? 'contained' : 'outlined'}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Box, Stack, Typography } from '@mui/material';
|
|
4
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
BrowserWalletFailure,
|
|
8
|
+
BrowserWalletResult,
|
|
9
|
+
EvidenceWallet,
|
|
10
|
+
EvidenceWalletsConfig,
|
|
11
|
+
} from '@12-apps/payments-backend';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
applePaySessionClass,
|
|
15
|
+
applePaySupported,
|
|
16
|
+
startApplePaySession,
|
|
17
|
+
type ApplePayPaymentMethod,
|
|
18
|
+
type ApplePaySessionClass,
|
|
19
|
+
} from '../checkout/apple-pay-button';
|
|
20
|
+
import {
|
|
21
|
+
paymentDataRequest,
|
|
22
|
+
sheetDismissed,
|
|
23
|
+
useGooglePayClient,
|
|
24
|
+
type GooglePayApi,
|
|
25
|
+
type GooglePaymentData,
|
|
26
|
+
type GooglePaymentsClient,
|
|
27
|
+
} from '../checkout/google-pay-button';
|
|
28
|
+
import type { HomologacaoEvidenceWalletsCopy } from './copy';
|
|
29
|
+
import { usePlatformCopy } from './copy-context';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The wallet step of the evidence card (FUT-3755): Google Pay's and Apple
|
|
33
|
+
* Pay's sheets, opened BEFORE "generate" because each opens only inside the
|
|
34
|
+
* operator's own tap — the browser refuses a sheet started from the async
|
|
35
|
+
* chain "generate" runs. A press keeps the wallet's token in state, the screen
|
|
36
|
+
* says the wallet is ready (network and last four, never the token), and
|
|
37
|
+
* "generate" hands the results to phase 2 with the cards.
|
|
38
|
+
*
|
|
39
|
+
* The sheets are the checkout's own: the same `PaymentsClient` probe and
|
|
40
|
+
* payment request (`google-pay-button.tsx`), the same `ApplePaySession`
|
|
41
|
+
* (`apple-pay-button.tsx`). Google Pay is ALWAYS `TEST` here — the evidence is
|
|
42
|
+
* sandbox-only — and mints against the test seller's PagBank account, as the
|
|
43
|
+
* host's seller view says; Apple Pay is offered only where the device has an
|
|
44
|
+
* `ApplePaySession`, the host has an Apple Pay identity, and the host validates
|
|
45
|
+
* the merchant.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/** The wallets, in the order the evidence file prints them. */
|
|
49
|
+
const WALLETS: readonly EvidenceWallet[] = ['GOOGLE_PAY', 'APPLE_PAY'];
|
|
50
|
+
|
|
51
|
+
type Results = Partial<Record<EvidenceWallet, BrowserWalletResult>>;
|
|
52
|
+
|
|
53
|
+
interface EvidenceWalletsInput {
|
|
54
|
+
/** What each sheet needs, from the host's seller view; null when the host gave none. */
|
|
55
|
+
config: EvidenceWalletsConfig | null;
|
|
56
|
+
/** The host's Apple Pay merchant validation (server-side); absent, Apple Pay is not offered. */
|
|
57
|
+
validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
58
|
+
/** Injectable `google.payments.api` for tests; omit to load pay.js. */
|
|
59
|
+
googlePayApi?: GooglePayApi | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const failed = (type: EvidenceWallet, failure: BrowserWalletFailure): BrowserWalletResult => ({
|
|
63
|
+
type,
|
|
64
|
+
token: null,
|
|
65
|
+
brand: null,
|
|
66
|
+
last4: null,
|
|
67
|
+
failure,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** Four trailing digits, or null — the only part of the number a wallet shows. */
|
|
71
|
+
function lastFour(text: string | undefined): string | null {
|
|
72
|
+
return /(\d{4})\s*$/.exec(text ?? '')?.[1] ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function googleResult(data: GooglePaymentData): BrowserWalletResult {
|
|
76
|
+
const info = data.paymentMethodData.info;
|
|
77
|
+
return {
|
|
78
|
+
type: 'GOOGLE_PAY',
|
|
79
|
+
token: data.paymentMethodData.tokenizationData.token,
|
|
80
|
+
brand: info?.cardNetwork ?? null,
|
|
81
|
+
last4: lastFour(info?.cardDetails),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appleResult(token: string, method: ApplePayPaymentMethod | null): BrowserWalletResult {
|
|
86
|
+
return { type: 'APPLE_PAY', token, brand: method?.network ?? null, last4: lastFour(method?.displayName) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Open Google's sheet — synchronously, inside the tap — and keep what it answers. */
|
|
90
|
+
function runGooglePay(
|
|
91
|
+
client: GooglePaymentsClient,
|
|
92
|
+
google: NonNullable<EvidenceWalletsConfig['googlePay']>,
|
|
93
|
+
cents: number,
|
|
94
|
+
record: (result: BrowserWalletResult) => void,
|
|
95
|
+
): void {
|
|
96
|
+
let loading: Promise<GooglePaymentData>;
|
|
97
|
+
try {
|
|
98
|
+
loading = client.loadPaymentData(paymentDataRequest(google, { totalCents: cents }));
|
|
99
|
+
} catch {
|
|
100
|
+
record(failed('GOOGLE_PAY', 'TOKEN_ERROR'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
loading.then(
|
|
104
|
+
(data) => record(googleResult(data)),
|
|
105
|
+
(error: unknown) => record(failed('GOOGLE_PAY', sheetDismissed(error) ? 'DISMISSED' : 'TOKEN_ERROR')),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Start Apple's session — synchronously, inside the tap — and keep its token.
|
|
111
|
+
* The order is raised later, at "generate", so the sheet is closed as soon as
|
|
112
|
+
* it authorizes: Apple's sheet would otherwise time out waiting for it.
|
|
113
|
+
*/
|
|
114
|
+
function runApplePay(
|
|
115
|
+
Session: ApplePaySessionClass,
|
|
116
|
+
cents: number,
|
|
117
|
+
input: { copy: HomologacaoEvidenceWalletsCopy; validate: (validationURL: string) => Promise<unknown> },
|
|
118
|
+
record: (result: BrowserWalletResult) => void,
|
|
119
|
+
): void {
|
|
120
|
+
const seen: { failure: BrowserWalletFailure | null; method: ApplePayPaymentMethod | null } = { failure: null, method: null };
|
|
121
|
+
const { copy } = input;
|
|
122
|
+
let sheet: ReturnType<typeof startApplePaySession>;
|
|
123
|
+
try {
|
|
124
|
+
sheet = startApplePaySession({
|
|
125
|
+
Session,
|
|
126
|
+
cents,
|
|
127
|
+
copy: {
|
|
128
|
+
orderTotal: copy.applePaySheetTotal,
|
|
129
|
+
cannotStart: copy.applePayCannotStart,
|
|
130
|
+
cannotComplete: copy.applePayCannotComplete,
|
|
131
|
+
payAction: copy.applePayButton,
|
|
132
|
+
},
|
|
133
|
+
validateMerchant: (url) =>
|
|
134
|
+
input.validate(url).catch((error: unknown) => {
|
|
135
|
+
seen.failure = 'MERCHANT_VALIDATION_FAILED';
|
|
136
|
+
throw error;
|
|
137
|
+
}),
|
|
138
|
+
onError: () => {
|
|
139
|
+
seen.failure ??= 'TOKEN_ERROR';
|
|
140
|
+
},
|
|
141
|
+
onPaymentMethod: (method) => {
|
|
142
|
+
seen.method = method;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
record(failed('APPLE_PAY', 'TOKEN_ERROR'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
void sheet.answer.then((key) => {
|
|
150
|
+
if (key === null) return record(failed('APPLE_PAY', seen.failure ?? 'DISMISSED'));
|
|
151
|
+
sheet.complete(true);
|
|
152
|
+
record(appleResult(key, seen.method));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Apple Pay, when this device can show the sheet, the host has an identity, and validates the merchant. */
|
|
157
|
+
function appleOffer(input: EvidenceWalletsInput): { Session: ApplePaySessionClass; validate: (url: string) => Promise<unknown> } | null {
|
|
158
|
+
const validate = input.validateApplePayMerchant;
|
|
159
|
+
if (!input.config?.applePay.configured || !validate || !applePaySupported()) return null;
|
|
160
|
+
const Session = applePaySessionClass();
|
|
161
|
+
return Session ? { Session, validate } : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Why a wallet with no press cannot run here — or undefined when it simply was not pressed. */
|
|
165
|
+
function notOffered(wallet: EvidenceWallet, input: EvidenceWalletsInput, googleReady: boolean): BrowserWalletResult | undefined {
|
|
166
|
+
if (wallet === 'GOOGLE_PAY') return input.config?.googlePay && !googleReady ? failed(wallet, 'NOT_AVAILABLE') : undefined;
|
|
167
|
+
if (!input.config?.applePay.configured) return failed(wallet, 'NOT_CONFIGURED');
|
|
168
|
+
// An identity with no host validation: the sheet could never be validated.
|
|
169
|
+
if (!input.validateApplePayMerchant) return failed(wallet, 'MERCHANT_VALIDATION_FAILED');
|
|
170
|
+
return applePaySupported() ? undefined : failed(wallet, 'NOT_AVAILABLE');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Why Apple Pay has no button on a screen whose host carries wallets — null when it has one, or no wallets at all. */
|
|
174
|
+
function appleAbsence(input: EvidenceWalletsInput, offered: boolean): BrowserWalletFailure | null {
|
|
175
|
+
if (offered || !input.config) return null;
|
|
176
|
+
return notOffered('APPLE_PAY', input, false)?.failure ?? null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The wallet step's state: the sheets' answers, the presses that open them, and what phase 2 gets. */
|
|
180
|
+
export function useEvidenceWallets(input: EvidenceWalletsInput) {
|
|
181
|
+
const copy = usePlatformCopy().evidence.wallets;
|
|
182
|
+
const [results, setResults] = useState<Results>({});
|
|
183
|
+
const google = input.config?.googlePay ?? null;
|
|
184
|
+
// `null` tells the client hook not to load pay.js at all.
|
|
185
|
+
const client = useGooglePayClient(google ? input.googlePayApi : null, 'TEST');
|
|
186
|
+
const apple = appleOffer(input);
|
|
187
|
+
const cents = input.config?.amountCents ?? 0;
|
|
188
|
+
const record = (result: BrowserWalletResult): void => setResults((previous) => ({ ...previous, [result.type]: result }));
|
|
189
|
+
return {
|
|
190
|
+
results,
|
|
191
|
+
google: client && google ? { client, press: () => runGooglePay(client, google, cents, record) } : null,
|
|
192
|
+
apple: apple ? { press: () => runApplePay(apple.Session, cents, { copy, validate: apple.validate }, record) } : null,
|
|
193
|
+
/** Why Apple Pay has no button here, or null when it has one. */
|
|
194
|
+
appleAbsent: appleAbsence(input, apple !== null),
|
|
195
|
+
/** What phase 2 gets: each wallet's answer, or why it could not be offered here. */
|
|
196
|
+
collect: (): BrowserWalletResult[] =>
|
|
197
|
+
WALLETS.flatMap((wallet) => {
|
|
198
|
+
const result = results[wallet] ?? notOffered(wallet, input, client !== null);
|
|
199
|
+
return result ? [result] : [];
|
|
200
|
+
}),
|
|
201
|
+
/** A token pays once: a run that used them clears them. */
|
|
202
|
+
reset: () => setResults({}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type EvidenceWallets = ReturnType<typeof useEvidenceWallets>;
|
|
207
|
+
|
|
208
|
+
/** Google's own button (its brand rule), drawn by `createButton`; the press opens the sheet in the tap. */
|
|
209
|
+
function GooglePayMount({ client, onPress, locale }: { client: GooglePaymentsClient; onPress: () => void; locale: string }): ReactNode {
|
|
210
|
+
const mount = useRef<HTMLDivElement | null>(null);
|
|
211
|
+
const latest = useRef(onPress);
|
|
212
|
+
latest.current = onPress;
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const element = mount.current;
|
|
215
|
+
if (!element) return undefined;
|
|
216
|
+
element.replaceChildren(client.createButton({ onClick: () => latest.current(), buttonSizeMode: 'fill', buttonLocale: locale }));
|
|
217
|
+
return () => element.replaceChildren();
|
|
218
|
+
}, [client, locale]);
|
|
219
|
+
return <Box ref={mount} data-testid="evidence-google-pay" sx={{ minHeight: 40, width: 240, '& > *': { width: '100%' } }} />;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Apple's button: Safari draws it (`-apple-pay-button`); elsewhere the name on a black pill. */
|
|
223
|
+
function ApplePayPress({ label, onPress }: { label: string; onPress: () => void }): ReactNode {
|
|
224
|
+
return (
|
|
225
|
+
<Box
|
|
226
|
+
component="button"
|
|
227
|
+
type="button"
|
|
228
|
+
aria-label={label}
|
|
229
|
+
onClick={onPress}
|
|
230
|
+
data-testid="evidence-apple-pay"
|
|
231
|
+
sx={{
|
|
232
|
+
WebkitAppearance: '-apple-pay-button',
|
|
233
|
+
height: 40,
|
|
234
|
+
width: 240,
|
|
235
|
+
border: 0,
|
|
236
|
+
borderRadius: 1,
|
|
237
|
+
cursor: 'pointer',
|
|
238
|
+
bgcolor: 'common.black',
|
|
239
|
+
color: 'common.white',
|
|
240
|
+
'@supports (-webkit-appearance: -apple-pay-button)': { '& > span': { display: 'none' } },
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
<span aria-hidden>{label}</span>
|
|
244
|
+
</Box>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** One wallet's line under the buttons: ready with its card, or that it answered no token. */
|
|
249
|
+
function WalletLine({ result, copy }: { result: BrowserWalletResult; copy: HomologacaoEvidenceWalletsCopy }): ReactNode {
|
|
250
|
+
const name = copy.names[result.type];
|
|
251
|
+
const card = [result.brand, result.last4 ? `•••• ${result.last4}` : null].filter(Boolean).join(' ');
|
|
252
|
+
return (
|
|
253
|
+
<Typography
|
|
254
|
+
variant="body2"
|
|
255
|
+
role="status"
|
|
256
|
+
color={result.token ? 'text.primary' : 'error.main'}
|
|
257
|
+
data-testid={`evidence-wallet-${result.token ? 'ready' : 'failed'}-${result.type}`}
|
|
258
|
+
>
|
|
259
|
+
{result.token ? copy.ready(name, card) : copy.failed(name)}
|
|
260
|
+
</Typography>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The wallet step: nothing at all when the host's seller view carries no wallets. */
|
|
265
|
+
export function EvidenceWalletBlock({ wallets }: { wallets: EvidenceWallets }): ReactNode {
|
|
266
|
+
const copy = usePlatformCopy().evidence.wallets;
|
|
267
|
+
if (!wallets.google && !wallets.apple && !wallets.appleAbsent) return null;
|
|
268
|
+
const lines = WALLETS.flatMap((wallet) => {
|
|
269
|
+
const result = wallets.results[wallet];
|
|
270
|
+
return result ? [result] : [];
|
|
271
|
+
});
|
|
272
|
+
return (
|
|
273
|
+
<Stack spacing={1} data-testid="evidence-wallets">
|
|
274
|
+
<Typography variant="body2" fontWeight={600}>
|
|
275
|
+
{copy.heading}
|
|
276
|
+
</Typography>
|
|
277
|
+
<Typography variant="caption" component="p" color="text.secondary">
|
|
278
|
+
{copy.hint}
|
|
279
|
+
</Typography>
|
|
280
|
+
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>
|
|
281
|
+
{wallets.google ? (
|
|
282
|
+
<GooglePayMount client={wallets.google.client} onPress={wallets.google.press} locale={copy.googlePayButtonLocale} />
|
|
283
|
+
) : null}
|
|
284
|
+
{wallets.apple ? <ApplePayPress label={copy.applePayButton} onPress={wallets.apple.press} /> : null}
|
|
285
|
+
</Stack>
|
|
286
|
+
{wallets.appleAbsent === 'NOT_CONFIGURED' || wallets.appleAbsent === 'NOT_AVAILABLE' ? (
|
|
287
|
+
<Typography variant="caption" component="p" color="text.secondary" data-testid="evidence-apple-pay-absent">
|
|
288
|
+
{copy.applePayAbsent[wallets.appleAbsent]}
|
|
289
|
+
</Typography>
|
|
290
|
+
) : null}
|
|
291
|
+
{lines.map((result) => (
|
|
292
|
+
<WalletLine key={result.type} result={result} copy={copy} />
|
|
293
|
+
))}
|
|
294
|
+
</Stack>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
@@ -152,6 +152,33 @@ export interface HomologacaoEvidenceCardCopy {
|
|
|
152
152
|
incomplete: string;
|
|
153
153
|
/** Which phase failed — and that the latest recorded file can still be downloaded. */
|
|
154
154
|
generateFailed: (phase: 'preparing' | 'encrypting' | 'running' | 'downloading') => string;
|
|
155
|
+
/** The wallet step before generating (FUT-3755): each sheet opens on the operator's tap. */
|
|
156
|
+
wallets: HomologacaoEvidenceWalletsCopy;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The Google Pay / Apple Pay step of the evidence card (FUT-3755). */
|
|
160
|
+
export interface HomologacaoEvidenceWalletsCopy {
|
|
161
|
+
heading: string;
|
|
162
|
+
/** Each wallet's name, as the ready and failed lines say it. */
|
|
163
|
+
names: { GOOGLE_PAY: string; APPLE_PAY: string };
|
|
164
|
+
/** Why the buttons come before "generate", and that skipping them leaves the sections pending. */
|
|
165
|
+
hint: string;
|
|
166
|
+
/** The language Google draws its own button's label in. */
|
|
167
|
+
googlePayButtonLocale: string;
|
|
168
|
+
/** The Apple Pay button's accessible name. */
|
|
169
|
+
applePayButton: string;
|
|
170
|
+
/** The line Apple's sheet shows above the test amount. */
|
|
171
|
+
applePaySheetTotal: string;
|
|
172
|
+
/** Apple Pay could not start (no merchant validation). */
|
|
173
|
+
applePayCannotStart: string;
|
|
174
|
+
/** Apple Pay started and failed. */
|
|
175
|
+
applePayCannotComplete: string;
|
|
176
|
+
/** Why Apple Pay has no button here: the platform has no Apple Pay identity yet, or this browser has no sheet. */
|
|
177
|
+
applePayAbsent: { NOT_CONFIGURED: string; NOT_AVAILABLE: string };
|
|
178
|
+
/** A wallet holds its token: `card` is the network and last four the wallet showed — never the token. */
|
|
179
|
+
ready: (wallet: string, card: string) => string;
|
|
180
|
+
/** The sheet answered no token; pressing again retries. */
|
|
181
|
+
failed: (wallet: string) => string;
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
/** The whole platform surface, in one object a host passes at the mount. */
|
|
@@ -128,7 +128,7 @@ 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 and
|
|
131
|
+
running: 'Creating the Pix, credit, debit and wallet orders in the sandbox — this can take a few minutes…',
|
|
132
132
|
downloading: 'Building the file…',
|
|
133
133
|
},
|
|
134
134
|
summary: ({ executed, failed, pending }) =>
|
|
@@ -144,5 +144,23 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
|
|
|
144
144
|
downloading: 'Building the file failed.',
|
|
145
145
|
}[phase]
|
|
146
146
|
} Try again; "Download the latest recorded" still returns what was recorded.`,
|
|
147
|
+
wallets: {
|
|
148
|
+
heading: 'Digital wallets (before generating)',
|
|
149
|
+
names: { GOOGLE_PAY: 'Google Pay', APPLE_PAY: 'Apple Pay' },
|
|
150
|
+
hint:
|
|
151
|
+
'Google Pay and Apple Pay open their own sheet, and only on your tap. Tap each one before generating the file; ' +
|
|
152
|
+
"without it, the wallets' sections stay pending. Test only: nothing is charged.",
|
|
153
|
+
googlePayButtonLocale: 'en',
|
|
154
|
+
applePayButton: 'Pay with Apple Pay',
|
|
155
|
+
applePaySheetTotal: 'Homologation test',
|
|
156
|
+
applePayCannotStart: 'Apple Pay is not configured.',
|
|
157
|
+
applePayCannotComplete: 'Apple Pay could not be completed.',
|
|
158
|
+
applePayAbsent: {
|
|
159
|
+
NOT_CONFIGURED: 'Apple Pay: waiting for the Apple Developer account. Its section stays pending.',
|
|
160
|
+
NOT_AVAILABLE: 'Apple Pay: open this screen in Safari on an iPhone or Mac to test it.',
|
|
161
|
+
},
|
|
162
|
+
ready: (wallet, card) => `${wallet} ready${card ? ` — ${card}` : ''}. It will be used when the file is generated.`,
|
|
163
|
+
failed: (wallet) => `${wallet} returned no token. Tap again to retry.`,
|
|
164
|
+
},
|
|
147
165
|
},
|
|
148
166
|
};
|
|
@@ -109,7 +109,7 @@ 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 e
|
|
112
|
+
running: 'Criando os pedidos Pix, crédito, débito e carteiras no sandbox — pode levar alguns minutos…',
|
|
113
113
|
downloading: 'Montando o arquivo…',
|
|
114
114
|
},
|
|
115
115
|
summary: ({ executed, failed, pending }) =>
|
|
@@ -128,5 +128,23 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
|
|
|
128
128
|
downloading: 'Falhou ao montar o arquivo.',
|
|
129
129
|
}[phase]
|
|
130
130
|
} Tente de novo; "Baixar o último registrado" ainda traz o que já foi registrado.`,
|
|
131
|
+
wallets: {
|
|
132
|
+
heading: 'Carteiras digitais (antes de gerar)',
|
|
133
|
+
names: { GOOGLE_PAY: 'Google Pay', APPLE_PAY: 'Apple Pay' },
|
|
134
|
+
hint:
|
|
135
|
+
'O Google Pay e o Apple Pay abrem a própria folha e só com o seu toque. Toque em cada um antes de gerar o arquivo; ' +
|
|
136
|
+
'sem isso, as seções das carteiras ficam pendentes. Somente teste: nada é cobrado.',
|
|
137
|
+
googlePayButtonLocale: 'pt',
|
|
138
|
+
applePayButton: 'Pagar com Apple Pay',
|
|
139
|
+
applePaySheetTotal: 'Teste de homologação',
|
|
140
|
+
applePayCannotStart: 'O Apple Pay não está configurado.',
|
|
141
|
+
applePayCannotComplete: 'Não foi possível concluir o Apple Pay.',
|
|
142
|
+
applePayAbsent: {
|
|
143
|
+
NOT_CONFIGURED: 'Apple Pay: aguardando a conta Apple Developer. A seção fica pendente.',
|
|
144
|
+
NOT_AVAILABLE: 'Apple Pay: abra esta tela no Safari de um iPhone ou Mac para testar.',
|
|
145
|
+
},
|
|
146
|
+
ready: (wallet, card) => `${wallet} pronto${card ? ` — ${card}` : ''}. Será usado ao gerar o arquivo.`,
|
|
147
|
+
failed: (wallet) => `O ${wallet} não devolveu o token. Toque de novo para tentar.`,
|
|
148
|
+
},
|
|
131
149
|
},
|
|
132
150
|
};
|