@12-apps/payments-frontend 3.35.0 → 3.36.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/card/fields.tsx +33 -14
- package/src/components/checkout/card-view.tsx +19 -6
- package/src/components/checkout/checkout-flow.tsx +63 -79
- package/src/components/checkout/checkout-frame.tsx +175 -0
- package/src/components/checkout/checkout-steps.tsx +34 -66
- package/src/components/checkout/icons.tsx +5 -0
- package/src/components/checkout/method-tabs.tsx +249 -0
- package/src/components/checkout/payment-status-parts.tsx +13 -3
- package/src/components/checkout/payment-step-frame.tsx +206 -0
- package/src/components/checkout/payment-step-layout.tsx +169 -14
- package/src/components/checkout/payment-summary-phone.tsx +158 -0
- package/src/components/checkout/payment-summary.tsx +290 -0
- package/src/components/checkout/pix-after-copy.tsx +1 -1
- package/src/components/checkout/pix-pane.tsx +172 -147
- package/src/components/checkout/pix-parts.tsx +6 -4
- package/src/components/checkout/pix-view.tsx +37 -17
- package/src/components/checkout/screens-copy.ts +32 -3
- package/src/components/checkout/screens-en-US.ts +11 -0
- package/src/components/checkout/screens-pt-BR.ts +11 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.36.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.",
|
package/src/card/fields.tsx
CHANGED
|
@@ -3,6 +3,10 @@ import type { JSX } from "react";
|
|
|
3
3
|
|
|
4
4
|
import { useCheckoutCopy } from "../components/checkout/copy-context";
|
|
5
5
|
import { useCheckoutComponents } from "../components/checkout/ui";
|
|
6
|
+
import {
|
|
7
|
+
StickyPayBar,
|
|
8
|
+
useCheckoutLayout,
|
|
9
|
+
} from "../components/checkout/payment-step-layout";
|
|
6
10
|
|
|
7
11
|
import {
|
|
8
12
|
cvvLength,
|
|
@@ -110,7 +114,9 @@ function ExpiryCvvFields({
|
|
|
110
114
|
const { Input } = useCheckoutComponents();
|
|
111
115
|
const copy = useCheckoutCopy().card.fields;
|
|
112
116
|
return (
|
|
113
|
-
|
|
117
|
+
// Side by side while "Validade (MM/AA)" fits whole; stacked below that
|
|
118
|
+
// (a phone's Pagamento card, FUT-3525) rather than cut to "Validade (MM/…".
|
|
119
|
+
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 2, "& > *": { flex: "1 1 170px" } }}>
|
|
114
120
|
<Input
|
|
115
121
|
label={copy.expiryLabel}
|
|
116
122
|
type="text"
|
|
@@ -216,7 +222,14 @@ export function NewCardForm({
|
|
|
216
222
|
);
|
|
217
223
|
}
|
|
218
224
|
|
|
219
|
-
/**
|
|
225
|
+
/**
|
|
226
|
+
* The primary "Pagar" action.
|
|
227
|
+
*
|
|
228
|
+
* On the Pagamento step it is the step's pay bar (FUT-3525): pinned to the
|
|
229
|
+
* bottom of the viewport with the total beside it on a phone, in the form on
|
|
230
|
+
* a computer. Composed anywhere else it keeps the look it always had —
|
|
231
|
+
* pinned to the bottom on a small screen, in the form on a wide one.
|
|
232
|
+
*/
|
|
220
233
|
export function CardPayBar({
|
|
221
234
|
totalLabel,
|
|
222
235
|
submitting,
|
|
@@ -227,6 +240,23 @@ export function CardPayBar({
|
|
|
227
240
|
onPay: () => void;
|
|
228
241
|
}): JSX.Element {
|
|
229
242
|
const { Button } = useCheckoutComponents();
|
|
243
|
+
const inStep = useCheckoutLayout() !== null;
|
|
244
|
+
const button = (
|
|
245
|
+
<Button
|
|
246
|
+
variant="solid"
|
|
247
|
+
color="primary"
|
|
248
|
+
size="lg"
|
|
249
|
+
fullWidth
|
|
250
|
+
loading={submitting}
|
|
251
|
+
disabled={submitting}
|
|
252
|
+
onClick={onPay}
|
|
253
|
+
dataTestId="card-pay"
|
|
254
|
+
>
|
|
255
|
+
Pagar {totalLabel}
|
|
256
|
+
</Button>
|
|
257
|
+
);
|
|
258
|
+
if (inStep)
|
|
259
|
+
return <StickyPayBar dataTestId="card-pay-bar">{button}</StickyPayBar>;
|
|
230
260
|
return (
|
|
231
261
|
<Box
|
|
232
262
|
data-testid="card-pay-bar"
|
|
@@ -242,18 +272,7 @@ export function CardPayBar({
|
|
|
242
272
|
borderColor: "divider",
|
|
243
273
|
}}
|
|
244
274
|
>
|
|
245
|
-
|
|
246
|
-
variant="solid"
|
|
247
|
-
color="primary"
|
|
248
|
-
size="lg"
|
|
249
|
-
fullWidth
|
|
250
|
-
loading={submitting}
|
|
251
|
-
disabled={submitting}
|
|
252
|
-
onClick={onPay}
|
|
253
|
-
dataTestId="card-pay"
|
|
254
|
-
>
|
|
255
|
-
Pagar {totalLabel}
|
|
256
|
-
</Button>
|
|
275
|
+
{button}
|
|
257
276
|
</Box>
|
|
258
277
|
);
|
|
259
278
|
}
|
|
@@ -15,9 +15,23 @@ import { StalledWait } from "./stalled-wait";
|
|
|
15
15
|
import type { CheckoutBasketIdentity } from "./basket";
|
|
16
16
|
import type { CardChainLink } from "./method-capability";
|
|
17
17
|
import type { BuyerInfo, CheckoutOrder, OnCheckoutResolved } from "./types";
|
|
18
|
+
import { useCheckoutLayout } from "./payment-step-layout";
|
|
18
19
|
import { useCheckoutComponents } from "./ui";
|
|
19
20
|
import { useCardCheckout, type CardCheckout } from "./use-card-checkout";
|
|
20
21
|
|
|
22
|
+
/** The pane's heading — left out inside the Pagamento step, where the "Cartão" tab names the pane (FUT-3525). */
|
|
23
|
+
function CardHeading(): JSX.Element | null {
|
|
24
|
+
const { Text } = useCheckoutComponents();
|
|
25
|
+
const copy = useCheckoutCopy().screens.card;
|
|
26
|
+
const inStep = useCheckoutLayout() !== null;
|
|
27
|
+
if (inStep) return null;
|
|
28
|
+
return (
|
|
29
|
+
<Text variant="heading" size="md" weight="bold" as="h2">
|
|
30
|
+
{copy.heading}
|
|
31
|
+
</Text>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
/**
|
|
22
36
|
* Post-submit confirmation state: timeout > error > spinner.
|
|
23
37
|
*
|
|
@@ -180,8 +194,6 @@ export function CardView({
|
|
|
180
194
|
*/
|
|
181
195
|
basket?: CheckoutBasketIdentity;
|
|
182
196
|
}): JSX.Element {
|
|
183
|
-
const { Text } = useCheckoutComponents();
|
|
184
|
-
const copy = useCheckoutCopy().screens.card;
|
|
185
197
|
const cc = useCardCheckout(
|
|
186
198
|
order,
|
|
187
199
|
buyer,
|
|
@@ -204,10 +216,11 @@ export function CardView({
|
|
|
204
216
|
}
|
|
205
217
|
|
|
206
218
|
return (
|
|
207
|
-
<Box
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
219
|
+
<Box
|
|
220
|
+
data-testid="card-view"
|
|
221
|
+
sx={{ display: "flex", flexDirection: "column", gap: 2 }}
|
|
222
|
+
>
|
|
223
|
+
<CardHeading />
|
|
211
224
|
|
|
212
225
|
<CardInstrumentFields card={cc} />
|
|
213
226
|
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import { Box } from "@mui/material";
|
|
2
1
|
import { useMemo, type JSX, type ReactNode } from "react";
|
|
3
2
|
|
|
4
3
|
import type { CheckoutBasketIdentity } from "./basket";
|
|
5
4
|
import { buyerFieldsFor } from "./buyer-fields";
|
|
6
5
|
import { EmptyCart, PaymentStep } from "./checkout-steps";
|
|
7
6
|
import { DadosStep } from "./dados-step";
|
|
8
|
-
import { ArrowBackIcon } from "./icons";
|
|
9
7
|
import { PaymentStatus } from "./payment-status";
|
|
10
8
|
import type { BackActionEmphasis } from "./payment-status-parts";
|
|
11
9
|
import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
12
10
|
import { CheckoutCopyProvider } from "./copy-context";
|
|
13
11
|
import { OneClickProvider, useOneClick } from "./one-click";
|
|
14
|
-
import { CheckoutComponentsProvider,
|
|
12
|
+
import { CheckoutComponentsProvider, type CheckoutComponents } from "./ui";
|
|
15
13
|
import type { CheckoutViewCopy } from "./view-copy";
|
|
16
|
-
import {
|
|
14
|
+
import { CheckoutFrame, FlowHeader } from "./checkout-frame";
|
|
15
|
+
import type { CheckoutDeliverySummary, CheckoutSummaryLine } from "./payment-summary";
|
|
17
16
|
import { useCheckoutController, type CheckoutHostPorts } from "./use-checkout-controller";
|
|
18
17
|
|
|
19
|
-
/** Step ids are the flow's own contract; the labels beside them are host copy. */
|
|
20
|
-
function stepperSteps(copy: CheckoutViewCopy): { id: string; label: string }[] {
|
|
21
|
-
return [
|
|
22
|
-
{ id: "dados", label: copy.steps.dados },
|
|
23
|
-
{ id: "payment", label: copy.steps.payment },
|
|
24
|
-
{ id: "status", label: copy.steps.status },
|
|
25
|
-
];
|
|
26
|
-
}
|
|
27
18
|
|
|
28
19
|
/** What the flow reads off the host's cart — display facts, never money math. */
|
|
29
20
|
export interface CheckoutCartView {
|
|
@@ -33,6 +24,19 @@ export interface CheckoutCartView {
|
|
|
33
24
|
totalItems: number;
|
|
34
25
|
/** Host-rendered discount itemization under the pay-bar total (FUT-246). */
|
|
35
26
|
discountLines?: ReactNode;
|
|
27
|
+
/**
|
|
28
|
+
* The cart's lines, worded by the host, for the order summary the Pagamento
|
|
29
|
+
* step shows beside the payment on a computer (FUT-3525). Absent, the
|
|
30
|
+
* summary shows the total alone.
|
|
31
|
+
*/
|
|
32
|
+
lines?: CheckoutSummaryLine[];
|
|
33
|
+
/**
|
|
34
|
+
* Where the order is delivered, and the host's way back to change it — set
|
|
35
|
+
* only for a delivery (FUT-3525). Absent, no address is shown.
|
|
36
|
+
*/
|
|
37
|
+
delivery?: CheckoutDeliverySummary;
|
|
38
|
+
/** The host's note control, shown in the Pagamento step's summary (FUT-3525). */
|
|
39
|
+
orderNote?: ReactNode;
|
|
36
40
|
/**
|
|
37
41
|
* WHICH basket this is, so a payment raised from another one cannot resume
|
|
38
42
|
* itself over it (FUT-1213).
|
|
@@ -147,36 +151,6 @@ function statusTotalLabel(
|
|
|
147
151
|
return order?.totalLabel ?? settlement?.totalLabel ?? cart.totalLabel;
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
/**
|
|
151
|
-
* The slim checkout header — the flow's only nav: a step-aware back link
|
|
152
|
-
* (Pagamento → Dados, else the menu).
|
|
153
|
-
*
|
|
154
|
-
* Deliberately nothing else. A "limpar carrinho" trash used to sit on the right
|
|
155
|
-
* of the Dados step; it was removed because emptying the cart is a CART action
|
|
156
|
-
* and belongs where the cart is edited — the drawer, which already offers a
|
|
157
|
-
* per-line remove. Offering it here put a destructive control next to the form
|
|
158
|
-
* a buyer is filling in, one tap away from the field they are typing into.
|
|
159
|
-
*/
|
|
160
|
-
function CheckoutHeader({ copy, step, onBack }: { copy: CheckoutViewCopy; step: string; onBack: () => void }): JSX.Element {
|
|
161
|
-
const { Button } = useCheckoutComponents();
|
|
162
|
-
return (
|
|
163
|
-
<Box sx={{ minHeight: 36, display: "flex", alignItems: "center", gap: 1 }}>
|
|
164
|
-
<Button variant="text" color="neutral" size="sm" icon={<ArrowBackIcon fontSize="small" />} iconPosition="left" onClick={onBack} dataTestId="checkout-back">
|
|
165
|
-
{step === "dados" ? copy.dados.keepShopping : copy.dados.back}
|
|
166
|
-
</Button>
|
|
167
|
-
</Box>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Compact 50px progress header so the form stays above the fold on mobile. */
|
|
172
|
-
function ProgressHeader({ copy, step, completed }: { copy: CheckoutViewCopy; step: string; completed: Set<string> }): JSX.Element {
|
|
173
|
-
const { Stepper } = useCheckoutComponents();
|
|
174
|
-
return (
|
|
175
|
-
<Box sx={{ height: 50, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
|
176
|
-
<Stepper steps={stepperSteps(copy)} activeId={step} completed={completed} orientation="horizontal" size="sm" data-testid="checkout-stepper" />
|
|
177
|
-
</Box>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
154
|
|
|
181
155
|
/**
|
|
182
156
|
* The 3-step flow body, below the slot provider. Ported from the storefront's
|
|
@@ -308,6 +282,10 @@ function PagamentoStep({
|
|
|
308
282
|
cartTotals={cart}
|
|
309
283
|
totalOverride={settlementTotalOverride(settlement)}
|
|
310
284
|
discountLines={cart.discountLines}
|
|
285
|
+
// The order around the payment, for the summary (FUT-3525).
|
|
286
|
+
lines={cart.lines}
|
|
287
|
+
delivery={cart.delivery}
|
|
288
|
+
orderNote={cart.orderNote}
|
|
311
289
|
// Retrying a refused card: the failed saved card is not chosen again (FUT-1145).
|
|
312
290
|
freshInstrument={c.freshInstrument}
|
|
313
291
|
// The card path parks an order of its own for a 3-D Secure challenge, so
|
|
@@ -341,46 +319,52 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
341
319
|
}
|
|
342
320
|
|
|
343
321
|
return (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<CheckoutHeader copy={copy} step={c.step} onBack={c.back} />
|
|
347
|
-
|
|
348
|
-
<ProgressHeader copy={copy} step={c.step} completed={c.completed} />
|
|
322
|
+
<CheckoutFrame payment={c.step === "payment"}>
|
|
323
|
+
<FlowHeader copy={copy} step={c.step} completed={c.completed} onBack={c.back} />
|
|
349
324
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
325
|
+
{c.step === "dados" ? (
|
|
326
|
+
<DadosStep
|
|
327
|
+
copy={copy.dados}
|
|
328
|
+
buyer={c.buyer}
|
|
329
|
+
onBuyerChange={c.setBuyer}
|
|
330
|
+
saveProfile={c.saveProfile}
|
|
331
|
+
onSaveProfileChange={c.setSaveProfile}
|
|
332
|
+
createError={c.createError}
|
|
333
|
+
errorField={c.errorField}
|
|
334
|
+
onContinue={c.goToPayment}
|
|
335
|
+
cartTotals={cart}
|
|
336
|
+
buyerFields={buyerFields}
|
|
337
|
+
discountLines={cart.discountLines}
|
|
338
|
+
totalOverride={settlementTotalOverride(settlement)}
|
|
339
|
+
/>
|
|
340
|
+
) : null}
|
|
366
341
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
342
|
+
{c.step === "payment" ? (
|
|
343
|
+
<OneClickProvider armed={armed}>
|
|
344
|
+
<PagamentoStep
|
|
345
|
+
c={c}
|
|
346
|
+
cart={cart}
|
|
347
|
+
settlement={settlement}
|
|
348
|
+
providerConfig={providerConfig}
|
|
349
|
+
tenantSlug={tenantSlug}
|
|
350
|
+
validateApplePayMerchant={validateApplePayMerchant}
|
|
351
|
+
/>
|
|
352
|
+
</OneClickProvider>
|
|
353
|
+
) : null}
|
|
379
354
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
355
|
+
{c.step === "status" ? (
|
|
356
|
+
<StatusStep
|
|
357
|
+
copy={copy}
|
|
358
|
+
c={c}
|
|
359
|
+
settlement={settlement}
|
|
360
|
+
cart={cart}
|
|
361
|
+
confirmationExtra={confirmationExtra}
|
|
362
|
+
confirmationHero={confirmationHero}
|
|
363
|
+
confirmationFooter={confirmationFooter}
|
|
364
|
+
backActionEmphasis={backActionEmphasis}
|
|
365
|
+
/>
|
|
366
|
+
) : null}
|
|
367
|
+
</CheckoutFrame>
|
|
384
368
|
);
|
|
385
369
|
}
|
|
386
370
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Box } from "@mui/material";
|
|
2
|
+
import { useMemo, type JSX, type ReactNode } from "react";
|
|
3
|
+
|
|
4
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
5
|
+
import { ArrowBackIcon } from "./icons";
|
|
6
|
+
import { CheckoutLayoutProvider, STEP_COLUMN_MAX, useCheckoutLayout, useWideLayout } from "./payment-step-layout";
|
|
7
|
+
import { useCheckoutComponents } from "./ui";
|
|
8
|
+
import type { CheckoutViewCopy } from "./view-copy";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The flow's frame and header: measured once at the host's full width
|
|
12
|
+
* (FUT-3525), so every step agrees on whether this is the computer's layout or
|
|
13
|
+
* the phone's.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Step ids are the flow's own contract; the labels beside them are host copy. */
|
|
17
|
+
function stepperSteps(copy: CheckoutViewCopy): { id: string; label: string }[] {
|
|
18
|
+
return [
|
|
19
|
+
{ id: "dados", label: copy.steps.dados },
|
|
20
|
+
{ id: "payment", label: copy.steps.payment },
|
|
21
|
+
{ id: "status", label: copy.steps.status },
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The slim checkout header — the flow's only nav: a step-aware back link
|
|
27
|
+
* (Pagamento → Dados, else the menu).
|
|
28
|
+
*
|
|
29
|
+
* Deliberately nothing else. A "limpar carrinho" trash used to sit on the right
|
|
30
|
+
* of the Dados step; it was removed because emptying the cart is a CART action
|
|
31
|
+
* and belongs where the cart is edited — the drawer, which already offers a
|
|
32
|
+
* per-line remove. Offering it here put a destructive control next to the form
|
|
33
|
+
* a buyer is filling in, one tap away from the field they are typing into.
|
|
34
|
+
*/
|
|
35
|
+
function CheckoutHeader({ copy, step, onBack }: { copy: CheckoutViewCopy; step: string; onBack: () => void }): JSX.Element {
|
|
36
|
+
const { Button } = useCheckoutComponents();
|
|
37
|
+
return (
|
|
38
|
+
<Box sx={{ minHeight: 36, display: "flex", alignItems: "center", gap: 1 }}>
|
|
39
|
+
<Button variant="text" color="neutral" size="sm" icon={<ArrowBackIcon fontSize="small" />} iconPosition="left" onClick={onBack} dataTestId="checkout-back">
|
|
40
|
+
{step === "dados" ? copy.dados.keepShopping : copy.dados.back}
|
|
41
|
+
</Button>
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The phone's header (FUT-3525, layout C): "← Voltar" with "Etapa 2 de 3 ·
|
|
48
|
+
* Pagamento" on the same row, and a thin bar filled to the step — the dots
|
|
49
|
+
* of the computer's stepper cost a phone a row for the same fact.
|
|
50
|
+
*/
|
|
51
|
+
function CompactHeader({
|
|
52
|
+
copy,
|
|
53
|
+
step,
|
|
54
|
+
onBack,
|
|
55
|
+
}: {
|
|
56
|
+
copy: CheckoutViewCopy;
|
|
57
|
+
step: string;
|
|
58
|
+
onBack: () => void;
|
|
59
|
+
}): JSX.Element {
|
|
60
|
+
const { Button, Text } = useCheckoutComponents();
|
|
61
|
+
const layout = useCheckoutCopy().screens.layout;
|
|
62
|
+
const steps = stepperSteps(copy);
|
|
63
|
+
const at = Math.max(
|
|
64
|
+
0,
|
|
65
|
+
steps.findIndex((item) => item.id === step),
|
|
66
|
+
);
|
|
67
|
+
const current = steps[at];
|
|
68
|
+
return (
|
|
69
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
70
|
+
<Box
|
|
71
|
+
sx={{
|
|
72
|
+
minHeight: 44,
|
|
73
|
+
display: "flex",
|
|
74
|
+
alignItems: "center",
|
|
75
|
+
justifyContent: "space-between",
|
|
76
|
+
gap: 1,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{/* Its label on the column's edge, as the prototype draws it: the text
|
|
80
|
+
button's own padding would push it 12px in. */}
|
|
81
|
+
<Box sx={{ ml: -1.5 }}> <Button
|
|
82
|
+
variant="text"
|
|
83
|
+
color="neutral"
|
|
84
|
+
size="sm"
|
|
85
|
+
icon={<ArrowBackIcon fontSize="small" />}
|
|
86
|
+
iconPosition="left"
|
|
87
|
+
onClick={onBack}
|
|
88
|
+
dataTestId="checkout-back"
|
|
89
|
+
>
|
|
90
|
+
{step === "dados" ? copy.dados.keepShopping : copy.dados.back}
|
|
91
|
+
</Button>
|
|
92
|
+
</Box>
|
|
93
|
+
<Text
|
|
94
|
+
variant="body"
|
|
95
|
+
size="xs"
|
|
96
|
+
as="span"
|
|
97
|
+
data-testid="checkout-step-progress"
|
|
98
|
+
>
|
|
99
|
+
{layout.stepProgress(at + 1, steps.length, current?.label ?? "")}
|
|
100
|
+
</Text>
|
|
101
|
+
</Box>
|
|
102
|
+
<Box
|
|
103
|
+
// The bar draws the sentence beside it; a reader hears that once.
|
|
104
|
+
aria-hidden
|
|
105
|
+
data-testid="checkout-stepper"
|
|
106
|
+
sx={{
|
|
107
|
+
height: 4,
|
|
108
|
+
borderRadius: 2,
|
|
109
|
+
bgcolor: "divider",
|
|
110
|
+
overflow: "hidden",
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<Box
|
|
114
|
+
sx={{
|
|
115
|
+
height: "100%",
|
|
116
|
+
width: `${((at + 1) / steps.length) * 100}%`,
|
|
117
|
+
bgcolor: "primary.main",
|
|
118
|
+
borderRadius: 2,
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
</Box>
|
|
122
|
+
</Box>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Compact 50px progress header so the form stays above the fold on mobile. */
|
|
127
|
+
function ProgressHeader({ copy, step, completed }: { copy: CheckoutViewCopy; step: string; completed: Set<string> }): JSX.Element {
|
|
128
|
+
const { Stepper } = useCheckoutComponents();
|
|
129
|
+
return (
|
|
130
|
+
<Box sx={{ height: 50, display: "flex", flexDirection: "column", justifyContent: "center" }}>
|
|
131
|
+
<Stepper steps={stepperSteps(copy)} activeId={step} completed={completed} orientation="horizontal" size="sm" data-testid="checkout-stepper" />
|
|
132
|
+
</Box>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The header for the frame the flow is in: back link and stepper on a computer, one compact row on a phone. */
|
|
137
|
+
export function FlowHeader({
|
|
138
|
+
copy,
|
|
139
|
+
step,
|
|
140
|
+
completed,
|
|
141
|
+
onBack,
|
|
142
|
+
}: {
|
|
143
|
+
copy: CheckoutViewCopy;
|
|
144
|
+
step: string;
|
|
145
|
+
completed: Set<string>;
|
|
146
|
+
onBack: () => void;
|
|
147
|
+
}): JSX.Element {
|
|
148
|
+
const wide = useCheckoutLayout()?.wide ?? false;
|
|
149
|
+
if (!wide) return <CompactHeader copy={copy} step={step} onBack={onBack} />;
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<CheckoutHeader copy={copy} step={step} onBack={onBack} />
|
|
153
|
+
<ProgressHeader copy={copy} step={step} completed={completed} />
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Measured at the HOST's full width: one frame for the whole flow. Pagamento
|
|
160
|
+
* alone takes that width (its summary column); every other step keeps the
|
|
161
|
+
* 672px column it always had.
|
|
162
|
+
*/
|
|
163
|
+
export function CheckoutFrame({ payment, children }: { payment: boolean; children: ReactNode }): JSX.Element {
|
|
164
|
+
const [wide, frame] = useWideLayout();
|
|
165
|
+
const layout = useMemo(() => ({ wide }), [wide]);
|
|
166
|
+
return (
|
|
167
|
+
<Box ref={frame} sx={{ width: "100%" }}>
|
|
168
|
+
<CheckoutLayoutProvider value={layout}>
|
|
169
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: { xs: 2, sm: 3 }, width: "100%", mx: "auto", maxWidth: payment ? "none" : STEP_COLUMN_MAX }}>
|
|
170
|
+
{children}
|
|
171
|
+
</Box>
|
|
172
|
+
</CheckoutLayoutProvider>
|
|
173
|
+
</Box>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -2,15 +2,14 @@ import { Box } from "@mui/material";
|
|
|
2
2
|
import { useEffect, useRef, type JSX, type ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import type { CheckoutBasketIdentity } from "./basket";
|
|
5
|
-
import { displayTotals, PayBarTotal } from "./checkout-totals";
|
|
6
5
|
import { CartChangedBoundary, type OrderRefresh } from "./cart-changed";
|
|
7
6
|
import { useCheckoutCopy } from "./copy-context";
|
|
8
7
|
import { useMethodChoice } from "./method-choice";
|
|
9
|
-
import { MethodPicker } from "./method-picker";
|
|
10
8
|
import { PixStageProvider } from "./pix-stage";
|
|
11
|
-
import {
|
|
9
|
+
import { PayBarTotalProvider, useCheckoutLayout, usePaymentPixStage } from "./payment-step-layout";
|
|
12
10
|
import { PaymentErrorPanel } from "./payment-error-panel";
|
|
13
|
-
import {
|
|
11
|
+
import { PaymentStepFrame, summaryParts } from "./payment-step-frame";
|
|
12
|
+
import type { CheckoutDeliverySummary, CheckoutSummaryLine, OrderSummaryParts } from "./payment-summary";
|
|
14
13
|
import { resolveCheckoutScreen } from "./providers/registry";
|
|
15
14
|
import type {
|
|
16
15
|
BuyerField,
|
|
@@ -124,48 +123,6 @@ export function EmptyCart({ copy, onBack }: { copy: EmptyCartCopy; onBack: () =>
|
|
|
124
123
|
);
|
|
125
124
|
}
|
|
126
125
|
|
|
127
|
-
/** What the step knows about the amount: the charge's, the balance's, the cart's. */
|
|
128
|
-
interface StepMoney {
|
|
129
|
-
order: CheckoutOrder | null;
|
|
130
|
-
cartTotals?: { totalLabel: string; totalItems: number };
|
|
131
|
-
totalOverride?: { label: string; items: number };
|
|
132
|
-
discountLines?: ReactNode;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* The Pagamento step's own money line (FUT-1179).
|
|
137
|
-
*
|
|
138
|
-
* THE RAISED ORDER'S TOTAL WINS, exactly as the confirmation screen's does
|
|
139
|
-
* (`statusTotalLabel`). Once a charge exists, that charge's amount is what is
|
|
140
|
-
* being taken — a repriced cart, or an order resumed from a parked entry, makes
|
|
141
|
-
* the cart's total a different number from the one about to leave the buyer's
|
|
142
|
-
* account. Showing the cart's number above a QR for the order's would be this
|
|
143
|
-
* ticket's own defect in a new place: an amount on screen that is not the
|
|
144
|
-
* amount being charged.
|
|
145
|
-
*
|
|
146
|
-
* The item COUNT stays the cart's — an order carries no line count, and the
|
|
147
|
-
* caption is a description of the basket rather than of the charge.
|
|
148
|
-
*
|
|
149
|
-
* Renders nothing at all when the host supplied no totals and no order — a step
|
|
150
|
-
* that would otherwise print "Total ·" with a blank beside it is worse than the
|
|
151
|
-
* silence this ticket is about. Every host mounting `CheckoutFlow` gets them;
|
|
152
|
-
* only a hand-composed step can be missing them.
|
|
153
|
-
*/
|
|
154
|
-
function PaymentStepTotal({ money }: { money: StepMoney }): JSX.Element | null {
|
|
155
|
-
const { order, cartTotals, totalOverride, discountLines } = money;
|
|
156
|
-
if (!cartTotals && !totalOverride && !order) return null;
|
|
157
|
-
const { label, items } = displayTotals(totalOverride, cartTotals ?? { totalLabel: "", totalItems: 0 });
|
|
158
|
-
return (
|
|
159
|
-
<Box data-testid="payment-step-total" sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 2 }}>
|
|
160
|
-
<PayBarTotal totalLabel={order?.totalLabel ?? label} totalItems={items}>
|
|
161
|
-
{/* Suppressed while settling a balance: those totals come from the
|
|
162
|
-
frozen ticket, not the cart. */}
|
|
163
|
-
{totalOverride ? null : discountLines}
|
|
164
|
-
</PayBarTotal>
|
|
165
|
-
</Box>
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
126
|
/** Props for {@link PaymentStep} — step 2's inputs, wired by the flow. */
|
|
170
127
|
interface PaymentStepProps {
|
|
171
128
|
method: PaymentMethod | null;
|
|
@@ -221,6 +178,12 @@ interface PaymentStepProps {
|
|
|
221
178
|
totalOverride?: { label: string; items: number };
|
|
222
179
|
/** The host's own rendered discount itemization, under the total (FUT-246). */
|
|
223
180
|
discountLines?: ReactNode;
|
|
181
|
+
/** The cart's lines, listed in the computer's order summary (FUT-3525). */
|
|
182
|
+
lines?: CheckoutSummaryLine[];
|
|
183
|
+
/** Where a delivery goes — Entrega only; absent hides the row (FUT-3525). */
|
|
184
|
+
delivery?: CheckoutDeliverySummary;
|
|
185
|
+
/** The host's note control, shown in the summary (FUT-3525). */
|
|
186
|
+
orderNote?: ReactNode;
|
|
224
187
|
pollIntervalMs?: number;
|
|
225
188
|
/** Retrying a refused card — preselect no saved instrument (FUT-1145). */
|
|
226
189
|
freshInstrument?: boolean;
|
|
@@ -254,6 +217,9 @@ export function PaymentStep({
|
|
|
254
217
|
cartTotals,
|
|
255
218
|
totalOverride,
|
|
256
219
|
discountLines,
|
|
220
|
+
lines,
|
|
221
|
+
delivery,
|
|
222
|
+
orderNote,
|
|
257
223
|
pollIntervalMs,
|
|
258
224
|
freshInstrument,
|
|
259
225
|
basket,
|
|
@@ -265,23 +231,23 @@ export function PaymentStep({
|
|
|
265
231
|
useAutoRaiseOrder(order, method, creating, createError, onGenerate);
|
|
266
232
|
const pixStage = usePaymentPixStage(choice, onMethodChange);
|
|
267
233
|
|
|
234
|
+
const wide = useCheckoutLayout()?.wide ?? false;
|
|
235
|
+
const parts = summaryParts(
|
|
236
|
+
{ order, cartTotals, totalOverride, discountLines },
|
|
237
|
+
{ lines, payer: payerOf(buyer, onEditBuyer), delivery, orderNote },
|
|
238
|
+
);
|
|
239
|
+
const tabs = {
|
|
240
|
+
show: !(choice.atProvider || pixStage.afterCopy),
|
|
241
|
+
method,
|
|
242
|
+
onChange: onMethodChange,
|
|
243
|
+
cardUnavailable: choice.cardUnavailable,
|
|
244
|
+
offered: choice.offered,
|
|
245
|
+
};
|
|
246
|
+
|
|
268
247
|
return (
|
|
269
248
|
<PixStageProvider stage={pixStage}>
|
|
270
|
-
<
|
|
271
|
-
{
|
|
272
|
-
bar of its own: a second bar would put two "pay" controls on one screen. */}
|
|
273
|
-
<PaymentStepTotal money={{ order, cartTotals, totalOverride, discountLines }} />
|
|
274
|
-
|
|
275
|
-
{/* Self-hiding: renders only for a flow whose Dados step was skipped. */}
|
|
276
|
-
<StepColumn>
|
|
277
|
-
<PayerSummary name={buyer.name} taxId={buyer.taxId} onEdit={onEditBuyer} />
|
|
278
|
-
</StepColumn>
|
|
279
|
-
|
|
280
|
-
{choice.atProvider || pixStage.afterCopy ? null : (
|
|
281
|
-
<MethodPicker value={method} onChange={onMethodChange} cardUnavailable={choice.cardUnavailable} offered={choice.offered} />
|
|
282
|
-
)}
|
|
283
|
-
|
|
284
|
-
<StepColumn full={order?.method === "PIX"}>
|
|
249
|
+
<PayBarTotalProvider value={parts?.totalLabel ?? null}>
|
|
250
|
+
<PaymentStepFrame wide={wide} parts={parts} tabs={tabs} filled={Boolean(method || order)}>
|
|
285
251
|
<CartChangedBoundary {...{ order, creating, onGenerate, refresh }} basket={totalOverride ? undefined : basket}>
|
|
286
252
|
<PaymentBody
|
|
287
253
|
order={order}
|
|
@@ -298,9 +264,6 @@ export function PaymentStep({
|
|
|
298
264
|
validateApplePayMerchant={validateApplePayMerchant}
|
|
299
265
|
/>
|
|
300
266
|
</CartChangedBoundary>
|
|
301
|
-
</StepColumn>
|
|
302
|
-
|
|
303
|
-
<StepColumn>
|
|
304
267
|
<RaisingState
|
|
305
268
|
order={order}
|
|
306
269
|
method={method}
|
|
@@ -311,12 +274,17 @@ export function PaymentStep({
|
|
|
311
274
|
onUseEmail={onUseEmail}
|
|
312
275
|
onGenerate={onGenerate}
|
|
313
276
|
/>
|
|
314
|
-
</
|
|
315
|
-
</
|
|
277
|
+
</PaymentStepFrame>
|
|
278
|
+
</PayBarTotalProvider>
|
|
316
279
|
</PixStageProvider>
|
|
317
280
|
);
|
|
318
281
|
}
|
|
319
282
|
|
|
283
|
+
/** The payer line's facts — only for a flow whose Dados step was skipped (FUT-465). */
|
|
284
|
+
function payerOf(buyer: BuyerInfo, onEditBuyer: (() => void) | undefined): OrderSummaryParts["payer"] {
|
|
285
|
+
return onEditBuyer ? { name: buyer.name, taxId: buyer.taxId, onEdit: onEditBuyer } : undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
320
288
|
/**
|
|
321
289
|
* What the shell says while the order is being raised, and if raising it
|
|
322
290
|
* failed.
|
|
@@ -69,3 +69,8 @@ export const QrCodeIcon = makeIcon([
|
|
|
69
69
|
]);
|
|
70
70
|
|
|
71
71
|
export const CheckIcon = makeIcon(['M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z']);
|
|
72
|
+
|
|
73
|
+
export const PlaceOutlinedIcon = makeIcon([
|
|
74
|
+
'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7M7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9',
|
|
75
|
+
'M12 6.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5',
|
|
76
|
+
]);
|