@12-apps/payments-frontend 3.19.0 → 3.20.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/card-view.tsx +30 -28
- package/src/components/checkout/checkout-flow.tsx +40 -20
- package/src/components/checkout/en-US.ts +9 -0
- package/src/components/checkout/one-click.tsx +179 -0
- package/src/components/checkout/payment-status.tsx +130 -38
- package/src/components/checkout/pix-view.tsx +34 -12
- package/src/components/checkout/pt-BR.ts +7 -0
- package/src/components/checkout/screens-copy.ts +19 -0
- package/src/components/checkout/screens-en-US.ts +4 -0
- package/src/components/checkout/screens-pt-BR.ts +2 -0
- package/src/components/checkout/stalled-wait.tsx +58 -0
- package/src/components/checkout/use-card-checkout.ts +53 -11
- package/src/components/checkout/use-checkout-controller.ts +48 -13
- package/src/components/checkout/use-payment-polling.ts +250 -64
- package/src/components/checkout/use-wallet-charge.ts +20 -6
- package/src/components/checkout/view-copy.ts +15 -0
- package/src/components/checkout/wallet-pane.tsx +17 -16
- package/src/flows/copy.ts +19 -0
- package/src/flows/screens-hosted.tsx +72 -17
|
@@ -4,6 +4,7 @@ import QRCode from "react-qr-code";
|
|
|
4
4
|
|
|
5
5
|
import { useCheckoutCopy } from "./copy-context";
|
|
6
6
|
import { ContentCopyIcon } from "./icons";
|
|
7
|
+
import { StalledWait } from "./stalled-wait";
|
|
7
8
|
import type { CheckoutOrder, OrderStatus, PixCharge } from "./types";
|
|
8
9
|
import { useCheckoutComponents } from "./ui";
|
|
9
10
|
import { usePaymentPolling } from "./use-payment-polling";
|
|
@@ -71,19 +72,38 @@ function PixCodeBox({ pix }: { pix: PixCharge }): JSX.Element {
|
|
|
71
72
|
);
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The live footer: the pulsing "awaiting payment" indicator, or — while the
|
|
77
|
+
* poll cannot reach us — the same wait said out loud, with a way to hurry it.
|
|
78
|
+
*
|
|
79
|
+
* A WARNING rather than a danger (FUT-1144). The old red panel said "não foi
|
|
80
|
+
* possível confirmar o pagamento" and meant it: four consecutive failures ended
|
|
81
|
+
* the wait, so a shopper who paid during a ten-second blip watched a QR under a
|
|
82
|
+
* final-sounding refusal that would never update. The QR is still good, the
|
|
83
|
+
* wait is still running, and the sentence now says both.
|
|
84
|
+
*/
|
|
85
|
+
function PixPollFooter({
|
|
86
|
+
error,
|
|
87
|
+
onCheckAgain,
|
|
88
|
+
}: {
|
|
89
|
+
error: string | null;
|
|
90
|
+
onCheckAgain: () => void;
|
|
91
|
+
}): JSX.Element {
|
|
92
|
+
const { Text } = useCheckoutComponents();
|
|
77
93
|
const { pix, settling } = useCheckoutCopy().screens;
|
|
78
94
|
if (error) {
|
|
95
|
+
// The same panel the card and wallet panes show, held to the width of the
|
|
96
|
+
// copy-and-paste strip above it so the centred PIX column stays a column.
|
|
79
97
|
return (
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
<Box sx={{ width: "100%", maxWidth: 420 }}>
|
|
99
|
+
<StalledWait
|
|
100
|
+
title={settling.connectionLost}
|
|
101
|
+
description={error}
|
|
102
|
+
onCheckAgain={onCheckAgain}
|
|
103
|
+
testId="pix-poll-error"
|
|
104
|
+
actionTestId="pix-check-again"
|
|
105
|
+
/>
|
|
106
|
+
</Box>
|
|
87
107
|
);
|
|
88
108
|
}
|
|
89
109
|
return (
|
|
@@ -107,7 +127,9 @@ export function PixView({
|
|
|
107
127
|
}): JSX.Element {
|
|
108
128
|
const { Text } = useCheckoutComponents();
|
|
109
129
|
const copy = useCheckoutCopy().screens.pix;
|
|
110
|
-
const { status, error } = usePaymentPolling(order.orderId, {
|
|
130
|
+
const { status, error, checkAgain } = usePaymentPolling(order.orderId, {
|
|
131
|
+
intervalMs: pollIntervalMs,
|
|
132
|
+
});
|
|
111
133
|
|
|
112
134
|
// Bubble a terminal status up once, so the parent can advance to the status step.
|
|
113
135
|
useEffect(() => {
|
|
@@ -160,7 +182,7 @@ export function PixView({
|
|
|
160
182
|
{copy.validUntil(validUntil)}
|
|
161
183
|
</Text>
|
|
162
184
|
|
|
163
|
-
<PixPollFooter error={error} />
|
|
185
|
+
<PixPollFooter error={error} onCheckAgain={checkAgain} />
|
|
164
186
|
</Box>
|
|
165
187
|
);
|
|
166
188
|
}
|
|
@@ -34,8 +34,15 @@ export const PT_BR_PAYMENT_STATUS_COPY: PaymentStatusCopy = {
|
|
|
34
34
|
"Se você já pagou, o pedido é confirmado assim que a operadora avisar — " +
|
|
35
35
|
"não pague de novo. Você pode fechar esta tela.",
|
|
36
36
|
},
|
|
37
|
+
awaitingUnreachable: {
|
|
38
|
+
heading: "Não conseguimos falar com o pagamento agora",
|
|
39
|
+
support:
|
|
40
|
+
"Continuamos tentando por aqui. Se você já pagou, não pague de novo — " +
|
|
41
|
+
"o pedido é confirmado assim que a operadora avisar.",
|
|
42
|
+
},
|
|
37
43
|
retryAction: "Tentar novamente",
|
|
38
44
|
regenerateAction: "Gerar novo código",
|
|
45
|
+
checkAgainAction: "Verificar de novo",
|
|
39
46
|
backAction: "Voltar ao cardápio",
|
|
40
47
|
amountLabel: "Valor pago",
|
|
41
48
|
referenceLabel: "Pedido",
|
|
@@ -61,6 +61,25 @@ export interface SettlingCopy {
|
|
|
61
61
|
confirming: string;
|
|
62
62
|
/** It came back refused. */
|
|
63
63
|
cannotPay: string;
|
|
64
|
+
/**
|
|
65
|
+
* We cannot reach the payment right now — a dropped connection, a handset
|
|
66
|
+
* moving between Wi-Fi and 4G, a browser that aborted our requests while the
|
|
67
|
+
* shopper was in their bank app (FUT-1144).
|
|
68
|
+
*
|
|
69
|
+
* TRANSIENT, and the sentence must say so: the screen is still asking, on a
|
|
70
|
+
* backoff, and it re-asks the moment the tab comes back or the signal
|
|
71
|
+
* returns. This was `cannotConfirm` — "não foi possível confirmar o
|
|
72
|
+
* pagamento" — under which the wait had actually STOPPED, so a shopper who
|
|
73
|
+
* had paid read a final-sounding refusal and was never told otherwise.
|
|
74
|
+
*/
|
|
75
|
+
connectionLost: string;
|
|
76
|
+
/**
|
|
77
|
+
* Ask again, now. Offered beside {@link connectionLost} and beside the
|
|
78
|
+
* elapsed wait, because a shopper watching a screen that cannot reach us
|
|
79
|
+
* needs something to press — and because pressing it is what restarts a wait
|
|
80
|
+
* that has run out.
|
|
81
|
+
*/
|
|
82
|
+
checkAgainAction: string;
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
/** The PIX pane: the QR, the copyable code, and the wait. */
|
|
@@ -35,6 +35,10 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
35
35
|
processing: 'Processing payment…',
|
|
36
36
|
confirming: 'We are confirming your payment',
|
|
37
37
|
cannotPay: 'Could not pay',
|
|
38
|
+
// "we keep trying" is the load-bearing half: the wait has not ended, and a
|
|
39
|
+
// shopper who reads a final-sounding refusal pays a second time.
|
|
40
|
+
connectionLost: 'No connection right now — we keep trying',
|
|
41
|
+
checkAgainAction: 'Check again',
|
|
38
42
|
},
|
|
39
43
|
pix: {
|
|
40
44
|
heading: 'Pay with PIX',
|
|
@@ -26,6 +26,8 @@ export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
26
26
|
processing: 'Processando pagamento…',
|
|
27
27
|
confirming: 'Estamos confirmando seu pagamento',
|
|
28
28
|
cannotPay: 'Não foi possível pagar',
|
|
29
|
+
connectionLost: 'Sem conexão no momento — continuamos tentando',
|
|
30
|
+
checkAgainAction: 'Verificar de novo',
|
|
29
31
|
},
|
|
30
32
|
pix: {
|
|
31
33
|
heading: 'Pague com PIX',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import type { JSX } from "react";
|
|
3
|
+
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
5
|
+
import { useCheckoutComponents } from "./ui";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A wait that has stopped LOOKING like progress, said as a warning with the
|
|
9
|
+
* buyer's own "ask now" underneath (FUT-1144).
|
|
10
|
+
*
|
|
11
|
+
* Two situations reach it and they are deliberately the same shape: the poll
|
|
12
|
+
* cannot reach us, or the wall-clock wait has run out. In both, the confirmation
|
|
13
|
+
* spinner would be telling the buyer something is happening when nothing is —
|
|
14
|
+
* and by this point in the card and wallet panes every pay control is already
|
|
15
|
+
* gone, so without this button the screen has no control at all while it reports
|
|
16
|
+
* a problem. Pressing it restarts the wait, which is what makes it worth
|
|
17
|
+
* offering after a timeout and not only during a blip.
|
|
18
|
+
*
|
|
19
|
+
* A WARNING rather than a danger, in both. Neither says the payment failed:
|
|
20
|
+
* the charge is recoverable by webhook, reconciliation or backfill, and a red
|
|
21
|
+
* panel over a recoverable charge is what pushes a buyer into paying twice.
|
|
22
|
+
*
|
|
23
|
+
* One component for the card and the wallet because it is one decision. The
|
|
24
|
+
* panes' own confirmation states were near-identical before this and drifted
|
|
25
|
+
* apart in exactly the way that ends with a product telling a buyer two
|
|
26
|
+
* different things about one situation depending on which button they pressed.
|
|
27
|
+
*/
|
|
28
|
+
export function StalledWait({
|
|
29
|
+
title,
|
|
30
|
+
description,
|
|
31
|
+
onCheckAgain,
|
|
32
|
+
testId,
|
|
33
|
+
actionTestId,
|
|
34
|
+
}: {
|
|
35
|
+
title: string;
|
|
36
|
+
description: string;
|
|
37
|
+
onCheckAgain: () => void;
|
|
38
|
+
/** The alert's id — each pane names its own situation for its own suites. */
|
|
39
|
+
testId: string;
|
|
40
|
+
actionTestId: string;
|
|
41
|
+
}): JSX.Element {
|
|
42
|
+
const { Alert, Button } = useCheckoutComponents();
|
|
43
|
+
const copy = useCheckoutCopy().screens.settling;
|
|
44
|
+
return (
|
|
45
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
46
|
+
<Alert variant="warning" title={title} description={description} showIcon data-testid={testId} />
|
|
47
|
+
<Button
|
|
48
|
+
variant="outline"
|
|
49
|
+
color="neutral"
|
|
50
|
+
size="md"
|
|
51
|
+
onClick={onCheckAgain}
|
|
52
|
+
dataTestId={actionTestId}
|
|
53
|
+
>
|
|
54
|
+
{copy.checkAgainAction}
|
|
55
|
+
</Button>
|
|
56
|
+
</Box>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -26,6 +26,7 @@ import { useCheckoutClientApi } from "./client-context";
|
|
|
26
26
|
import { rememberHostedOrder } from "./hosted-return";
|
|
27
27
|
import { useCheckoutNavigate, type CheckoutNavigate } from "./navigate-context";
|
|
28
28
|
import type { CardChainLink } from "./method-capability";
|
|
29
|
+
import { useOneClickArmed, useOneClickPay } from "./one-click";
|
|
29
30
|
import type { BuyerInfo, CheckoutOrder, OrderStatus } from "./types";
|
|
30
31
|
import { usePaymentPolling } from "./use-payment-polling";
|
|
31
32
|
import type { CardCopy } from "../../card/copy";
|
|
@@ -99,7 +100,7 @@ function useCardPublicKey(
|
|
|
99
100
|
}
|
|
100
101
|
|
|
101
102
|
/** Everything the card view renders — all checkout state + the submit handler. */
|
|
102
|
-
interface CardCheckout {
|
|
103
|
+
export interface CardCheckout {
|
|
103
104
|
savedCards: SavedCard[];
|
|
104
105
|
selection: string;
|
|
105
106
|
setSelection: (id: string) => void;
|
|
@@ -116,9 +117,16 @@ interface CardCheckout {
|
|
|
116
117
|
errorCode: string | null;
|
|
117
118
|
submitting: boolean;
|
|
118
119
|
submitted: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* The last status poll failed. TRANSIENT (FUT-1144): the wait carries on at a
|
|
122
|
+
* backed-off cadence and this clears on the next success, so the view shows
|
|
123
|
+
* it as "still trying" beside {@link pollCheckAgain} rather than as an end.
|
|
124
|
+
*/
|
|
119
125
|
pollError: string | null;
|
|
120
|
-
/** The
|
|
126
|
+
/** The bounded AWAITING wait elapsed (FUT-191, now wall-clock — FUT-1144). */
|
|
121
127
|
pollTimedOut: boolean;
|
|
128
|
+
/** Ask now and restart the wait — the buyer's "verificar de novo". */
|
|
129
|
+
pollCheckAgain: () => void;
|
|
122
130
|
handlePay: () => Promise<void>;
|
|
123
131
|
}
|
|
124
132
|
|
|
@@ -141,14 +149,20 @@ type CardSubmit = Pick<
|
|
|
141
149
|
| "errorCode"
|
|
142
150
|
| "pollError"
|
|
143
151
|
| "pollTimedOut"
|
|
152
|
+
| "pollCheckAgain"
|
|
144
153
|
| "handlePay"
|
|
145
154
|
>;
|
|
146
155
|
|
|
147
156
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
157
|
+
* The card AWAITING wait, bounded in WALL TIME: 90 s (FUT-191, FUT-1144).
|
|
158
|
+
*
|
|
159
|
+
* It was 36 healthy polls, which is the same 90 s at the default 2500 ms
|
|
160
|
+
* interval and an unbounded wait at any other — including the one that
|
|
161
|
+
* mattered, where every poll is FAILING and the healthy count never moves.
|
|
162
|
+
* PIX passes no bound at all and keeps today's behavior: its charge expires
|
|
163
|
+
* server-side and comes back as a terminal EXPIRED.
|
|
150
164
|
*/
|
|
151
|
-
const
|
|
165
|
+
const CARD_AWAITING_WAIT_MS = 90_000;
|
|
152
166
|
|
|
153
167
|
/**
|
|
154
168
|
* Hand the buyer to the provider's authentication page (FUT-698) — Stripe's
|
|
@@ -209,6 +223,31 @@ async function resolveInstruments(input: {
|
|
|
209
223
|
);
|
|
210
224
|
}
|
|
211
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The card's status wait, named the way the view reads it.
|
|
228
|
+
*
|
|
229
|
+
* Its own function so `useCardSubmit` stays inside the size gate, and so the
|
|
230
|
+
* one decision here — this wait is bounded in WALL TIME — sits beside the
|
|
231
|
+
* constant that states it rather than inside a submit machine.
|
|
232
|
+
*/
|
|
233
|
+
function useCardWait(
|
|
234
|
+
orderId: string,
|
|
235
|
+
submitted: boolean,
|
|
236
|
+
intervalMs: number,
|
|
237
|
+
): {
|
|
238
|
+
status: OrderStatus | null;
|
|
239
|
+
pollError: string | null;
|
|
240
|
+
pollTimedOut: boolean;
|
|
241
|
+
pollCheckAgain: () => void;
|
|
242
|
+
} {
|
|
243
|
+
const { status, error, timedOut, checkAgain } = usePaymentPolling(orderId, {
|
|
244
|
+
enabled: submitted,
|
|
245
|
+
intervalMs,
|
|
246
|
+
maxWaitMs: CARD_AWAITING_WAIT_MS,
|
|
247
|
+
});
|
|
248
|
+
return { status, pollError: error, pollTimedOut: timedOut, pollCheckAgain: checkAgain };
|
|
249
|
+
}
|
|
250
|
+
|
|
212
251
|
/**
|
|
213
252
|
* The submit state machine (FUT-58): validate → tokenize (self-heal) → charge →
|
|
214
253
|
* poll for the async confirmation, bubbling the terminal status up via
|
|
@@ -234,11 +273,7 @@ function useCardSubmit(
|
|
|
234
273
|
const navigate = useCheckoutNavigate();
|
|
235
274
|
const cardCopy = useCheckoutCopy().card;
|
|
236
275
|
|
|
237
|
-
const { status,
|
|
238
|
-
enabled: submitted,
|
|
239
|
-
intervalMs: pollIntervalMs,
|
|
240
|
-
maxHealthyPolls: CARD_AWAITING_POLL_CAP,
|
|
241
|
-
});
|
|
276
|
+
const { status, ...wait } = useCardWait(order.orderId, submitted, pollIntervalMs);
|
|
242
277
|
|
|
243
278
|
useEffect(() => {
|
|
244
279
|
if (status && status !== "AWAITING_PAYMENT") onResolved(status);
|
|
@@ -292,7 +327,7 @@ function useCardSubmit(
|
|
|
292
327
|
else setSubmitted(true);
|
|
293
328
|
};
|
|
294
329
|
|
|
295
|
-
return { submitting, submitted, error, errorCode,
|
|
330
|
+
return { submitting, submitted, error, errorCode, ...wait, handlePay };
|
|
296
331
|
}
|
|
297
332
|
|
|
298
333
|
/**
|
|
@@ -339,6 +374,13 @@ export function useCardCheckout(
|
|
|
339
374
|
{ card, usingNewCard, selection, saveCard, validate, setFieldErrors },
|
|
340
375
|
providerChain,
|
|
341
376
|
);
|
|
377
|
+
// The tap a one-click buyer already made (`./one-click.tsx`). Nothing about
|
|
378
|
+
// the charge differs — this only presses the button, and only while a SAVED
|
|
379
|
+
// card is the selection, which is a state the picker reaches exactly when the
|
|
380
|
+
// instrument list came back with something. A buyer with no saved card is
|
|
381
|
+
// left on the form, which is the ordinary step 2.
|
|
382
|
+
const ready = !usingNewCard && !submit.submitting && !submit.submitted && submit.error === null;
|
|
383
|
+
useOneClickPay({ armed: useOneClickArmed(), ready, pay: submit.handlePay });
|
|
342
384
|
|
|
343
385
|
return {
|
|
344
386
|
savedCards,
|
|
@@ -147,15 +147,19 @@ function handOverToProvider(
|
|
|
147
147
|
* confirmation is at most 2.5 s late, and an abandoned checkout costs ~126
|
|
148
148
|
* polls instead of the 360 a flat 2.5 s would have.
|
|
149
149
|
*
|
|
150
|
-
* The three constants are ONE decision — 48 × 2.5 s + 78 × 10 s ≈ 15 min — so
|
|
151
|
-
* the cap is derived rather than typed, and cannot drift from the comment.
|
|
152
|
-
*
|
|
153
150
|
* Fifteen minutes because by then a webhook that was ever coming has come. Past
|
|
154
151
|
* that the answer will not change while the buyer watches: the scheduled
|
|
155
152
|
* reconciliation is what rescues a genuinely late one, and it does that whether
|
|
156
153
|
* the tab is open or not.
|
|
157
154
|
*
|
|
158
|
-
* The
|
|
155
|
+
* The BOUND is that wall-clock window, not a poll count (FUT-1144). The two are
|
|
156
|
+
* the same number for a healthy wait — 48 × 2.5 s + 78 × 10 s = 15 min — and
|
|
157
|
+
* they part company for the wait that needed bounding: a poll that FAILS
|
|
158
|
+
* incremented nothing, so a connection that never came back left this screen
|
|
159
|
+
* asking, and spinning, with no end at all. A clock cannot be stopped by the
|
|
160
|
+
* failure it is measuring.
|
|
161
|
+
*
|
|
162
|
+
* The card wait is bounded at 90 s (`CARD_AWAITING_WAIT_MS`) because a card
|
|
159
163
|
* authorises inline and a buyer is holding their phone. This leg is the other
|
|
160
164
|
* shape: the buyer has already been off to another site and back, and may
|
|
161
165
|
* legitimately still be finishing there.
|
|
@@ -165,8 +169,7 @@ const HOSTED_RESUME_SLOW_MS = 10_000;
|
|
|
165
169
|
/** Two minutes at the fast rate, before the wait is worth economising on. */
|
|
166
170
|
const HOSTED_RESUME_FAST_POLLS = (2 * 60_000) / HOSTED_RESUME_FAST_MS;
|
|
167
171
|
/** Thirteen more at the slow one — 15 minutes all told. */
|
|
168
|
-
const
|
|
169
|
-
HOSTED_RESUME_FAST_POLLS + (13 * 60_000) / HOSTED_RESUME_SLOW_MS;
|
|
172
|
+
const HOSTED_RESUME_WINDOW_MS = 15 * 60_000;
|
|
170
173
|
|
|
171
174
|
/**
|
|
172
175
|
* The leg of checkout that resumes after a hosted provider sent the buyer back
|
|
@@ -184,21 +187,56 @@ const HOSTED_RESUME_POLL_CAP =
|
|
|
184
187
|
* had no way to reach a terminal state, because the ORDER has none: expiry is
|
|
185
188
|
* PIX-only, and a redirect charge carries no QR window to lapse. `timedOut` is
|
|
186
189
|
* what the screen says instead of spinning.
|
|
190
|
+
*
|
|
191
|
+
* `error` is the half that was dropped on the floor (FUT-1144), and dropping it
|
|
192
|
+
* is what made this leg the SILENT one. The poll below has always been able to
|
|
193
|
+
* fail; this hook returned `status` and `timedOut` and nothing else, so a leg
|
|
194
|
+
* whose every request was failing looked identical to one still waiting — the
|
|
195
|
+
* spinner, forever, with the reason a `console`-less browser away. It is
|
|
196
|
+
* surfaced now, together with the wait's own `checkAgain`, because a screen that
|
|
197
|
+
* says "we cannot reach the payment" and offers nothing to press is only half
|
|
198
|
+
* of an answer.
|
|
187
199
|
*/
|
|
188
200
|
function useHostedResume(tenantSlug?: string): {
|
|
189
201
|
order: CheckoutOrder | null;
|
|
190
202
|
status: OrderStatus | null;
|
|
191
203
|
timedOut: boolean;
|
|
204
|
+
error: string | null;
|
|
205
|
+
checkAgain: () => void;
|
|
192
206
|
} {
|
|
193
207
|
const [order] = useState(() => takeHostedOrder(tenantSlug));
|
|
194
|
-
const { status, timedOut } = usePaymentPolling(order?.orderId ?? null, {
|
|
208
|
+
const { status, timedOut, error, checkAgain } = usePaymentPolling(order?.orderId ?? null, {
|
|
195
209
|
enabled: Boolean(order),
|
|
196
210
|
intervalMs: HOSTED_RESUME_FAST_MS,
|
|
197
211
|
slowAfterPolls: HOSTED_RESUME_FAST_POLLS,
|
|
198
212
|
slowIntervalMs: HOSTED_RESUME_SLOW_MS,
|
|
199
|
-
|
|
213
|
+
maxWaitMs: HOSTED_RESUME_WINDOW_MS,
|
|
200
214
|
});
|
|
201
|
-
return { order, status, timedOut };
|
|
215
|
+
return { order, status, timedOut, error, checkAgain };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* What the resumed leg contributes to the controller's surface.
|
|
220
|
+
*
|
|
221
|
+
* `resumeTimedOut` is only ever true on that leg — `useHostedResume` is the
|
|
222
|
+
* sole caller that bounds its wait, and a buyer who never left has a card or
|
|
223
|
+
* PIX view reporting its own. `resumeError` and `resumeCheckAgain` are the
|
|
224
|
+
* transient failure and the buyer's way out of it (FUT-1144).
|
|
225
|
+
*
|
|
226
|
+
* All three are inert for a checkout that never left this tab: with nothing
|
|
227
|
+
* parked the poll is disabled, so the error stays null, the bound never
|
|
228
|
+
* elapses, and the action has no wait to restart.
|
|
229
|
+
*/
|
|
230
|
+
function resumeSurface(resume: ReturnType<typeof useHostedResume>): {
|
|
231
|
+
resumeTimedOut: boolean;
|
|
232
|
+
resumeError: string | null;
|
|
233
|
+
resumeCheckAgain: () => void;
|
|
234
|
+
} {
|
|
235
|
+
return {
|
|
236
|
+
resumeTimedOut: resume.timedOut,
|
|
237
|
+
resumeError: resume.error,
|
|
238
|
+
resumeCheckAgain: resume.checkAgain,
|
|
239
|
+
};
|
|
202
240
|
}
|
|
203
241
|
|
|
204
242
|
/**
|
|
@@ -338,10 +376,7 @@ export function useCheckoutController(
|
|
|
338
376
|
return {
|
|
339
377
|
step, setStep, method, setMethod, buyer, setBuyer, saveProfile, setSaveProfile,
|
|
340
378
|
order, finalStatus: finalStatus ?? resume.status, creating,
|
|
341
|
-
|
|
342
|
-
// that caps its polls, and a buyer who never left has a card or PIX view
|
|
343
|
-
// reporting its own wait.
|
|
344
|
-
resumeTimedOut: resume.timedOut,
|
|
379
|
+
...resumeSurface(resume),
|
|
345
380
|
createError: failure.message, errorField: failure.field, errorCode: failure.code,
|
|
346
381
|
goToMenu: onExitToMenu, back, editBuyer,
|
|
347
382
|
goToPayment, startPayment, payWithEmail, handleResolved, retry, completed,
|