@12-apps/payments-frontend 3.21.4 → 3.22.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 +341 -0
- package/src/components/checkout/checkout-flow.tsx +112 -18
- 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/dados-step.tsx +141 -0
- package/src/components/checkout/decline.ts +48 -0
- package/src/components/checkout/en-US.ts +33 -0
- package/src/components/checkout/hosted-return.ts +190 -206
- package/src/components/checkout/hosted-store.ts +269 -0
- package/src/components/checkout/payment-status-parts.tsx +311 -0
- package/src/components/checkout/payment-status.tsx +69 -264
- package/src/components/checkout/providers/types.ts +20 -3
- package/src/components/checkout/pt-BR.ts +33 -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 +24 -0
- package/src/components/checkout/use-card-checkout.ts +31 -31
- package/src/components/checkout/use-checkout-controller.ts +51 -271
- package/src/components/checkout/use-hosted-resume.ts +326 -0
- package/src/components/checkout/view-copy.ts +32 -0
- package/src/components/checkout/wallet-pane.tsx +3 -0
- package/src/flows/create-payment-flows.tsx +7 -0
- package/src/flows/screens-hosted.tsx +19 -2
- package/src/index.ts +22 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
2
|
import { useMemo, type JSX, type ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
import type { CheckoutBasketIdentity } from "./basket";
|
|
4
5
|
import { buyerFieldsFor } from "./buyer-fields";
|
|
5
|
-
import {
|
|
6
|
+
import { EmptyCart, PaymentStep } from "./checkout-steps";
|
|
7
|
+
import { DadosStep } from "./dados-step";
|
|
6
8
|
import { ArrowBackIcon } from "./icons";
|
|
7
9
|
import { PaymentStatus } from "./payment-status";
|
|
8
10
|
import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
@@ -29,6 +31,18 @@ export interface CheckoutCartView {
|
|
|
29
31
|
totalItems: number;
|
|
30
32
|
/** Host-rendered discount itemization under the pay-bar total (FUT-246). */
|
|
31
33
|
discountLines?: ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* WHICH basket this is, so a payment raised from another one cannot resume
|
|
36
|
+
* itself over it (FUT-1213).
|
|
37
|
+
*
|
|
38
|
+
* Optional, and absent means the pre-1213 behaviour — a parked payment
|
|
39
|
+
* resumes on whatever checkout mounts next, which is what shipped and what
|
|
40
|
+
* this ticket exists to bound. A host supplies it by calling
|
|
41
|
+
* `basketSignature(lines)` on its own cart and passing `ready: false` while
|
|
42
|
+
* that cart is still loading; see `./basket.ts` for why the identity is the
|
|
43
|
+
* LINES and not the cart's id.
|
|
44
|
+
*/
|
|
45
|
+
identity?: CheckoutBasketIdentity;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
48
|
/**
|
|
@@ -84,6 +98,16 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
84
98
|
components?: Partial<CheckoutComponents>;
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Whether the host's cart has actually answered yet.
|
|
103
|
+
*
|
|
104
|
+
* `true` for every host that wires no identity, which is what it meant before
|
|
105
|
+
* there was one to wire.
|
|
106
|
+
*/
|
|
107
|
+
function cartLoaded(cart: CheckoutCartView): boolean {
|
|
108
|
+
return cart.identity?.ready !== false;
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
/** The pay-bar total override when settling a settlement (else the cart's own totals). */
|
|
88
112
|
function settlementTotalOverride(
|
|
89
113
|
settlement: SettlementCheckout | null | undefined,
|
|
@@ -179,6 +203,87 @@ function StatusStep({
|
|
|
179
203
|
// so nothing is being polled here.
|
|
180
204
|
awaitingError={c.resumeError}
|
|
181
205
|
onCheckAgain={c.resumeCheckAgain}
|
|
206
|
+
// The buyer's own way out of a hosted wait with no terminal state
|
|
207
|
+
// (FUT-1146). Absent — and so unrendered — until the resumed leg has one
|
|
208
|
+
// to offer, which is every checkout that never left this tab.
|
|
209
|
+
onNotPaid={c.resumeRelease}
|
|
210
|
+
releasing={c.resumeReleasing}
|
|
211
|
+
// WHY a card was refused, when the server said (FUT-1145). Read on
|
|
212
|
+
// FAILED only, where it picks the sentence and decides whether a retry
|
|
213
|
+
// could work at all.
|
|
214
|
+
decline={c.decline}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Nothing to check out — and the cart has ANSWERED, which is the half FUT-1213
|
|
221
|
+
* added.
|
|
222
|
+
*
|
|
223
|
+
* A settlement pays already-sent items, so the cart is legitimately empty
|
|
224
|
+
* there. The guard cannot key on the Dados step either: skipping it (FUT-465)
|
|
225
|
+
* makes Pagamento the first screen. And it holds until an order exists — once
|
|
226
|
+
* one does, its lines are snapshotted server-side and the cart no longer speaks
|
|
227
|
+
* for it.
|
|
228
|
+
*
|
|
229
|
+
* A cart still being FETCHED is empty in exactly the way a real one is not, so
|
|
230
|
+
* a host that wires `identity` gets this screen when its cart is empty rather
|
|
231
|
+
* than when it is late — which is also the moment the resume decision waits for.
|
|
232
|
+
*/
|
|
233
|
+
function nothingToPayFor(
|
|
234
|
+
settlement: SettlementCheckout | null | undefined,
|
|
235
|
+
cart: CheckoutCartView,
|
|
236
|
+
c: ReturnType<typeof useCheckoutController>,
|
|
237
|
+
): boolean {
|
|
238
|
+
if (settlement || !cart.empty || !cartLoaded(cart)) return false;
|
|
239
|
+
return !c.order && c.step !== "status";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Step 2, with the controller's facts and the host's money mapped onto it. */
|
|
243
|
+
function PagamentoStep({
|
|
244
|
+
c,
|
|
245
|
+
cart,
|
|
246
|
+
settlement,
|
|
247
|
+
providerConfig,
|
|
248
|
+
tenantSlug,
|
|
249
|
+
validateApplePayMerchant,
|
|
250
|
+
}: {
|
|
251
|
+
c: ReturnType<typeof useCheckoutController>;
|
|
252
|
+
cart: CheckoutCartView;
|
|
253
|
+
settlement: SettlementCheckout | null | undefined;
|
|
254
|
+
providerConfig: CheckoutProviderConfig | null | undefined;
|
|
255
|
+
tenantSlug: string | undefined;
|
|
256
|
+
validateApplePayMerchant: ((validationURL: string) => Promise<unknown>) | undefined;
|
|
257
|
+
}): JSX.Element {
|
|
258
|
+
return (
|
|
259
|
+
<PaymentStep
|
|
260
|
+
method={c.method}
|
|
261
|
+
onMethodChange={c.setMethod}
|
|
262
|
+
order={c.order}
|
|
263
|
+
buyer={c.buyer}
|
|
264
|
+
creating={c.creating}
|
|
265
|
+
createError={c.createError}
|
|
266
|
+
errorField={c.errorField}
|
|
267
|
+
errorCode={c.errorCode}
|
|
268
|
+
onGenerate={(chosen) => void c.startPayment(chosen)}
|
|
269
|
+
onUseEmail={c.payWithEmail}
|
|
270
|
+
// Set only for a skipped-Dados flow (the controller decides); the payer
|
|
271
|
+
// block hides itself when it is absent.
|
|
272
|
+
onEditBuyer={c.editBuyer}
|
|
273
|
+
providerConfig={providerConfig}
|
|
274
|
+
tenantSlug={tenantSlug}
|
|
275
|
+
// The amount, on the step that asks for it (FUT-1179).
|
|
276
|
+
cartTotals={cart}
|
|
277
|
+
totalOverride={settlementTotalOverride(settlement)}
|
|
278
|
+
discountLines={cart.discountLines}
|
|
279
|
+
// Retrying a refused card: the saved card that failed is not chosen for
|
|
280
|
+
// them again (FUT-1145).
|
|
281
|
+
freshInstrument={c.freshInstrument}
|
|
282
|
+
// The card path parks an order of its own for a 3-D Secure challenge, so
|
|
283
|
+
// it needs the same basket the flow was mounted for (FUT-1213).
|
|
284
|
+
basket={cart.identity}
|
|
285
|
+
validateApplePayMerchant={validateApplePayMerchant}
|
|
286
|
+
onResolved={c.handleResolved}
|
|
182
287
|
/>
|
|
183
288
|
);
|
|
184
289
|
}
|
|
@@ -190,7 +295,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
190
295
|
// any chain member may need rather than re-opening after the choice. A chain
|
|
191
296
|
// that declares nothing degrades to CPF-required, never to "ask nothing".
|
|
192
297
|
const buyerFields = useMemo(() => buyerFieldsFor(providerConfig?.chain, null), [providerConfig]);
|
|
193
|
-
const c = useCheckoutController(ports, defaultBuyer, taxIdOnFile, buyerFields, tenantSlug);
|
|
298
|
+
const c = useCheckoutController(ports, defaultBuyer, taxIdOnFile, buyerFields, tenantSlug, cart.identity);
|
|
194
299
|
const armed = useOneClick({ requested: oneClick, config: providerConfig, taxIdOnFile, step: c.step, method: c.method, setMethod: c.setMethod });
|
|
195
300
|
|
|
196
301
|
// A settlement settlement pays already-sent kitchen items — the cart is
|
|
@@ -200,7 +305,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
200
305
|
// Pagamento the first screen, so an empty cart would otherwise reach the
|
|
201
306
|
// method picker. It holds until an order exists — once one does, its lines are
|
|
202
307
|
// snapshotted server-side and the cart no longer speaks for it.
|
|
203
|
-
if (
|
|
308
|
+
if (nothingToPayFor(settlement, cart, c)) {
|
|
204
309
|
return <EmptyCart copy={copy.emptyCart} onBack={c.goToMenu} />;
|
|
205
310
|
}
|
|
206
311
|
|
|
@@ -229,24 +334,13 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
229
334
|
|
|
230
335
|
{c.step === "payment" ? (
|
|
231
336
|
<OneClickProvider armed={armed}>
|
|
232
|
-
<
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
buyer={c.buyer}
|
|
237
|
-
creating={c.creating}
|
|
238
|
-
createError={c.createError}
|
|
239
|
-
errorField={c.errorField}
|
|
240
|
-
errorCode={c.errorCode}
|
|
241
|
-
onGenerate={(chosen) => void c.startPayment(chosen)}
|
|
242
|
-
onUseEmail={c.payWithEmail}
|
|
243
|
-
// Set only for a skipped-Dados flow (the controller decides); the
|
|
244
|
-
// payer block hides itself when it is absent.
|
|
245
|
-
onEditBuyer={c.editBuyer}
|
|
337
|
+
<PagamentoStep
|
|
338
|
+
c={c}
|
|
339
|
+
cart={cart}
|
|
340
|
+
settlement={settlement}
|
|
246
341
|
providerConfig={providerConfig}
|
|
247
342
|
tenantSlug={tenantSlug}
|
|
248
343
|
validateApplePayMerchant={validateApplePayMerchant}
|
|
249
|
-
onResolved={c.handleResolved}
|
|
250
344
|
/>
|
|
251
345
|
</OneClickProvider>
|
|
252
346
|
) : null}
|
|
@@ -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),
|