@12-apps/payments-frontend 3.21.4 → 3.23.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 +2 -2
- package/src/components/checkout/basket.ts +85 -0
- package/src/components/checkout/card-outcome.ts +81 -0
- package/src/components/checkout/card-view.tsx +62 -22
- package/src/components/checkout/checkout-actions.ts +387 -0
- package/src/components/checkout/checkout-flow.tsx +126 -24
- package/src/components/checkout/checkout-steps.tsx +149 -174
- package/src/components/checkout/checkout-totals.tsx +51 -0
- package/src/components/checkout/client-context.tsx +3 -0
- package/src/components/checkout/confirmation-wait.ts +97 -0
- package/src/components/checkout/dados-step.tsx +141 -0
- package/src/components/checkout/decline.ts +48 -0
- package/src/components/checkout/en-US.ts +41 -0
- package/src/components/checkout/failure-codes.ts +82 -0
- package/src/components/checkout/hosted-return.ts +190 -206
- package/src/components/checkout/hosted-store.ts +291 -0
- package/src/components/checkout/payment-error-panel.tsx +9 -3
- package/src/components/checkout/payment-status-parts.tsx +311 -0
- package/src/components/checkout/payment-status.tsx +69 -264
- package/src/components/checkout/pix-view.tsx +97 -8
- package/src/components/checkout/poll-loop.ts +5 -3
- package/src/components/checkout/providers/types.ts +20 -3
- package/src/components/checkout/pt-BR.ts +42 -0
- package/src/components/checkout/screens-copy.ts +14 -0
- package/src/components/checkout/screens-en-US.ts +1 -0
- package/src/components/checkout/screens-pt-BR.ts +3 -0
- package/src/components/checkout/transport.ts +21 -1
- package/src/components/checkout/types.ts +35 -0
- package/src/components/checkout/use-card-checkout.ts +34 -33
- package/src/components/checkout/use-checkout-controller.ts +68 -274
- package/src/components/checkout/use-hosted-resume.ts +326 -0
- package/src/components/checkout/use-payment-polling.ts +58 -7
- package/src/components/checkout/use-wallet-charge.ts +24 -1
- package/src/components/checkout/view-copy.ts +70 -0
- package/src/components/checkout/wallet-pane.tsx +9 -1
- package/src/flows/catalog-exit.ts +33 -0
- package/src/flows/create-payment-flows.tsx +19 -1
- package/src/flows/pipeline/actions.tsx +104 -0
- package/src/flows/pipeline/admission.ts +55 -0
- package/src/flows/pipeline/context.ts +140 -0
- package/src/flows/pipeline/derive-step.ts +234 -0
- package/src/flows/pipeline/engine-actions.ts +257 -0
- package/src/flows/pipeline/engine-chrome.tsx +123 -0
- package/src/flows/pipeline/engine-state.ts +107 -0
- package/src/flows/pipeline/engine.tsx +377 -0
- package/src/flows/pipeline/methods.ts +71 -0
- package/src/flows/pipeline/refusal-routing.ts +106 -0
- package/src/flows/pipeline/slices.ts +110 -0
- package/src/flows/pipeline/stable-plugins.ts +72 -0
- package/src/flows/pipeline/steps/buyer-steps.tsx +297 -0
- package/src/flows/pipeline/steps/index.ts +54 -0
- package/src/flows/pipeline/steps/pay-steps.tsx +182 -0
- package/src/flows/pipeline/steps/status-step.tsx +41 -0
- package/src/flows/pipeline/types.ts +232 -0
- package/src/flows/public.ts +78 -0
- package/src/flows/screens-hosted.tsx +55 -5
- package/src/flows/screens-pay.tsx +6 -1
- package/src/flows/types.ts +25 -2
- package/src/index.ts +29 -19
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import { useEffect, useRef, type JSX, type ReactNode } from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import type { CheckoutBasketIdentity } from "./basket";
|
|
5
|
+
import { displayTotals, PayBarTotal } from "./checkout-totals";
|
|
5
6
|
import { useCheckoutCopy } from "./copy-context";
|
|
6
|
-
import { LockOutlinedIcon } from "./icons";
|
|
7
7
|
import { useMethodChoice } from "./method-choice";
|
|
8
8
|
import { MethodPicker } from "./method-picker";
|
|
9
9
|
import { PaymentErrorPanel } from "./payment-error-panel";
|
|
@@ -12,13 +12,12 @@ import { resolveCheckoutScreen } from "./providers/registry";
|
|
|
12
12
|
import type {
|
|
13
13
|
BuyerField,
|
|
14
14
|
BuyerInfo,
|
|
15
|
-
CheckoutCustomerField,
|
|
16
15
|
CheckoutOrder,
|
|
17
16
|
CheckoutProviderConfig,
|
|
18
|
-
|
|
17
|
+
OnCheckoutResolved,
|
|
19
18
|
PaymentMethod,
|
|
20
19
|
} from "./types";
|
|
21
|
-
import type {
|
|
20
|
+
import type { EmptyCartCopy } from "./view-copy";
|
|
22
21
|
import { useCheckoutComponents } from "./ui";
|
|
23
22
|
|
|
24
23
|
/**
|
|
@@ -70,6 +69,8 @@ function PaymentBody({
|
|
|
70
69
|
onStart,
|
|
71
70
|
creating,
|
|
72
71
|
pollIntervalMs,
|
|
72
|
+
freshInstrument,
|
|
73
|
+
basket,
|
|
73
74
|
validateApplePayMerchant,
|
|
74
75
|
}: {
|
|
75
76
|
order: CheckoutOrder | null;
|
|
@@ -77,11 +78,13 @@ function PaymentBody({
|
|
|
77
78
|
providerConfig: CheckoutProviderConfig | null;
|
|
78
79
|
method: PaymentMethod | null;
|
|
79
80
|
tenantSlug?: string;
|
|
80
|
-
onResolved:
|
|
81
|
+
onResolved: OnCheckoutResolved;
|
|
81
82
|
/** Set only when the shell hid its picker — see {@link PaymentStep}. */
|
|
82
83
|
onStart?: () => void;
|
|
83
84
|
creating: boolean;
|
|
84
85
|
pollIntervalMs?: number;
|
|
86
|
+
freshInstrument?: boolean;
|
|
87
|
+
basket?: CheckoutBasketIdentity;
|
|
85
88
|
validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
86
89
|
}): JSX.Element | null {
|
|
87
90
|
const Screen = resolveCheckoutScreen(providerConfig?.chain?.[0]?.checkoutScreen);
|
|
@@ -96,6 +99,8 @@ function PaymentBody({
|
|
|
96
99
|
onStart={onStart}
|
|
97
100
|
creating={creating}
|
|
98
101
|
pollIntervalMs={pollIntervalMs}
|
|
102
|
+
freshInstrument={freshInstrument}
|
|
103
|
+
basket={basket}
|
|
99
104
|
validateApplePayMerchant={validateApplePayMerchant}
|
|
100
105
|
/>
|
|
101
106
|
);
|
|
@@ -116,162 +121,45 @@ export function EmptyCart({ copy, onBack }: { copy: EmptyCartCopy; onBack: () =>
|
|
|
116
121
|
);
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
override: { label: string; items: number } | undefined,
|
|
126
|
-
cart: { totalLabel: string; totalItems: number },
|
|
127
|
-
): { label: string; items: number } {
|
|
128
|
-
return { label: override?.label ?? cart.totalLabel, items: override?.items ?? cart.totalItems };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** The pay bar's money column: item count, grand total, host discount lines. */
|
|
132
|
-
function PayBarTotal({
|
|
133
|
-
totalLabel,
|
|
134
|
-
totalItems,
|
|
135
|
-
children,
|
|
136
|
-
}: {
|
|
137
|
-
totalLabel: string;
|
|
138
|
-
totalItems: number;
|
|
139
|
-
children?: ReactNode;
|
|
140
|
-
}): JSX.Element {
|
|
141
|
-
const { Text } = useCheckoutComponents();
|
|
142
|
-
return (
|
|
143
|
-
<Box sx={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
|
144
|
-
<Text variant="caption" size="xs" color="secondary" as="span">
|
|
145
|
-
Total · {totalItems} {totalItems === 1 ? "item" : "itens"}
|
|
146
|
-
</Text>
|
|
147
|
-
<Text variant="heading" size="md" weight="bold" color="primary" as="span" data-testid="pay-bar-total">
|
|
148
|
-
{totalLabel}
|
|
149
|
-
</Text>
|
|
150
|
-
{children}
|
|
151
|
-
</Box>
|
|
152
|
-
);
|
|
124
|
+
/** What the step knows about the amount: the charge's, the balance's, the cart's. */
|
|
125
|
+
interface StepMoney {
|
|
126
|
+
order: CheckoutOrder | null;
|
|
127
|
+
cartTotals?: { totalLabel: string; totalItems: number };
|
|
128
|
+
totalOverride?: { label: string; items: number };
|
|
129
|
+
discountLines?: ReactNode;
|
|
153
130
|
}
|
|
154
131
|
|
|
155
132
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* (
|
|
160
|
-
* —
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
discountLines,
|
|
174
|
-
totalOverride,
|
|
175
|
-
}: {
|
|
176
|
-
/** The step's own sentences — the HOST's words (see `./view-copy`). */
|
|
177
|
-
copy: DadosStepCopy;
|
|
178
|
-
buyer: BuyerInfo;
|
|
179
|
-
onBuyerChange: (buyer: BuyerInfo) => void;
|
|
180
|
-
saveProfile: boolean;
|
|
181
|
-
onSaveProfileChange: (value: boolean) => void;
|
|
182
|
-
createError: string | null;
|
|
183
|
-
errorField: BuyerField | null;
|
|
184
|
-
onContinue: () => void;
|
|
185
|
-
/** The host cart's own totals — what the pay bar shows in cart mode. */
|
|
186
|
-
cartTotals: { totalLabel: string; totalItems: number };
|
|
187
|
-
/** What the store's chain declares it needs (FUT-595); absent ⇒ CPF-required. */
|
|
188
|
-
buyerFields?: readonly CheckoutCustomerField[];
|
|
189
|
-
/**
|
|
190
|
-
* The saving, itemized under the total the buyer is about to authorize
|
|
191
|
-
* (FUT-246) — RENDERED BY THE HOST from its cart (the storefront passes its
|
|
192
|
-
* cart footer's money block), never re-implemented here, so the two surfaces
|
|
193
|
-
* can never word the same discount differently.
|
|
133
|
+
* The Pagamento step's own money line (FUT-1179).
|
|
134
|
+
*
|
|
135
|
+
* THE RAISED ORDER'S TOTAL WINS, exactly as the confirmation screen's does
|
|
136
|
+
* (`statusTotalLabel`). Once a charge exists, that charge's amount is what is
|
|
137
|
+
* being taken — a repriced cart, or an order resumed from a parked entry, makes
|
|
138
|
+
* the cart's total a different number from the one about to leave the buyer's
|
|
139
|
+
* account. Showing the cart's number above a QR for the order's would be this
|
|
140
|
+
* ticket's own defect in a new place: an amount on screen that is not the
|
|
141
|
+
* amount being charged.
|
|
142
|
+
*
|
|
143
|
+
* The item COUNT stays the cart's — an order carries no line count, and the
|
|
144
|
+
* caption is a description of the basket rather than of the charge.
|
|
145
|
+
*
|
|
146
|
+
* Renders nothing at all when the host supplied no totals and no order — a step
|
|
147
|
+
* that would otherwise print "Total ·" with a blank beside it is worse than the
|
|
148
|
+
* silence this ticket is about. Every host mounting `CheckoutFlow` gets them;
|
|
149
|
+
* only a hand-composed step can be missing them.
|
|
194
150
|
*/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
const { Checkbox } = useCheckoutComponents();
|
|
200
|
-
const { label: totalLabel, items: totalItems } = displayTotals(totalOverride, cartTotals);
|
|
201
|
-
|
|
151
|
+
function PaymentStepTotal({ money }: { money: StepMoney }): JSX.Element | null {
|
|
152
|
+
const { order, cartTotals, totalOverride, discountLines } = money;
|
|
153
|
+
if (!cartTotals && !totalOverride && !order) return null;
|
|
154
|
+
const { label, items } = displayTotals(totalOverride, cartTotals ?? { totalLabel: "", totalItems: 0 });
|
|
202
155
|
return (
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
<BuyerInfoForm
|
|
206
|
-
value={buyer}
|
|
207
|
-
onChange={onBuyerChange}
|
|
208
|
-
fields={buyerFields}
|
|
209
|
-
fieldError={errorField && createError ? { field: errorField, message: createError } : null}
|
|
210
|
-
/>
|
|
211
|
-
<Checkbox
|
|
212
|
-
checked={saveProfile}
|
|
213
|
-
onChange={(_event, checked) => onSaveProfileChange(checked)}
|
|
214
|
-
label={copy.saveProfile}
|
|
215
|
-
data-testid="buyer-save-profile"
|
|
216
|
-
/>
|
|
217
|
-
</Box>
|
|
218
|
-
|
|
219
|
-
<DadosPayBar
|
|
220
|
-
copy={copy}
|
|
221
|
-
totalLabel={totalLabel}
|
|
222
|
-
totalItems={totalItems}
|
|
223
|
-
createError={createError}
|
|
224
|
-
onContinue={onContinue}
|
|
225
|
-
>
|
|
156
|
+
<Box data-testid="payment-step-total" sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 2 }}>
|
|
157
|
+
<PayBarTotal totalLabel={order?.totalLabel ?? label} totalItems={items}>
|
|
226
158
|
{/* Suppressed while settling a balance: those totals come from the
|
|
227
159
|
frozen ticket, not the cart. */}
|
|
228
160
|
{totalOverride ? null : discountLines}
|
|
229
|
-
</
|
|
230
|
-
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** The sticky "Continuar" bar: the refusal, the money, and the one action. */
|
|
235
|
-
function DadosPayBar({
|
|
236
|
-
copy,
|
|
237
|
-
totalLabel,
|
|
238
|
-
totalItems,
|
|
239
|
-
createError,
|
|
240
|
-
onContinue,
|
|
241
|
-
children,
|
|
242
|
-
}: {
|
|
243
|
-
copy: DadosStepCopy;
|
|
244
|
-
totalLabel: string;
|
|
245
|
-
totalItems: number;
|
|
246
|
-
createError: string | null;
|
|
247
|
-
onContinue: () => void;
|
|
248
|
-
children?: ReactNode;
|
|
249
|
-
}): JSX.Element {
|
|
250
|
-
const { ActionBar, Alert, Button, Text } = useCheckoutComponents();
|
|
251
|
-
return (
|
|
252
|
-
<ActionBar dataTestId="checkout-pay-bar">
|
|
253
|
-
<Box sx={{ width: "100%", display: "flex", flexDirection: "column", gap: 1.5 }}>
|
|
254
|
-
{createError ? (
|
|
255
|
-
<Alert variant="danger" title={copy.cannotContinueTitle} description={createError} showIcon data-testid="checkout-error" />
|
|
256
|
-
) : null}
|
|
257
|
-
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
|
|
258
|
-
<PayBarTotal totalLabel={totalLabel} totalItems={totalItems}>{children}</PayBarTotal>
|
|
259
|
-
<Box sx={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 0.5, minWidth: 0 }}>
|
|
260
|
-
<Button variant="solid" color="primary" size="lg" fullWidth onClick={onContinue} dataTestId="checkout-continue">
|
|
261
|
-
{copy.continueAction}
|
|
262
|
-
</Button>
|
|
263
|
-
{copy.secureNotice ? (
|
|
264
|
-
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, color: "text.secondary" }}>
|
|
265
|
-
<LockOutlinedIcon sx={{ fontSize: 13 }} />
|
|
266
|
-
<Text variant="caption" size="xs" color="secondary" as="span">
|
|
267
|
-
{copy.secureNotice}
|
|
268
|
-
</Text>
|
|
269
|
-
</Box>
|
|
270
|
-
) : null}
|
|
271
|
-
</Box>
|
|
272
|
-
</Box>
|
|
273
|
-
</Box>
|
|
274
|
-
</ActionBar>
|
|
161
|
+
</PayBarTotal>
|
|
162
|
+
</Box>
|
|
275
163
|
);
|
|
276
164
|
}
|
|
277
165
|
|
|
@@ -305,10 +193,39 @@ interface PaymentStepProps {
|
|
|
305
193
|
providerConfig?: CheckoutProviderConfig | null;
|
|
306
194
|
/** Scopes the saved-card list to the store being paid (host routing owns it). */
|
|
307
195
|
tenantSlug?: string;
|
|
196
|
+
/**
|
|
197
|
+
* WHAT THE BUYER IS ABOUT TO PAY (FUT-1179) — the host cart's own totals, or
|
|
198
|
+
* the settlement's where one is being settled.
|
|
199
|
+
*
|
|
200
|
+
* The Pagamento step showed no amount at all before a method was chosen, and
|
|
201
|
+
* a store that finishes on the provider's page sent a buyer with a CPF on
|
|
202
|
+
* file straight out to that provider with NO total ever having been on
|
|
203
|
+
* screen: the flow opens on Pagamento, the hand-off screen owns the only
|
|
204
|
+
* button, and the amount lived exclusively on the Dados step they skipped.
|
|
205
|
+
* Asking for money without showing the amount is the one thing a checkout
|
|
206
|
+
* may not do.
|
|
207
|
+
*
|
|
208
|
+
* Optional so a host composing this step by hand is not broken by the
|
|
209
|
+
* addition; absent, the step renders as it did.
|
|
210
|
+
*/
|
|
211
|
+
cartTotals?: { totalLabel: string; totalItems: number };
|
|
212
|
+
/** Settling an open balance: totals come from the settlement, not the cart. */
|
|
213
|
+
totalOverride?: { label: string; items: number };
|
|
214
|
+
/** The host's own rendered discount itemization, under the total (FUT-246). */
|
|
215
|
+
discountLines?: ReactNode;
|
|
308
216
|
pollIntervalMs?: number;
|
|
217
|
+
/** Retrying a refused card — preselect no saved instrument (FUT-1145). */
|
|
218
|
+
freshInstrument?: boolean;
|
|
219
|
+
/**
|
|
220
|
+
* WHICH basket this checkout is for (FUT-1213) — passed down because the
|
|
221
|
+
* card path can park an order too: a 3-D Secure challenge is a hand-off, and
|
|
222
|
+
* one parked without a basket resumes over any basket at any store, which is
|
|
223
|
+
* this ticket's own bug on a sibling path.
|
|
224
|
+
*/
|
|
225
|
+
basket?: CheckoutBasketIdentity;
|
|
309
226
|
/** The host's Apple Pay merchant-validation port (FUT-472) — see the screen contract. */
|
|
310
227
|
validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
311
|
-
onResolved:
|
|
228
|
+
onResolved: OnCheckoutResolved;
|
|
312
229
|
}
|
|
313
230
|
|
|
314
231
|
/**
|
|
@@ -341,18 +258,28 @@ export function PaymentStep({
|
|
|
341
258
|
onEditBuyer,
|
|
342
259
|
providerConfig,
|
|
343
260
|
tenantSlug,
|
|
261
|
+
cartTotals,
|
|
262
|
+
totalOverride,
|
|
263
|
+
discountLines,
|
|
344
264
|
pollIntervalMs,
|
|
265
|
+
freshInstrument,
|
|
266
|
+
basket,
|
|
345
267
|
validateApplePayMerchant,
|
|
346
268
|
onResolved,
|
|
347
269
|
}: PaymentStepProps): JSX.Element {
|
|
348
|
-
const { LoadingState } = useCheckoutComponents();
|
|
349
|
-
const screens = useCheckoutCopy().screens;
|
|
350
270
|
const config = providerConfig ?? null;
|
|
351
271
|
const choice = useMethodChoice(config, method, onMethodChange);
|
|
352
272
|
useAutoRaiseOrder(order, method, creating, createError, onGenerate);
|
|
353
273
|
|
|
354
274
|
return (
|
|
355
275
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
|
276
|
+
{/* THE AMOUNT, before anything asks for it (FUT-1179). At the top rather
|
|
277
|
+
than in a sticky bar of its own: this step's actions belong to the
|
|
278
|
+
pane below it — the PIX code, the card form's own pay bar, the
|
|
279
|
+
hand-off button — and a second bar would put two "pay" controls on
|
|
280
|
+
one screen. */}
|
|
281
|
+
<PaymentStepTotal money={{ order, cartTotals, totalOverride, discountLines }} />
|
|
282
|
+
|
|
356
283
|
{/* Self-hiding: renders only for a flow whose Dados step was skipped. */}
|
|
357
284
|
<PayerSummary name={buyer.name} taxId={buyer.taxId} onEdit={onEditBuyer} />
|
|
358
285
|
|
|
@@ -375,26 +302,74 @@ export function PaymentStep({
|
|
|
375
302
|
onStart={choice.onStart}
|
|
376
303
|
creating={creating}
|
|
377
304
|
pollIntervalMs={pollIntervalMs}
|
|
305
|
+
freshInstrument={freshInstrument}
|
|
306
|
+
basket={basket}
|
|
378
307
|
validateApplePayMerchant={validateApplePayMerchant}
|
|
379
308
|
/>
|
|
380
309
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
message={createError}
|
|
392
|
-
emailFlagged={errorField === "email"}
|
|
393
|
-
code={errorCode}
|
|
394
|
-
onUseEmail={onUseEmail}
|
|
395
|
-
onRetry={() => onGenerate(method)}
|
|
396
|
-
/>
|
|
397
|
-
) : null}
|
|
310
|
+
<RaisingState
|
|
311
|
+
order={order}
|
|
312
|
+
method={method}
|
|
313
|
+
creating={creating && !choice.atProvider}
|
|
314
|
+
createError={createError}
|
|
315
|
+
errorField={errorField}
|
|
316
|
+
errorCode={errorCode}
|
|
317
|
+
onUseEmail={onUseEmail}
|
|
318
|
+
onGenerate={onGenerate}
|
|
319
|
+
/>
|
|
398
320
|
</Box>
|
|
399
321
|
);
|
|
400
322
|
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* What the shell says while the order is being raised, and if raising it
|
|
326
|
+
* failed.
|
|
327
|
+
*
|
|
328
|
+
* The spinner is SUPPRESSED for a hand-off screen — that screen renders its own
|
|
329
|
+
* "Preparando o pagamento" while the charge is raised, and two stacked spinners
|
|
330
|
+
* saying the same thing is what the buyer actually saw. The caller decides
|
|
331
|
+
* that; here `creating` is simply true or false.
|
|
332
|
+
*/
|
|
333
|
+
function RaisingState({
|
|
334
|
+
order,
|
|
335
|
+
method,
|
|
336
|
+
creating,
|
|
337
|
+
createError,
|
|
338
|
+
errorField,
|
|
339
|
+
errorCode,
|
|
340
|
+
onUseEmail,
|
|
341
|
+
onGenerate,
|
|
342
|
+
}: {
|
|
343
|
+
order: CheckoutOrder | null;
|
|
344
|
+
method: PaymentMethod | null;
|
|
345
|
+
creating: boolean;
|
|
346
|
+
createError: string | null;
|
|
347
|
+
errorField: BuyerField | null;
|
|
348
|
+
errorCode?: string | null;
|
|
349
|
+
onUseEmail: (email: string) => void;
|
|
350
|
+
onGenerate: (method: PaymentMethod) => void;
|
|
351
|
+
}): JSX.Element | null {
|
|
352
|
+
const { LoadingState } = useCheckoutComponents();
|
|
353
|
+
const screens = useCheckoutCopy().screens;
|
|
354
|
+
if (order) return null;
|
|
355
|
+
if (creating) {
|
|
356
|
+
return (
|
|
357
|
+
<LoadingState
|
|
358
|
+
variant="spinner"
|
|
359
|
+
size="md"
|
|
360
|
+
message={screens.generatingPayment}
|
|
361
|
+
dataTestId="payment-generating"
|
|
362
|
+
/>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
if (!method || !createError) return null;
|
|
366
|
+
return (
|
|
367
|
+
<PaymentErrorPanel
|
|
368
|
+
message={createError}
|
|
369
|
+
emailFlagged={errorField === "email"}
|
|
370
|
+
code={errorCode}
|
|
371
|
+
onUseEmail={onUseEmail}
|
|
372
|
+
onRetry={() => onGenerate(method)}
|
|
373
|
+
/>
|
|
374
|
+
);
|
|
375
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import type { JSX, ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
5
|
+
import { useCheckoutComponents } from "./ui";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* THE MONEY LINE both steps show.
|
|
9
|
+
*
|
|
10
|
+
* Its own module because two steps now render it — the Dados bar always did,
|
|
11
|
+
* and the Pagamento step does since FUT-1179, which is the ticket about a
|
|
12
|
+
* checkout that asked for money without ever showing the amount. One
|
|
13
|
+
* implementation so the two can never word the same total differently.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The totals shown on the pay bar: the settled balance's when settling a settlement
|
|
18
|
+
* one, otherwise the cart's own — both supplied by the host, which
|
|
19
|
+
* is the only side that knows either.
|
|
20
|
+
*/
|
|
21
|
+
export function displayTotals(
|
|
22
|
+
override: { label: string; items: number } | undefined,
|
|
23
|
+
cart: { totalLabel: string; totalItems: number },
|
|
24
|
+
): { label: string; items: number } {
|
|
25
|
+
return { label: override?.label ?? cart.totalLabel, items: override?.items ?? cart.totalItems };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The pay bar's money column: item count, grand total, host discount lines. */
|
|
29
|
+
export function PayBarTotal({
|
|
30
|
+
totalLabel,
|
|
31
|
+
totalItems,
|
|
32
|
+
children,
|
|
33
|
+
}: {
|
|
34
|
+
totalLabel: string;
|
|
35
|
+
totalItems: number;
|
|
36
|
+
children?: ReactNode;
|
|
37
|
+
}): JSX.Element {
|
|
38
|
+
const { Text } = useCheckoutComponents();
|
|
39
|
+
const copy = useCheckoutCopy().screens;
|
|
40
|
+
return (
|
|
41
|
+
<Box sx={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
|
42
|
+
<Text variant="caption" size="xs" color="secondary" as="span">
|
|
43
|
+
{copy.totalCaption(totalItems)}
|
|
44
|
+
</Text>
|
|
45
|
+
<Text variant="heading" size="md" weight="bold" color="primary" as="span" data-testid="pay-bar-total">
|
|
46
|
+
{totalLabel}
|
|
47
|
+
</Text>
|
|
48
|
+
{children}
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -47,6 +47,9 @@ function unboundClient(copy: CheckoutTransportCopy): CheckoutClient {
|
|
|
47
47
|
// The vault pair (FUT-183) has no `client.ts` free function to bind — it is
|
|
48
48
|
// newer than that module. Built lazily from the default transport instead,
|
|
49
49
|
// which is the same wire: `/api/checkout`, ambient `fetch` resolved per call.
|
|
50
|
+
// Same lazy build as the vault pair below, and for the same reason: this
|
|
51
|
+
// row is newer than `client.ts`, so there is no free function to bind.
|
|
52
|
+
releaseCheckout: (input) => createCheckoutClient({ copy }).releaseCheckout(input),
|
|
50
53
|
beginVault: () => createCheckoutClient({ copy }).beginVault(),
|
|
51
54
|
completeVault: (input) => createCheckoutClient({ copy }).completeVault(input),
|
|
52
55
|
refreshBrowserKey: (input) => refreshCardPublicKey(input, copy),
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
import type { OrderStatus } from "./types";
|
|
4
|
+
import { usePaymentPolling } from "./use-payment-polling";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE CONFIRMATION STEP'S OWN WAIT (FUT-1170).
|
|
8
|
+
*
|
|
9
|
+
* Polling used to live entirely in the method's screen — the PIX code's footer,
|
|
10
|
+
* the card pane's post-submit state — which is right for the payment step and
|
|
11
|
+
* leaves the LAST step with none. A flow parked on Confirmação holding an
|
|
12
|
+
* unsettled order therefore rendered "Confirmando seu pagamento" over a spinner
|
|
13
|
+
* that stood for nothing: no poll was scheduled, no error could appear, no
|
|
14
|
+
* clock could elapse, and the only control on screen was "Voltar ao cardápio".
|
|
15
|
+
*
|
|
16
|
+
* FUT-1170 reaches that state through the regenerate path, and dropping the
|
|
17
|
+
* replaced charge closes that particular door. This is the other half, and it
|
|
18
|
+
* is the half that holds for doors nobody has opened yet: any caller that lands
|
|
19
|
+
* on the confirmation screen with a live order now has something asking about
|
|
20
|
+
* it, and — when the clock runs out — something to press.
|
|
21
|
+
*
|
|
22
|
+
* ## Why here rather than hoisting the screens' polls into the flow
|
|
23
|
+
*
|
|
24
|
+
* The three waits are genuinely different. PIX is bounded by the CODE's own
|
|
25
|
+
* expiry and decays over minutes; the card wait is 90 s from the submit; the
|
|
26
|
+
* resumed hosted leg has its own release action attached to it. One hoisted
|
|
27
|
+
* poll would have to serve all three at one cadence and one bound, and the
|
|
28
|
+
* cadence is the thing each of them tunes. So the payment step keeps the wait
|
|
29
|
+
* that belongs to the pane the buyer is looking at, and this covers the step
|
|
30
|
+
* that has no pane of its own.
|
|
31
|
+
*
|
|
32
|
+
* The two can never run together: this one is gated on the confirmation step,
|
|
33
|
+
* and every screen that polls renders on the payment step.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** How often the confirmation screen asks. The same cadence the panes open at. */
|
|
37
|
+
const CONFIRMATION_INTERVAL_MS = 2_500;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* How long it asks for, in WALL TIME — the card pane's bound, for the same
|
|
41
|
+
* reason it has one: past this point the spinner is no longer a description of
|
|
42
|
+
* anything, and the honest screen says so and offers the buyer the ask.
|
|
43
|
+
*
|
|
44
|
+
* Nothing is lost when it elapses. The order stays AWAITING server-side and is
|
|
45
|
+
* still recoverable by webhook, reconciliation or backfill; what changes is
|
|
46
|
+
* only that the screen stops pretending to watch.
|
|
47
|
+
*/
|
|
48
|
+
const CONFIRMATION_WAIT_MS = 90_000;
|
|
49
|
+
|
|
50
|
+
/** What the confirmation screen reads off a wait it owns. */
|
|
51
|
+
export interface ConfirmationWait {
|
|
52
|
+
/** The bound elapsed — nothing further is scheduled. */
|
|
53
|
+
timedOut: boolean;
|
|
54
|
+
/** The last ask failed, and the wait is still running (FUT-1144). */
|
|
55
|
+
error: string | null;
|
|
56
|
+
/** Ask now, and start the clock over. */
|
|
57
|
+
checkAgain: () => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Watch the order the flow is holding, while the confirmation screen is the one
|
|
62
|
+
* on display and nothing has settled it yet.
|
|
63
|
+
*
|
|
64
|
+
* @param active Whether this wait is the live one. FALSE for every checkout
|
|
65
|
+
* that has an answer already, and for every step that polls for itself — the
|
|
66
|
+
* caller decides, because only it knows which screen is up.
|
|
67
|
+
* @param onSettled A terminal status arrived. Called once per status, with the
|
|
68
|
+
* status only: whether it carries a refusal is the charge path's knowledge and
|
|
69
|
+
* a poll has none of it.
|
|
70
|
+
*/
|
|
71
|
+
export function useConfirmationWait(input: {
|
|
72
|
+
orderId: string | null;
|
|
73
|
+
active: boolean;
|
|
74
|
+
onSettled: (status: OrderStatus) => void;
|
|
75
|
+
}): ConfirmationWait {
|
|
76
|
+
const { orderId, active, onSettled } = input;
|
|
77
|
+
const { status, error, timedOut, checkAgain } = usePaymentPolling(orderId, {
|
|
78
|
+
enabled: active,
|
|
79
|
+
intervalMs: CONFIRMATION_INTERVAL_MS,
|
|
80
|
+
maxWaitMs: CONFIRMATION_WAIT_MS,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (!active || !status || status === "AWAITING_PAYMENT") return;
|
|
85
|
+
onSettled(status);
|
|
86
|
+
}, [active, status, onSettled]);
|
|
87
|
+
|
|
88
|
+
// Reported only while this wait is the live one. A bound that elapsed for an
|
|
89
|
+
// order the flow has since settled must not turn a paid confirmation into a
|
|
90
|
+
// warning, and `usePaymentPolling` keeps its last answer after `enabled` goes
|
|
91
|
+
// false — deliberately, so a settled wait can still be read.
|
|
92
|
+
return {
|
|
93
|
+
timedOut: active && timedOut,
|
|
94
|
+
error: active ? error : null,
|
|
95
|
+
checkAgain,
|
|
96
|
+
};
|
|
97
|
+
}
|