@12-apps/payments-frontend 3.34.4 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.34.4",
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.",
@@ -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
- <Box sx={{ display: "flex", gap: 2 }}>
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
- /** Sticky primary "Pagar" action — pinned to the bottom of the viewport on mobile. */
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
- <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>
275
+ {button}
257
276
  </Box>
258
277
  );
259
278
  }
@@ -8,15 +8,30 @@ import {
8
8
  type CardTokenizationConfig,
9
9
  } from "../../card";
10
10
 
11
+ import { useRefreshOnCartChanged } from "./cart-changed";
11
12
  import { useCheckoutCopy } from "./copy-context";
12
13
  import { UNRESOLVED_CODE } from "./failure-codes";
13
14
  import { StalledWait } from "./stalled-wait";
14
15
  import type { CheckoutBasketIdentity } from "./basket";
15
16
  import type { CardChainLink } from "./method-capability";
16
17
  import type { BuyerInfo, CheckoutOrder, OnCheckoutResolved } from "./types";
18
+ import { useCheckoutLayout } from "./payment-step-layout";
17
19
  import { useCheckoutComponents } from "./ui";
18
20
  import { useCardCheckout, type CardCheckout } from "./use-card-checkout";
19
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
+
20
35
  /**
21
36
  * Post-submit confirmation state: timeout > error > spinner.
22
37
  *
@@ -179,8 +194,6 @@ export function CardView({
179
194
  */
180
195
  basket?: CheckoutBasketIdentity;
