@12-apps/payments-frontend 3.31.2 → 3.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/checkout/checkout-flow.tsx +3 -0
- package/src/components/checkout/en-US.ts +10 -0
- package/src/components/checkout/payment-status-again.tsx +81 -0
- package/src/components/checkout/payment-status-parts.tsx +33 -32
- package/src/components/checkout/payment-status.tsx +8 -0
- package/src/components/checkout/pix-view.tsx +40 -2
- package/src/components/checkout/pt-BR.ts +10 -0
- package/src/components/checkout/screens-copy.ts +7 -0
- package/src/components/checkout/screens-en-US.ts +5 -0
- package/src/components/checkout/screens-pt-BR.ts +5 -0
- package/src/components/checkout/view-copy.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.33.0",
|
|
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.",
|
|
@@ -233,6 +233,9 @@ function StatusStep({
|
|
|
233
233
|
// FAILED only, where it picks the sentence and decides whether a retry
|
|
234
234
|
// could work at all.
|
|
235
235
|
decline={c.decline}
|
|
236
|
+
// A Pix the store confirms by hand ends without proof either way
|
|
237
|
+
// (FUT-3232), so its refusal and lapse get their own sentences.
|
|
238
|
+
manual={c.order?.pix?.confirmation === "MANUAL"}
|
|
236
239
|
/>
|
|
237
240
|
);
|
|
238
241
|
}
|
|
@@ -34,6 +34,16 @@ export const EN_US_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
|
|
|
34
34
|
heading: "The code expired",
|
|
35
35
|
support: "Nothing was charged. Generate a new code to carry on.",
|
|
36
36
|
},
|
|
37
|
+
manual: {
|
|
38
|
+
failed: {
|
|
39
|
+
heading: "The store did not find your Pix",
|
|
40
|
+
support: "If you have already paid, talk to the store before paying again.",
|
|
41
|
+
},
|
|
42
|
+
expired: {
|
|
43
|
+
heading: "The store did not confirm the payment in time",
|
|
44
|
+
support: "If you have already paid, talk to the store before paying again.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
37
47
|
awaitingTimedOut: {
|
|
38
48
|
heading: "We have not had the confirmation yet",
|
|
39
49
|
support:
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CheckoutDecline } from "./decline";
|
|
4
|
+
import type { OrderStatus } from "./types";
|
|
5
|
+
import { useCheckoutComponents } from "./ui";
|
|
6
|
+
import type { PaymentStatusCopy } from "./view-copy";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The confirmation screen's "pay again" action — retry or regenerate — split
|
|
10
|
+
* out of `./payment-status-parts.tsx` when the Pix the store confirms
|
|
11
|
+
* (FUT-3232) gave it a second look and took that file past its size gate.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Whether "Tentar novamente" may be offered for a refusal (FUT-1145).
|
|
16
|
+
*
|
|
17
|
+
* `retriable === false` is the provider's OWN verdict that another attempt with
|
|
18
|
+
* this instrument cannot succeed — attempts exhausted (10001), a cancelled
|
|
19
|
+
* recurring mandate (20118), a malformed request. Offering a retry there is
|
|
20
|
+
* offering a button that mints another failed order and shows the same screen
|
|
21
|
+
* again; on a card the issuer is already counting, it is worse than useless.
|
|
22
|
+
*
|
|
23
|
+
* SILENCE MEANS YES. An undefined verdict is a provider that offered no
|
|
24
|
+
* guidance, not a refusal to retry, and withholding the button on silence
|
|
25
|
+
* would strand a buyer whose card is fine.
|
|
26
|
+
*/
|
|
27
|
+
function retryable(decline: CheckoutDecline | null): boolean {
|
|
28
|
+
return decline?.retriable !== false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* How "Tentar novamente" / "Gerar novo código" look. Under a Pix the store
|
|
33
|
+
* confirms they step back to outline: the sentence above them asks a buyer who
|
|
34
|
+
* may already have paid to talk to the store first, and a filled button would
|
|
35
|
+
* say the opposite.
|
|
36
|
+
*/
|
|
37
|
+
function againLook(byStore: boolean): { variant: "solid" | "outline"; color: "primary" | "neutral" } {
|
|
38
|
+
return byStore ? { variant: "outline", color: "neutral" } : { variant: "solid", color: "primary" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** "Tentar novamente" on a retryable FAILED, "Gerar novo código" on EXPIRED, else nothing. */
|
|
42
|
+
export function PayAgain({
|
|
43
|
+
copy,
|
|
44
|
+
status,
|
|
45
|
+
decline,
|
|
46
|
+
onRetry,
|
|
47
|
+
onRegenerate,
|
|
48
|
+
byStore,
|
|
49
|
+
}: {
|
|
50
|
+
copy: PaymentStatusCopy;
|
|
51
|
+
status: OrderStatus;
|
|
52
|
+
decline: CheckoutDecline | null;
|
|
53
|
+
onRetry?: () => void;
|
|
54
|
+
onRegenerate?: () => void;
|
|
55
|
+
byStore: boolean;
|
|
56
|
+
}): JSX.Element | null {
|
|
57
|
+
const { Button } = useCheckoutComponents();
|
|
58
|
+
const again = againLook(byStore);
|
|
59
|
+
if (status === "FAILED" && onRetry && retryable(decline)) {
|
|
60
|
+
return (
|
|
61
|
+
<Button variant={again.variant} color={again.color} size="lg" onClick={onRetry} dataTestId="payment-retry">
|
|
62
|
+
{copy.retryAction}
|
|
63
|
+
</Button>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (status === "EXPIRED" && onRegenerate) {
|
|
67
|
+
return (
|
|
68
|
+
<Button
|
|
69
|
+
variant={again.variant}
|
|
70
|
+
color={again.color}
|
|
71
|
+
size="lg"
|
|
72
|
+
onClick={onRegenerate}
|
|
73
|
+
dataTestId="payment-regenerate"
|
|
74
|
+
>
|
|
75
|
+
{copy.regenerateAction}
|
|
76
|
+
</Button>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
@@ -4,6 +4,7 @@ import type { JSX, ReactNode } from "react";
|
|
|
4
4
|
import { CheckCircleOutlineIcon, ErrorOutlineIcon, ScheduleIcon } from "./icons";
|
|
5
5
|
import type { CheckoutDecline } from "./decline";
|
|
6
6
|
import type { OrderStatus } from "./types";
|
|
7
|
+
import { PayAgain } from "./payment-status-again";
|
|
7
8
|
import { useCheckoutComponents } from "./ui";
|
|
8
9
|
import type { PaymentStatusCopy, StatusOutcomeCopy } from "./view-copy";
|
|
9
10
|
|
|
@@ -120,6 +121,23 @@ function failedOutcome(copy: PaymentStatusCopy, decline: CheckoutDecline | null)
|
|
|
120
121
|
return (reason ? copy.declined?.[reason] : undefined) ?? copy.failed;
|
|
121
122
|
}
|
|
122
123
|
|
|
124
|
+
/**
|
|
125
|
+
* A SETTLED outcome's sentences. A Pix only the store confirms swaps FAILED and
|
|
126
|
+
* EXPIRED for its own pair when the host wrote one (`copy.manual`): neither the
|
|
127
|
+
* store's "não recebi" nor its lapsed window proves no money moved.
|
|
128
|
+
*/
|
|
129
|
+
function settledOutcome(
|
|
130
|
+
copy: PaymentStatusCopy,
|
|
131
|
+
status: OrderStatus,
|
|
132
|
+
decline: CheckoutDecline | null,
|
|
133
|
+
manual: boolean,
|
|
134
|
+
): StatusOutcomeCopy {
|
|
135
|
+
const byStore = manual ? copy.manual : undefined;
|
|
136
|
+
if (status === "FAILED") return byStore?.failed ?? failedOutcome(copy, decline);
|
|
137
|
+
if (status === "EXPIRED") return byStore?.expired ?? copy.expired;
|
|
138
|
+
return copy[OUTCOME_COPY_KEY[status]];
|
|
139
|
+
}
|
|
140
|
+
|
|
123
141
|
/**
|
|
124
142
|
* The headline block: icon, outcome, and one supporting line.
|
|
125
143
|
*
|
|
@@ -135,23 +153,22 @@ export function OutcomeHero({
|
|
|
135
153
|
wait,
|
|
136
154
|
decline,
|
|
137
155
|
hero,
|
|
156
|
+
manual = false,
|
|
138
157
|
}: {
|
|
139
158
|
copy: PaymentStatusCopy;
|
|
140
159
|
status: OrderStatus;
|
|
141
160
|
wait: WaitState;
|
|
142
161
|
decline: CheckoutDecline | null;
|
|
143
162
|
hero?: ReactNode;
|
|
163
|
+
/** The payment was a Pix only the store confirms — see {@link settledOutcome}. */
|
|
164
|
+
manual?: boolean;
|
|
144
165
|
}): JSX.Element {
|
|
145
166
|
const { Text } = useCheckoutComponents();
|
|
146
167
|
const face = status === "AWAITING_PAYMENT" ? awaitingFace(copy, wait) : null;
|
|
147
168
|
const visual = face
|
|
148
169
|
? { icon: OUTCOME_VISUAL.AWAITING_PAYMENT.icon, tone: face.tone }
|
|
149
170
|
: OUTCOME_VISUAL[status];
|
|
150
|
-
const outcome = face
|
|
151
|
-
? face.outcome
|
|
152
|
-
: status === "FAILED"
|
|
153
|
-
? failedOutcome(copy, decline)
|
|
154
|
-
: copy[OUTCOME_COPY_KEY[status]];
|
|
171
|
+
const outcome = face ? face.outcome : settledOutcome(copy, status, decline, manual);
|
|
155
172
|
return (
|
|
156
173
|
<Box
|
|
157
174
|
// `payment-paid` is load-bearing for the storefront journeys — it is how
|
|
@@ -233,23 +250,6 @@ export function PaidFacts({
|
|
|
233
250
|
);
|
|
234
251
|
}
|
|
235
252
|
|
|
236
|
-
/**
|
|
237
|
-
* Whether "Tentar novamente" may be offered for a refusal (FUT-1145).
|
|
238
|
-
*
|
|
239
|
-
* `retriable === false` is the provider's OWN verdict that another attempt with
|
|
240
|
-
* this instrument cannot succeed — attempts exhausted (10001), a cancelled
|
|
241
|
-
* recurring mandate (20118), a malformed request. Offering a retry there is
|
|
242
|
-
* offering a button that mints another failed order and shows the same screen
|
|
243
|
-
* again; on a card the issuer is already counting, it is worse than useless.
|
|
244
|
-
*
|
|
245
|
-
* SILENCE MEANS YES. An undefined verdict is a provider that offered no
|
|
246
|
-
* guidance, not a refusal to retry, and withholding the button on silence
|
|
247
|
-
* would strand a buyer whose card is fine.
|
|
248
|
-
*/
|
|
249
|
-
function retryable(decline: CheckoutDecline | null): boolean {
|
|
250
|
-
return decline?.retriable !== false;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
253
|
/**
|
|
254
254
|
* How much of the eye the way out asks for on a PAID confirmation.
|
|
255
255
|
*
|
|
@@ -293,6 +293,7 @@ export function StatusActions({
|
|
|
293
293
|
onNotPaid,
|
|
294
294
|
onBackToMenu,
|
|
295
295
|
backActionEmphasis = "primary",
|
|
296
|
+
manual,
|
|
296
297
|
}: {
|
|
297
298
|
copy: PaymentStatusCopy;
|
|
298
299
|
status: OrderStatus;
|
|
@@ -310,6 +311,8 @@ export function StatusActions({
|
|
|
310
311
|
onBackToMenu: () => void;
|
|
311
312
|
/** Whether back-to-menu leads a PAID screen. See {@link BackActionEmphasis}. */
|
|
312
313
|
backActionEmphasis?: BackActionEmphasis;
|
|
314
|
+
/** The payment was a Pix only the store confirms — see `PayAgain`. */
|
|
315
|
+
manual?: boolean;
|
|
313
316
|
}): JSX.Element {
|
|
314
317
|
const { Button } = useCheckoutComponents();
|
|
315
318
|
const back = backLook(status, backActionEmphasis);
|
|
@@ -337,16 +340,14 @@ export function StatusActions({
|
|
|
337
340
|
{copy.notPaidAction}
|
|
338
341
|
</Button>
|
|
339
342
|
) : null}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
</Button>
|
|
349
|
-
) : null}
|
|
343
|
+
<PayAgain
|
|
344
|
+
copy={copy}
|
|
345
|
+
status={status}
|
|
346
|
+
decline={decline}
|
|
347
|
+
onRetry={onRetry}
|
|
348
|
+
onRegenerate={onRegenerate}
|
|
349
|
+
byStore={manual === true}
|
|
350
|
+
/>
|
|
350
351
|
<Button
|
|
351
352
|
// Full width and last, so the thumb lands on the same place in every
|
|
352
353
|
// outcome instead of hunting a button that moves with the state.
|
|
@@ -117,6 +117,12 @@ interface PaymentStatusProps {
|
|
|
117
117
|
* sentence, and it decides whether a retry is offered at all.
|
|
118
118
|
*/
|
|
119
119
|
decline?: CheckoutDecline | null;
|
|
120
|
+
/**
|
|
121
|
+
* The payment was a Pix only the STORE confirms (FUT-3232): a FAILED or
|
|
122
|
+
* EXPIRED screen then says the store did not find it, never that nothing
|
|
123
|
+
* was charged.
|
|
124
|
+
*/
|
|
125
|
+
manual?: boolean;
|
|
120
126
|
}
|
|
121
127
|
|
|
122
128
|
/**
|
|
@@ -198,6 +204,7 @@ export function PaymentStatus(props: PaymentStatusProps): JSX.Element {
|
|
|
198
204
|
wait={view.wait}
|
|
199
205
|
decline={view.decline}
|
|
200
206
|
hero={props.paidHero}
|
|
207
|
+
manual={props.manual === true}
|
|
201
208
|
/>
|
|
202
209
|
|
|
203
210
|
{view.paid ? (
|
|
@@ -220,6 +227,7 @@ export function PaymentStatus(props: PaymentStatusProps): JSX.Element {
|
|
|
220
227
|
onNotPaid={view.notPaid}
|
|
221
228
|
onBackToMenu={onBackToMenu}
|
|
222
229
|
backActionEmphasis={props.backActionEmphasis}
|
|
230
|
+
manual={props.manual === true}
|
|
223
231
|
/>
|
|
224
232
|
|
|
225
233
|
{view.paid ? props.paidFooter : null}
|
|
@@ -66,7 +66,7 @@ function pixWaitMs(expiresAt: string | undefined): number {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/** The copyable "copia e cola" strip with its copy button. */
|
|
69
|
-
function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
|
|
69
|
+
function PixCodeBox({ pix, onCopied }: { pix: PixCharge; onCopied: () => void }): JSX.Element {
|
|
70
70
|
const { Button, Text } = useCheckoutComponents();
|
|
71
71
|
const copy = useCheckoutCopy().screens.pix;
|
|
72
72
|
const [copied, setCopied] = useState(false);
|
|
@@ -75,6 +75,7 @@ function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
|
|
|
75
75
|
try {
|
|
76
76
|
await navigator.clipboard?.writeText(pix.copyPaste);
|
|
77
77
|
setCopied(true);
|
|
78
|
+
onCopied();
|
|
78
79
|
setTimeout(() => setCopied(false), 2000);
|
|
79
80
|
} catch {
|
|
80
81
|
/* clipboard unavailable — the code is still visible to copy manually */
|
|
@@ -200,6 +201,43 @@ function PixPollFooter({
|
|
|
200
201
|
);
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
/**
|
|
205
|
+
* What a store-confirmed Pix says once its code is copied (FUT-3232).
|
|
206
|
+
*
|
|
207
|
+
* The footer's caption already names the store's wait, but in a grey line
|
|
208
|
+
* under the QR that a buyer on their way to the bank app does not read. Copying
|
|
209
|
+
* is that moment, so it answers with a notice that stays: the wait can run for
|
|
210
|
+
* minutes, and "volte para esta tela" is the instruction that keeps the buyer
|
|
211
|
+
* where the confirmation will land.
|
|
212
|
+
*/
|
|
213
|
+
function ManualCopiedNotice({ manual, copied }: { manual: boolean; copied: boolean }): JSX.Element | null {
|
|
214
|
+
const { Alert } = useCheckoutComponents();
|
|
215
|
+
const notice = useCheckoutCopy().screens.pix.manual?.copied;
|
|
216
|
+
if (!manual || !copied || !notice) return null;
|
|
217
|
+
return (
|
|
218
|
+
<Box sx={{ width: "100%", maxWidth: 420, textAlign: "left" }}>
|
|
219
|
+
<Alert
|
|
220
|
+
variant="info"
|
|
221
|
+
title={notice.title}
|
|
222
|
+
description={notice.description}
|
|
223
|
+
showIcon
|
|
224
|
+
data-testid="pix-manual-copied"
|
|
225
|
+
/>
|
|
226
|
+
</Box>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** The code strip and, once it has been copied, what that copy means. */
|
|
231
|
+
function PixCopyRow({ pix, manual }: { pix: PixCharge; manual: boolean }): JSX.Element {
|
|
232
|
+
const [copied, setCopied] = useState(false);
|
|
233
|
+
return (
|
|
234
|
+
<>
|
|
235
|
+
<PixCodeBox pix={pix} onCopied={() => setCopied(true)} />
|
|
236
|
+
<ManualCopiedNotice manual={manual} copied={copied} />
|
|
237
|
+
</>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
203
241
|
/**
|
|
204
242
|
* The pane's two sentences that depend on who confirms. A store-confirmed Pix
|
|
205
243
|
* must not say "a confirmação é automática": nothing confirms it but the store.
|
|
@@ -279,7 +317,7 @@ export function PixView({
|
|
|
279
317
|
<QRCode value={pix.copyPaste} size={200} />
|
|
280
318
|
</Box>
|
|
281
319
|
|
|
282
|
-
<
|
|
320
|
+
<PixCopyRow pix={pix} manual={manual} />
|
|
283
321
|
|
|
284
322
|
<Text variant="caption" size="xs" color="secondary" as="p" data-testid="pix-expiry">
|
|
285
323
|
{sentences.validUntil(validUntil)}
|
|
@@ -28,6 +28,16 @@ export const PT_BR_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
|
|
|
28
28
|
heading: "O código expirou",
|
|
29
29
|
support: "Nenhum valor foi cobrado. Gere um novo código para continuar.",
|
|
30
30
|
},
|
|
31
|
+
manual: {
|
|
32
|
+
failed: {
|
|
33
|
+
heading: "A loja não identificou o seu Pix",
|
|
34
|
+
support: "Se você já pagou, fale com a loja antes de pagar de novo.",
|
|
35
|
+
},
|
|
36
|
+
expired: {
|
|
37
|
+
heading: "A loja não confirmou o pagamento a tempo",
|
|
38
|
+
support: "Se você já pagou, fale com a loja antes de pagar de novo.",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
31
41
|
awaitingTimedOut: {
|
|
32
42
|
heading: "Ainda não recebemos a confirmação",
|
|
33
43
|
support:
|
|
@@ -123,6 +123,13 @@ export interface PixPaneCopy {
|
|
|
123
123
|
instructions(totalLabel: string): string;
|
|
124
124
|
validUntil(time: string): string;
|
|
125
125
|
awaiting: string;
|
|
126
|
+
/**
|
|
127
|
+
* Shown once the buyer copies the code, and kept: copying is the moment
|
|
128
|
+
* they leave for their bank app, so it is when they must learn the wait
|
|
129
|
+
* that follows is the STORE's, and that the answer arrives on this screen.
|
|
130
|
+
* Optional so a host's own copy without it simply shows no notice.
|
|
131
|
+
*/
|
|
132
|
+
copied?: { title: string; description: string };
|
|
126
133
|
};
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -57,6 +57,11 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
57
57
|
`Pay ${totalLabel} with the QR code or the code below. The store checks it arrived and confirms your order.`,
|
|
58
58
|
validUntil: (time) => `The store confirms the payment by ${time}.`,
|
|
59
59
|
awaiting: 'Waiting for the store to confirm the payment…',
|
|
60
|
+
copied: {
|
|
61
|
+
title: 'Code copied. Now pay in your bank app',
|
|
62
|
+
description:
|
|
63
|
+
'Once you have paid, come back to this screen and wait: the store checks it arrived and confirms your order right here.',
|
|
64
|
+
},
|
|
60
65
|
},
|
|
61
66
|
},
|
|
62
67
|
card: {
|
|
@@ -46,6 +46,11 @@ export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
46
46
|
`Pague ${totalLabel} com o QR code ou o código abaixo. A loja confere o recebimento e confirma o seu pedido.`,
|
|
47
47
|
validUntil: (time) => `A loja confirma o pagamento até ${time}.`,
|
|
48
48
|
awaiting: 'Aguardando confirmação do pagamento pela loja…',
|
|
49
|
+
copied: {
|
|
50
|
+
title: 'Código copiado. Agora pague no app do seu banco',
|
|
51
|
+
description:
|
|
52
|
+
'Depois de pagar, volte para esta tela e aguarde: a loja confere o recebimento e confirma o seu pedido aqui mesmo.',
|
|
53
|
+
},
|
|
49
54
|
},
|
|
50
55
|
},
|
|
51
56
|
card: {
|
|
@@ -93,6 +93,14 @@ export interface PaymentStatusCopy {
|
|
|
93
93
|
*/
|
|
94
94
|
declined: Partial<Record<CheckoutDeclineReason, StatusOutcomeCopy>>;
|
|
95
95
|
awaitingTimedOut: StatusOutcomeCopy;
|
|
96
|
+
/**
|
|
97
|
+
* FAILED and EXPIRED for a Pix only the STORE confirms (FUT-3232). The store
|
|
98
|
+
* answering "não recebi", or letting its window lapse, says nothing about
|
|
99
|
+
* whether the buyer paid — a static key takes money whatever the store
|
|
100
|
+
* later says — so "nothing was charged" would be a guess, and "pay again" an
|
|
101
|
+
* invitation to pay twice. Absent, those two keep their ordinary sentences.
|
|
102
|
+
*/
|
|
103
|
+
manual?: { failed: StatusOutcomeCopy; expired: StatusOutcomeCopy };
|
|
96
104
|
/**
|
|
97
105
|
* The wait cannot reach the payment right now (FUT-1144) — and is STILL
|
|
98
106
|
* ASKING, which is the whole difference between this outcome and the one
|