@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "3.35.0",
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
  }
@@ -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
 
@@ -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
+ }
@@ -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 { StepColumn, usePaymentPixStage } from "./payment-step-layout";
9
+ import { PayBarTotalProvider, useCheckoutLayout, usePaymentPixStage } from "./payment-step-layout";
12
10
  import { PaymentErrorPanel } from "./payment-error-panel";
13
- import { PayerSummary } from "./payer-summary";
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
- <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
271
- {/* THE AMOUNT, before anything asks for it (FUT-1179) — at the top, not in a
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
- </StepColumn>
315
- </Box>
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
+ ]);