@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.35.0",
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.",
@@ -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
  }
@@ -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 data-testid="card-view" sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
208
- <Text variant="heading" size="md" weight="bold" as="h2">
209
- {copy.heading}
210
- </Text>
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?.pending ?? false;
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
- <PaymentRefreshProvider refresh={run}>{children}</PaymentRefreshProvider>
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, 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
@@ -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
- // 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
 
@@ -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
+ }