@12-apps/payments-frontend 3.35.0 → 3.37.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/cart-changed.tsx +24 -3
- 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 +11 -4
- package/src/components/checkout/pix-pane.tsx +197 -161
- package/src/components/checkout/pix-parts.tsx +53 -5
- package/src/components/checkout/pix-view.tsx +37 -17
- package/src/components/checkout/screens-copy.ts +38 -3
- package/src/components/checkout/screens-en-US.ts +12 -0
- package/src/components/checkout/screens-pt-BR.ts +12 -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.37.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
|
|
|
@@ -199,6 +199,23 @@ export function useRefreshOnCartChanged(errorCode: string | null): void {
|
|
|
199
199
|
}, [errorCode]);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** Where a refresh stands, for a step that may not have one (a settlement). */
|
|
203
|
+
function refreshStatus(refresh: OrderRefresh | undefined): { pending: boolean; error: string | null } {
|
|
204
|
+
return { pending: refresh?.pending ?? false, error: refresh?.error ?? null };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const PaymentStaleContext = createContext(false);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether the payment on screen was prepared for a cart that has since changed
|
|
211
|
+
* (FUT-3535). A PIX pane reads it to veil its QR and code and to stop the copy
|
|
212
|
+
* controls: the amount is IN the code, so a buyer who pays it now pays the old
|
|
213
|
+
* order. False outside a {@link CartChangedBoundary}.
|
|
214
|
+
*/
|
|
215
|
+
export function usePaymentStale(): boolean {
|
|
216
|
+
return useContext(PaymentStaleContext);
|
|
217
|
+
}
|
|
218
|
+
|
|
202
219
|
/**
|
|
203
220
|
* The payment pane, with the step's warning above it when the cart moved, and
|
|
204
221
|
* the refresh handed down to the card form. Nothing changes on screen while the
|
|
@@ -222,15 +239,19 @@ export function CartChangedBoundary({
|
|
|
222
239
|
}): JSX.Element {
|
|
223
240
|
const changed = useBasketChanged(order, basket) && !creating;
|
|
224
241
|
const run = paymentRefresh(order, onGenerate, refresh?.run);
|
|
225
|
-
const pending = refresh
|
|
226
|
-
const error = refresh?.error ?? null;
|
|
242
|
+
const { pending, error } = refreshStatus(refresh);
|
|
227
243
|
// ONE tree shape whether or not the warning shows: wrapping the pane only
|
|
228
244
|
// while it does would remount the card form the moment the refresh lands,
|
|
229
245
|
// dropping what the buyer typed and the refusal that explains the new total.
|
|
230
246
|
return (
|
|
231
247
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
|
232
248
|
{(changed || error) && run ? <CartChangedNotice onRefresh={run} pending={pending} error={error} /> : null}
|
|
233
|
-
|
|
249
|
+
{/* Stale while the warning stands, so the veil goes with it the moment the
|
|
250
|
+
new order lands. A PIX refresh mints a new code, which takes the old
|
|
251
|
+
one off the screen first, so a failed one leaves nothing to copy. */}
|
|
252
|
+
<PaymentStaleContext.Provider value={changed && run !== null}>
|
|
253
|
+
<PaymentRefreshProvider refresh={run}>{children}</PaymentRefreshProvider>
|
|
254
|
+
</PaymentStaleContext.Provider>
|
|
234
255
|
</Box>
|
|
235
256
|
);
|
|
236
257
|
}
|
|
@@ -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
|
+
}
|