181
196
  }): JSX.Element {
182
- const { Text } = useCheckoutComponents();
183
- const copy = useCheckoutCopy().screens.card;
184
197
  const cc = useCardCheckout(
185
198
  order,
186
199
  buyer,
@@ -192,21 +205,22 @@ export function CardView({
192
205
  freshInstrument,
193
206
  { tenantSlug, basket },
194
207
  );
195
- // A charge NOBODY can confirm yet is not a decline (FUT-563). Some provider
196
- // may be holding the buyer's money, so it gets its own presentation: the
197
- // danger heading "Não foi possível pagar" contradicts the body's "não pague
198
- // de novo" and pushes the buyer toward exactly the retry it forbids.
208
+ // A charge NOBODY can confirm yet is not a decline (FUT-563): a provider may
209
+ // hold the money, and "Não foi possível pagar" over "não pague de novo"
210
+ // pushes the buyer toward exactly the retry it forbids.
199
211
  const unresolved = cc.errorCode === UNRESOLVED_CODE;
212
+ useRefreshOnCartChanged(cc.errorCode); // re-priced by the host (FUT-1139)
200
213
 
201
214
  if (cc.submitted) {
202
215
  return <SubmittedState card={cc} />;
203
216
  }
204
217
 
205
218
  return (
206
- <Box data-testid="card-view" sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
207
- <Text variant="heading" size="md" weight="bold" as="h2">
208
- {copy.heading}
209
- </Text>
219
+ <Box
220
+ data-testid="card-view"
221
+ sx={{ display: "flex", flexDirection: "column", gap: 2 }}
222
+ >
223
+ <CardHeading />
210
224
 
211
225
  <CardInstrumentFields card={cc} />
212
226
 
@@ -0,0 +1,273 @@
1
+ import { Box } from "@mui/material";
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ type JSX,
10
+ type ReactNode,
11
+ } from "react";
12
+
13
+ import type { CheckoutBasketIdentity } from "./basket";
14
+ import { useCheckoutCopy } from "./copy-context";
15
+ import type {
16
+ BuyerInfo,
17
+ CheckoutOrder,
18
+ CreateOrderRequest,
19
+ CreateOrderResult,
20
+ PaymentMethod,
21
+ } from "./types";
22
+ import { useCheckoutComponents } from "./ui";
23
+
24
+ /**
25
+ * THE CART MOVED UNDER A PREPARED PAYMENT (FUT-1139).
26
+ *
27
+ * An order is raised the moment the buyer picks a method, and it prices the
28
+ * cart as it stood THEN. Another tab can add a line, a store can move a price,
29
+ * a product can stop selling — and the card charge or the PIX code on screen
30
+ * still asks for the old amount. Paying it settles the old order and the
31
+ * server closes the cart, so whatever changed was neither charged nor kept.
32
+ *
33
+ * Two halves, both here:
34
+ *
35
+ * - the SERVER refuses a card charge whose order no longer matches the cart,
36
+ * with {@link CART_CHANGED_CODE}, after re-pricing the order to the cart as it
37
+ * stands. The card form then re-reads that order, so the pay bar shows the
38
+ * amount that will actually be charged before the buyer presses it again;
39
+ * - the SCREEN notices on its own that the basket it was raised from is not the
40
+ * basket in front of it (the host's signature, `./basket.ts`), and says so
41
+ * with one control that brings the payment up to date. That is the only
42
+ * warning a PIX buyer can get: a QR code is paid outside the app.
43
+ */
44
+
45
+ /** The refusal a host answers when the order no longer prices the cart. */
46
+ export const CART_CHANGED_CODE = "CART_CHANGED";
47
+
48
+ interface RaisedFrom {
49
+ order: CheckoutOrder;
50
+ signature: string | null;
51
+ }
52
+
53
+ /**
54
+ * Whether the basket in front of the checkout is no longer the one `order` was
55
+ * raised from.
56
+ *
57
+ * The basket is recorded when the ORDER changes — a re-raise or a refresh
58
+ * hands back a new object, so it is re-recorded and the warning clears — and
59
+ * only once the host's cart has answered (`ready`): a loading cart reports the
60
+ * EMPTY signature, and recording that would read every real cart as changed.
61
+ * A host that passes no basket never sees the warning, which is the behaviour
62
+ * before this existed.
63
+ */
64
+ export function useBasketChanged(
65
+ order: CheckoutOrder | null,
66
+ basket: CheckoutBasketIdentity | undefined,
67
+ ): boolean {
68
+ const [raised, setRaised] = useState<RaisedFrom | null>(null);
69
+ const ready = basket?.ready === true;
70
+ const signature = basket?.signature ?? null;
71
+ useEffect(() => {
72
+ setRaised((prev) => {
73
+ if (order === null) return null;
74
+ if (prev?.order === order || !ready) return prev;
75
+ return { order, signature };
76
+ });
77
+ // A cart change re-runs this and keeps `prev`: only a NEW order records, so
78
+ // the raised basket never follows the cart it is compared with.
79
+ }, [order, ready, signature]);
80
+ if (order === null || raised === null || raised.order !== order || !ready) return false;
81
+ return raised.signature !== signature;
82
+ }
83
+
84
+ /** The step's "bring this payment up to date", with what the buyer must see of it. */
85
+ export interface OrderRefresh {
86
+ run: () => void;
87
+ /** A refresh is on its way — the control waits rather than firing twice. */
88
+ pending: boolean;
89
+ /** The last refresh failed, in the host's words; null otherwise. */
90
+ error: string | null;
91
+ }
92
+
93
+ /**
94
+ * Re-price the order IN PLACE: ask the host for the order again and swap it in
95
+ * without dropping the one on screen first.
96
+ *
97
+ * Unlike raising a payment (`./start-payment.ts`), nothing is cleared before
98
+ * the answer: the card form keeps what the buyer typed, and the refusal that
99
+ * sent us here stays on screen while the amount under it corrects itself. The
100
+ * answer is routed exactly as a raise is (`route`, which is
101
+ * `routeRaisedOrder`), so a re-priced order that is already PAID or must finish
102
+ * on the provider's page goes where a raised one would.
103
+ *
104
+ * Three guards, each against something a review found:
105
+ *
106
+ * - one refresh at a time — a second tap while one is out does nothing;
107
+ * - an answer for an order the screen has moved on from (the buyer switched
108
+ * method, or raised the payment again) is DROPPED rather than resurrected;
109
+ * - a failure is KEPT here and shown by {@link CartChangedBoundary}, because the
110
+ * step's own create-error slot is hidden whenever an order is on screen.
111
+ */
112
+ export function useOrderRefresh(input: {
113
+ order: CheckoutOrder | null;
114
+ buyer: BuyerInfo;
115
+ saveProfile: boolean;
116
+ createOrder: (request: CreateOrderRequest) => Promise<CreateOrderResult>;
117
+ route: (order: CheckoutOrder) => void;
118
+ }): OrderRefresh {
119
+ const { order, buyer, saveProfile, createOrder, route } = input;
120
+ const offline = useCheckoutCopy().screens.transport.offline;
121
+ const [pending, setPending] = useState(false);
122
+ const [error, setError] = useState<string | null>(null);
123
+ const current = useRef(order);
124
+ const inFlight = useRef(false);
125
+ useEffect(() => {
126
+ current.current = order;
127
+ }, [order]);
128
+ // A failure is about the order it was raised for: once another is on screen
129
+ // (a method switch, a fresh raise) it is no longer true, and must not stay up.
130
+ const orderId = order?.orderId;
131
+ const method = order?.method;
132
+ useEffect(() => {
133
+ setError(null);
134
+ }, [orderId, method]);
135
+ const run = useCallback(() => {
136
+ if (!order || inFlight.current) return;
137
+ inFlight.current = true;
138
+ setPending(true);
139
+ setError(null);
140
+ void createOrder({ method: order.method, buyer, saveProfile })
141
+ .catch((): CreateOrderResult => ({ ok: false, error: { code: "NETWORK", message: offline, field: null } }))
142
+ .then((result) => {
143
+ inFlight.current = false;
144
+ setPending(false);
145
+ // The same order still on screen — by id and method, since a re-priced
146
+ // order comes back as a new object for the same id.
147
+ const now = current.current;
148
+ if (!now || now.orderId !== order.orderId || now.method !== order.method) return;
149
+ if (!result.ok) setError(result.error.message);
150
+ else route(result.data);
151
+ });
152
+ }, [order, buyer, saveProfile, createOrder, route, offline]);
153
+ return { run, pending, error };
154
+ }
155
+
156
+ /**
157
+ * How this step brings a payment up to date with the cart (FUT-1139): a PIX
158
+ * code is minted again — the amount is IN the code — and a card order is
159
+ * re-priced in place so the form keeps what was typed.
160
+ */
161
+ export function paymentRefresh(
162
+ order: CheckoutOrder | null,
163
+ onGenerate: (method: PaymentMethod) => void,
164
+ onRefreshOrder: (() => void) | undefined,
165
+ ): (() => void) | null {
166
+ if (!order) return null;
167
+ if (order.method === "CARD" && onRefreshOrder) return onRefreshOrder;
168
+ return () => onGenerate(order.method);
169
+ }
170
+
171
+ const PaymentRefreshContext = createContext<(() => void) | null>(null);
172
+
173
+ /** Hands the payment panes the step's "bring this payment up to date". */
174
+ export function PaymentRefreshProvider({
175
+ refresh,
176
+ children,
177
+ }: {
178
+ refresh: (() => void) | null;
179
+ children: ReactNode;
180
+ }): JSX.Element {
181
+ return <PaymentRefreshContext.Provider value={refresh}>{children}</PaymentRefreshContext.Provider>;
182
+ }
183
+
184
+ /**
185
+ * The card pane's half: a charge the host refused with
186
+ * {@link CART_CHANGED_CODE} re-reads the order, once per refusal.
187
+ */
188
+ export function useRefreshOnCartChanged(errorCode: string | null): void {
189
+ const refresh = useContext(PaymentRefreshContext);
190
+ // Held in a ref: the step hands down a fresh closure on every render, and
191
+ // keying the effect on it would re-fire the refresh after each re-price for
192
+ // as long as the refusal stays on screen.
193
+ const latest = useRef(refresh);
194
+ useEffect(() => {
195
+ latest.current = refresh;
196
+ }, [refresh]);
197
+ useEffect(() => {
198
+ if (errorCode === CART_CHANGED_CODE) latest.current?.();
199
+ }, [errorCode]);
200
+ }
201
+
202
+ /**
203
+ * The payment pane, with the step's warning above it when the cart moved, and
204
+ * the refresh handed down to the card form. Nothing changes on screen while the
205
+ * basket is the one the order was raised from.
206
+ */
207
+ export function CartChangedBoundary({
208
+ order,
209
+ basket,
210
+ creating,
211
+ onGenerate,
212
+ refresh,
213
+ children,
214
+ }: {
215
+ order: CheckoutOrder | null;
216
+ /** Absent for a settlement: it pays a balance, not the cart, so its basket is no signal. */
217
+ basket: CheckoutBasketIdentity | undefined;
218
+ creating: boolean;
219
+ onGenerate: (method: PaymentMethod) => void;
220
+ refresh: OrderRefresh | undefined;
221
+ children: ReactNode;
222
+ }): JSX.Element {
223
+ const changed = useBasketChanged(order, basket) && !creating;
224
+ const run = paymentRefresh(order, onGenerate, refresh?.run);
225
+ const pending = refresh?.pending ?? false;
226
+ const error = refresh?.error ?? null;
227
+ // ONE tree shape whether or not the warning shows: wrapping the pane only
228
+ // while it does would remount the card form the moment the refresh lands,
229
+ // dropping what the buyer typed and the refusal that explains the new total.
230
+ return (
231
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
232
+ {(changed || error) && run ? <CartChangedNotice onRefresh={run} pending={pending} error={error} /> : null}
233
+ <PaymentRefreshProvider refresh={run}>{children}</PaymentRefreshProvider>
234
+ </Box>
235
+ );
236
+ }
237
+
238
+ /** "Seu carrinho mudou" — what the step says, and the one thing to do about it. */
239
+ function CartChangedNotice({
240
+ onRefresh,
241
+ pending = false,
242
+ error = null,
243
+ }: {
244
+ onRefresh: () => void;
245
+ pending?: boolean;
246
+ /** A refresh that failed, in the host's words: said, with the same control to try again. */
247
+ error?: string | null;
248
+ }): JSX.Element {
249
+ const { Alert, Button } = useCheckoutComponents();
250
+ const copy = useCheckoutCopy().screens.cartChanged;
251
+ return (
252
+ <Box data-testid="checkout-cart-changed" sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
253
+ {error ? (
254
+ <Alert variant="danger" title={copy.refreshFailed} description={error} showIcon data-testid="checkout-cart-changed-error" />
255
+ ) : (
256
+ <Alert variant="warning" title={copy.heading} description={copy.support} showIcon />
257
+ )}
258
+ <Box>
259
+ <Button
260
+ variant="solid"
261
+ color="primary"
262
+ size="md"
263
+ onClick={onRefresh}
264
+ loading={pending}
265
+ disabled={pending}
266
+ dataTestId="checkout-cart-changed-refresh"
267
+ >
268
+ {copy.refreshAction}
269
+ </Button>
270
+ </Box>
271
+ </Box>
272
+ );
273
+ }
@@ -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, useCheckoutComponents, type CheckoutComponents } from "./ui";
12
+ import { CheckoutComponentsProvider, type CheckoutComponents } from "./ui";
15
13
  import type { CheckoutViewCopy } from "./view-copy";
16
- import { STEP_COLUMN_MAX } from "./payment-step-layout";
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
@@ -297,6 +271,7 @@ function PagamentoStep({
297
271
  errorField={c.errorField}
298
272
  errorCode={c.errorCode}
299
273
  onGenerate={(chosen) => void c.startPayment(chosen)}
274
+ refresh={c.refresh}
300
275
  onUseEmail={c.payWithEmail}
301
276
  // Set only for a skipped-Dados flow (the controller decides); the payer
302
277
  // block hides itself when it is absent.
@@ -307,8 +282,11 @@ function PagamentoStep({
307
282
  cartTotals={cart}
308
283
  totalOverride={settlementTotalOverride(settlement)}
309
284
  discountLines={cart.discountLines}
310
- // Retrying a refused card: the saved card that failed is not chosen for
311
- // them again (FUT-1145).
285
+ // The order around the payment, for the summary (FUT-3525).
286
+ lines={cart.lines}
287
+ delivery={cart.delivery}
288
+ orderNote={cart.orderNote}
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
314
292
  // it needs the same basket the flow was mounted for (FUT-1213).
@@ -341,46 +319,52 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
341
319
  }
342
320
 
343
321
  return (
344
- // Pagamento alone takes the host's full width (its PIX pane, FUT-3367).
345
- <Box sx={{ display: "flex", flexDirection: "column", gap: { xs: 2, sm: 3 }, width: "100%", mx: "auto", maxWidth: c.step === "payment" ? "none" : STEP_COLUMN_MAX }}>
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
- {c.step === "dados" ? (
351
- <DadosStep
352
- copy={copy.dados}
353
- buyer={c.buyer}
354
- onBuyerChange={c.setBuyer}
355
- saveProfile={c.saveProfile}
356
- onSaveProfileChange={c.setSaveProfile}
357
- createError={c.createError}
358
- errorField={c.errorField}
359
- onContinue={c.goToPayment}
360
- cartTotals={cart}
361
- buyerFields={buyerFields}
362
- discountLines={cart.discountLines}
363
- totalOverride={settlementTotalOverride(settlement)}
364
- />
365
- ) : null}
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
- {c.step === "payment" ? (
368
- <OneClickProvider armed={armed}>
369
- <PagamentoStep
370
- c={c}
371
- cart={cart}
372
- settlement={settlement}
373
- providerConfig={providerConfig}
374
- tenantSlug={tenantSlug}
375
- validateApplePayMerchant={validateApplePayMerchant}
376
- />
377
- </OneClickProvider>
378
- ) : null}
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
- {c.step === "status" ? (
381
- <StatusStep copy={copy} c={c} settlement={settlement} cart={cart} confirmationExtra={confirmationExtra} confirmationHero={confirmationHero} confirmationFooter={confirmationFooter} backActionEmphasis={backActionEmphasis} />
382
- ) : null}
383
- </Box>
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