@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.
@@ -1,29 +1,27 @@
1
1
  import { Box } from "@mui/material";
2
- import { useId, type JSX, type KeyboardEvent, type ReactNode } from "react";
2
+ import type { JSX, ReactNode } from "react";
3
3
 
4
4
  import { useCheckoutCopy } from "./copy-context";
5
5
  import { ContentCopyIcon } from "./icons";
6
+ import { StickyPayBar, useCheckoutLayout } from "./payment-step-layout";
6
7
  import { Emphasized, PIX_WIDE, PixQr } from "./pix-parts";
7
8
  import type { PixCharge } from "./types";
8
9
  import { useCheckoutComponents } from "./ui";
9
10
 
10
11
  /**
11
- * The PIX pane before a copy (the 2026-10-06 Pix redesign).
12
+ * The PIX pane before a copy (the 2026-10-06 Pix redesign, laid out again by
13
+ * the 2026-10-11 Pagamento redesign, FUT-3525).
12
14
  *
13
- * Narrow — a phone — it is two tabs, "Copia e cola" first: the buyer pays on
14
- * the phone they are holding and cannot scan a QR drawn on that same screen,
15
- * so the code and its full-width button come before anything else. Wide — a
16
- * desktop — the same two panels stand side by side with "ou" between them:
17
- * the QR, for the phone in the buyer's hand, and the code, for their internet
18
- * banking.
15
+ * Narrow — a phone — the code and its button come first and the QR follows,
16
+ * "Ou pague de outro aparelho": the buyer pays on the phone they are holding
17
+ * and cannot scan a QR drawn on that same screen. Wide — a computer — the two
18
+ * stand side by side, the QR on the left. The Copia/QR tabs this replaced hid
19
+ * the QR behind a tap the phone's own buyer never needed.
19
20
  *
20
21
  * Both panels are always mounted and the switch is CSS (`PIX_WIDE`), so the
21
- * layout needs no measurement: the first paint is already right, and the QR is
22
- * in the document whichever tab is showing.
22
+ * layout needs no measurement: the first paint is already right.
23
23
  */
24
24
 
25
- export type PixTab = "copy" | "qr";
26
-
27
25
  /** A narrow-only element: shown under the container query's threshold. */
28
26
  const NARROW_ONLY = { [PIX_WIDE]: { display: "none" } } as const;
29
27
 
@@ -32,120 +30,82 @@ function wideOnly(display: string) {
32
30
  return { display: "none", [PIX_WIDE]: { display } } as const;
33
31
  }
34
32
 
35
- /** A tab panel: shown when its tab is picked, and always in the wide layout. */
36
- function panelSx(shown: boolean) {
37
- return {
38
- display: shown ? "flex" : "none",
39
- flexDirection: "column",
40
- gap: 1.75,
41
- minWidth: 0,
42
- [PIX_WIDE]: { display: "flex" },
43
- } as const;
44
- }
45
-
46
- /** The small capitals over each wide column. Uppercase by CSS only. */
47
- function ColumnHeading({ children, center }: { children: ReactNode; center?: boolean }): JSX.Element {
33
+ /** The small capitals over each column. Uppercase by CSS only. */
34
+ function ColumnHeading({
35
+ children,
36
+ center,
37
+ sx,
38
+ }: {
39
+ children: ReactNode;
40
+ center?: boolean;
41
+ sx?: object;
42
+ }): JSX.Element {
48
43
  const { Text } = useCheckoutComponents();
49
44
  return (
50
- <Box sx={{ ...wideOnly("block"), textAlign: center ? "center" : "left" }}>
51
- <Text variant="body" size="xs" weight="bold" color="secondary" as="p" style={{ textTransform: "uppercase", letterSpacing: "0.04em", margin: 0 }}>
45
+ <Box sx={{ textAlign: center ? "center" : "left", ...sx }}>
46
+ <Text
47
+ variant="body"
48
+ size="xs"
49
+ weight="bold"
50
+ color="secondary"
51
+ as="p"
52
+ style={{
53
+ textTransform: "uppercase",
54
+ letterSpacing: "0.04em",
55
+ margin: 0,
56
+ }}
57
+ >
52
58
  {children}
53
59
  </Text>
54
60
  </Box>
55
61
  );
56
62
  }
57
63
 
58
- const TAB_ORDER: readonly PixTab[] = ["copy", "qr"];
59
-
60
- /** The ARIA tabs pattern's keys: arrows move and select, Home and End jump. */
61
- function nextTab(key: string, tab: PixTab): PixTab | null {
62
- const at = TAB_ORDER.indexOf(tab);
63
- if (key === "ArrowRight") return TAB_ORDER[(at + 1) % TAB_ORDER.length] ?? null;
64
- if (key === "ArrowLeft") return TAB_ORDER[(at + TAB_ORDER.length - 1) % TAB_ORDER.length] ?? null;
65
- if (key === "Home") return TAB_ORDER[0] ?? null;
66
- if (key === "End") return TAB_ORDER[TAB_ORDER.length - 1] ?? null;
67
- return null;
68
- }
69
-
70
- /** DOM ids for the tabs and panels, unique per pane (two panes on one page must not collide). */
71
- function paneIds(base: string, id: PixTab): { tab: string; panel: string } {
72
- return { tab: `${base}-tab-${id}`, panel: `${base}-panel-${id}` };
73
- }
74
-
75
- function PixTabs({ tab, onTab, idBase }: { tab: PixTab; onTab: (tab: PixTab) => void; idBase: string }): JSX.Element {
76
- const copy = useCheckoutCopy().screens.pix;
77
- const tabs: { id: PixTab; label: string }[] = [
78
- { id: "copy", label: copy.copyPasteTab },
79
- { id: "qr", label: copy.qrTab },
80
- ];
81
- const onKeyDown = (event: KeyboardEvent<HTMLElement>): void => {
82
- const next = nextTab(event.key, tab);
83
- if (!next) return;
84
- event.preventDefault();
85
- onTab(next);
86
- document.getElementById(paneIds(idBase, next).tab)?.focus();
87
- };
88
- return (
89
- <Box
90
- role="tablist"
91
- aria-label={copy.tabsLabel}
92
- onKeyDown={onKeyDown}
93
- sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", borderBottom: "1px solid", borderColor: "divider", ...NARROW_ONLY }}
94
- >
95
- {tabs.map((item) => {
96
- const selected = item.id === tab;
97
- return (
98
- <Box
99
- key={item.id}
100
- component="button"
101
- type="button"
102
- role="tab"
103
- id={paneIds(idBase, item.id).tab}
104
- aria-selected={selected}
105
- aria-controls={paneIds(idBase, item.id).panel}
106
- tabIndex={selected ? 0 : -1}
107
- data-testid={`pix-tab-${item.id}`}
108
- onClick={() => onTab(item.id)}
109
- sx={{
110
- minHeight: 44,
111
- border: 0,
112
- borderBottom: "3px solid",
113
- borderColor: selected ? "primary.main" : "transparent",
114
- bgcolor: "transparent",
115
- font: "inherit",
116
- fontSize: "0.875rem",
117
- fontWeight: selected ? 700 : 600,
118
- color: selected ? "text.primary" : "text.secondary",
119
- cursor: "pointer",
120
- "&:focus-visible": { outline: "2px solid", outlineColor: "primary.main", outlineOffset: -2 },
121
- }}
122
- >
123
- {item.label}
124
- </Box>
125
- );
126
- })}
127
- </Box>
128
- );
129
- }
130
-
131
- function QrPanel({ pix, shown, idBase }: { pix: PixCharge; shown: boolean; idBase: string }): JSX.Element {
64
+ function QrPanel({ pix }: { pix: PixCharge }): JSX.Element {
132
65
  const { Text } = useCheckoutComponents();
133
66
  const copy = useCheckoutCopy().screens.pix;
134
67
  return (
135
68
  <Box
136
- id={paneIds(idBase, "qr").panel}
137
- role="tabpanel"
138
- aria-labelledby={paneIds(idBase, "qr").tab}
69
+ data-testid="pix-qr-panel"
139
70
  sx={{
140
- ...panelSx(shown),
71
+ display: "flex",
72
+ flexDirection: "column",
141
73
  alignItems: "center",
74
+ gap: 1.25,
75
+ minWidth: 0,
142
76
  textAlign: "center",
143
- [PIX_WIDE]: { display: "flex", p: 3, borderRadius: 1.75, border: "1px solid", borderColor: "divider", bgcolor: "action.hover" },
77
+ pt: 2,
78
+ borderTop: "1px solid",
79
+ borderColor: "divider",
80
+ [PIX_WIDE]: {
81
+ gap: 1.75,
82
+ p: 3,
83
+ borderTop: "1px solid",
84
+ borderRadius: 1.75,
85
+ border: "1px solid",
86
+ borderColor: "divider",
87
+ bgcolor: "action.hover",
88
+ },
144
89
  }}
145
90
  >
146
- <ColumnHeading center>{copy.qrHeading}</ColumnHeading>
147
- <Box sx={{ "& svg": { width: 200, height: 200 }, [PIX_WIDE]: { "& svg": { width: 232, height: 232 } } }}>
148
- <PixQr payload={pix.copyPaste} size={232} alt={copy.qrAlt} testId="pix-qr" />
91
+ <ColumnHeading center sx={NARROW_ONLY}>
92
+ {copy.otherDeviceHeading}
93
+ </ColumnHeading>
94
+ <ColumnHeading center sx={wideOnly("block")}>
95
+ {copy.qrHeading}
96
+ </ColumnHeading>
97
+ <Box
98
+ sx={{
99
+ "& svg": { width: 168, height: 168 },
100
+ [PIX_WIDE]: { "& svg": { width: 232, height: 232 } },
101
+ }}
102
+ >
103
+ <PixQr
104
+ payload={pix.copyPaste}
105
+ size={232}
106
+ alt={copy.qrAlt}
107
+ testId="pix-qr"
108
+ />
149
109
  </Box>
150
110
  <Text variant="body" size="sm" color="secondary" as="p" style={{ margin: 0, maxWidth: 280 }}>
151
111
  <Box component="span" sx={NARROW_ONLY}>{copy.qrTabCaption}</Box>
@@ -157,11 +117,62 @@ function QrPanel({ pix, shown, idBase }: { pix: PixCharge; shown: boolean; idBas
157
117
  );
158
118
  }
159
119
 
160
- function CopyPanel({ pix, shown, onCopy, idBase }: { pix: PixCharge; shown: boolean; onCopy: () => void; idBase: string }): JSX.Element {
161
- const { Button, Text } = useCheckoutComponents();
120
+ /**
121
+ * The one action on the pane: the prototype's 56px bar (52 wide), whatever
122
+ * the host's lg is — and on the phone's Pagamento step, the sticky pay bar's
123
+ * button (FUT-3525).
124
+ */
125
+ function CopyButton({ onCopy }: { onCopy: () => void }): JSX.Element {
126
+ const { Button } = useCheckoutComponents();
127
+ const copy = useCheckoutCopy().screens.pix;
128
+ return (
129
+ <StickyPayBar>
130
+ <Box
131
+ sx={{
132
+ "& > *": { minHeight: 56 },
133
+ "& > *, & > * *": { fontWeight: 700 },
134
+ [PIX_WIDE]: { "& > *": { minHeight: 52 } },
135
+ }}
136
+ >
137
+ <Button
138
+ variant="solid"
139
+ color="primary"
140
+ size="lg"
141
+ fullWidth
142
+ icon={<ContentCopyIcon fontSize="small" />}
143
+ onClick={onCopy}
144
+ dataTestId="pix-copy"
145
+ >
146
+ {copy.copyAction}
147
+ </Button>
148
+ </Box>
149
+ </StickyPayBar>
150
+ );
151
+ }
152
+
153
+ function CopyPanel({
154
+ pix,
155
+ onCopy,
156
+ inPanel,
157
+ }: {
158
+ pix: PixCharge;
159
+ onCopy: () => void;
160
+ /** False when the phone's pay bar carries the button, at the pane's foot. */
161
+ inPanel: boolean;
162
+ }): JSX.Element {
163
+ const { Text } = useCheckoutComponents();
162
164
  const copy = useCheckoutCopy().screens.pix;
163
165
  return (
164
- <Box id={paneIds(idBase, "copy").panel} role="tabpanel" aria-labelledby={paneIds(idBase, "copy").tab} sx={{ ...panelSx(shown), [PIX_WIDE]: { display: "flex", justifyContent: "center" } }}>
166
+ <Box
167
+ data-testid="pix-copy-panel"
168
+ sx={{
169
+ display: "flex",
170
+ flexDirection: "column",
171
+ gap: 1.75,
172
+ minWidth: 0,
173
+ [PIX_WIDE]: { justifyContent: "center" },
174
+ }}
175
+ >
165
176
  <ColumnHeading>{copy.copyPasteHeading}</ColumnHeading>
166
177
  {/*
167
178
  The box IS the code's frame, so whatever the host draws for inline code
@@ -183,14 +194,15 @@ function CopyPanel({ pix, shown, onCopy, idBase }: { pix: PixCharge; shown: bool
183
194
  {pix.copyPaste}
184
195
  </Text>
185
196
  </Box>
186
- {/* The one action on the pane: the prototype's 56px bar (52 wide), whatever the host's lg is. */}
187
- <Box sx={{ "& > *": { minHeight: 56 }, "& > *, & > * *": { fontWeight: 700 }, [PIX_WIDE]: { "& > *": { minHeight: 52 } } }}>
188
- <Button variant="solid" color="primary" size="lg" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopy} dataTestId="pix-copy">
189
- {copy.copyAction}
190
- </Button>
191
- </Box>
192
- <Text variant="body" size="sm" color="secondary" as="p" style={{ margin: 0 }}>
193
- <Box component="span" sx={{ display: "block", textAlign: "center", ...NARROW_ONLY }}>
197
+ {inPanel ? <CopyButton onCopy={onCopy} /> : null}
198
+ <Text
199
+ variant="body"
200
+ size="sm"
201
+ color="secondary"
202
+ as="p"
203
+ style={{ margin: 0 }}
204
+ >
205
+ <Box component="span" sx={{ display: "block", ...NARROW_ONLY }}>
194
206
  <Emphasized text={copy.copyPasteHint} />
195
207
  </Box>
196
208
  <Box component="span" sx={wideOnly("block")}>
@@ -201,38 +213,51 @@ function CopyPanel({ pix, shown, onCopy, idBase }: { pix: PixCharge; shown: bool
201
213
  );
202
214
  }
203
215
 
204
- /** The word between the wide layout's two columns, on a hairline. */
205
- function OrDivider(): JSX.Element {
206
- const { Text } = useCheckoutComponents();
207
- const copy = useCheckoutCopy().screens.pix;
208
- const line = { width: "1px", height: 56, bgcolor: "divider" };
209
- return (
210
- <Box aria-hidden sx={{ ...wideOnly("flex"), flexDirection: "column", alignItems: "center", alignSelf: "center", gap: 1 }}>
211
- <Box sx={line} />
212
- <Text variant="body" size="xs" weight="semibold" color="secondary" as="span">
213
- {copy.or}
214
- </Text>
215
- <Box sx={line} />
216
- </Box>
217
- );
218
- }
219
-
220
- export function PixPane({ pix, tab, onTab, onCopy }: { pix: PixCharge; tab: PixTab; onTab: (tab: PixTab) => void; onCopy: () => void }): JSX.Element {
221
- const idBase = useId();
216
+ /**
217
+ * The pane itself. Narrow, the code comes first — the buyer pays on the phone
218
+ * they are holding and cannot scan a QR drawn on that same screen — and the
219
+ * QR follows under a hairline, for paying from another device. Wide, the two
220
+ * stand side by side: the QR for the phone in the buyer's hand, the code for
221
+ * their internet banking.
222
+ */
223
+ export function PixPane({
224
+ pix,
225
+ onCopy,
226
+ }: {
227
+ pix: PixCharge;
228
+ onCopy: () => void;
229
+ }): JSX.Element {
230
+ // The phone's Pagamento step pins the button to the bottom of the screen
231
+ // (FUT-3525), so it goes LAST in the pane — under the QR — where a sticky
232
+ // bar rests when the page is scrolled to its end.
233
+ const layout = useCheckoutLayout();
234
+ const barAtFoot = layout !== null && !layout.wide;
222
235
  return (
223
- <>
224
- <PixTabs tab={tab} onTab={onTab} idBase={idBase} />
236
+ <Box
237
+ sx={{
238
+ display: "grid",
239
+ gap: 2.5,
240
+ [PIX_WIDE]: {
241
+ gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
242
+ gap: 4,
243
+ alignItems: "stretch",
244
+ },
245
+ }}
246
+ >
247
+ <Box sx={{ minWidth: 0, [PIX_WIDE]: { order: 2 } }}>
248
+ <CopyPanel pix={pix} onCopy={onCopy} inPanel={!barAtFoot} />
249
+ </Box>
225
250
  <Box
226
251
  sx={{
227
- display: "grid",
228
- gap: 2,
229
- [PIX_WIDE]: { gridTemplateColumns: "minmax(0, 1fr) auto minmax(0, 1fr)", gap: 4, alignItems: "stretch" },
252
+ minWidth: 0,
253
+ display: "flex",
254
+ flexDirection: "column",
255
+ [PIX_WIDE]: { order: 1 },
230
256
  }}
231
257
  >
232
- <QrPanel pix={pix} shown={tab === "qr"} idBase={idBase} />
233
- <OrDivider />
234
- <CopyPanel pix={pix} shown={tab === "copy"} onCopy={onCopy} idBase={idBase} />
258
+ <QrPanel pix={pix} />
235
259
  </Box>
236
- </>
260
+ {barAtFoot ? <CopyButton onCopy={onCopy} /> : null}
261
+ </Box>
237
262
  );
238
263
  }
@@ -9,11 +9,13 @@ import QRCode from "react-qr-code";
9
9
  */
10
10
 
11
11
  /**
12
- * The pane switches from tabs to two columns by its OWN width, never the
13
- * window's: the same checkout is mounted in a 720px column, a 1040px one and a
14
- * phone, and only the room it was given says whether two columns fit.
12
+ * The pane switches from one column to two by its OWN width, never the
13
+ * window's: the same checkout is mounted beside an order summary, alone in a
14
+ * column and on a phone, and only the room it was given says whether two
15
+ * columns fit. 680px is what the Pagamento step's payment column leaves it
16
+ * beside the summary card (FUT-3525).
15
17
  */
16
- export const PIX_WIDE = "@container pixpane (min-width: 760px)";
18
+ export const PIX_WIDE = "@container pixpane (min-width: 680px)";
17
19
 
18
20
  /** The container the {@link PIX_WIDE} query reads. */
19
21
  export const PIX_CONTAINER = { containerType: "inline-size", containerName: "pixpane" } as const;
@@ -1,9 +1,10 @@
1
1
  import { Box } from "@mui/material";
2
- import { useEffect, useMemo, useRef, useState, type JSX } from "react";
2
+ import { useEffect, useMemo, useRef, type JSX } from "react";
3
3
 
4
4
  import { useCheckoutCopy } from "./copy-context";
5
5
  import { PixAfterCopy } from "./pix-after-copy";
6
- import { PixPane, type PixTab } from "./pix-pane";
6
+ import { PixPane } from "./pix-pane";
7
+ import { useCheckoutLayout } from "./payment-step-layout";
7
8
  import { PIX_CONTAINER, PIX_WIDE, useClipboardCopy } from "./pix-parts";
8
9
  import { usePixStage } from "./pix-stage";
9
10
  import type { PixPaneCopy, SettlingCopy } from "./screens-copy";
@@ -202,11 +203,27 @@ function usePixPolling(order: CheckoutOrder, onResolved: (status: OrderStatus) =
202
203
  return polling;
203
204
  }
204
205
 
205
- function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; poll: ReturnType<typeof usePixPolling> }): JSX.Element {
206
+ /**
207
+ * The pane's title, at the prototype's scale: 20px, 26px wide — left out
208
+ * inside the Pagamento step, where the "Pix" tab names the pane (FUT-3525).
209
+ */
210
+ function PixTitle(): JSX.Element | null {
206
211
  const { Text } = useCheckoutComponents();
212
+ const copy = useCheckoutCopy().screens.pix;
213
+ const inStep = useCheckoutLayout() !== null;
214
+ if (inStep) return null;
215
+ return (
216
+ <Box sx={{ "&& > *": { fontSize: "1.25rem", lineHeight: 1.25 }, [PIX_WIDE]: { "&& > *": { fontSize: "1.625rem" } } }}>
217
+ <Text variant="heading" size="md" weight="bold" as="h2" style={{ margin: 0 }}>
218
+ {copy.heading}
219
+ </Text>
220
+ </Box>
221
+ );
222
+ }
223
+
224
+ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; poll: ReturnType<typeof usePixPolling> }): JSX.Element {
207
225
  const copy = useCheckoutCopy().screens.pix;
208
226
  const stage = usePixStage();
209
- const [tab, setTab] = useState<PixTab>("copy");
210
227
  const { copy: writeClipboard, justCopied } = useClipboardCopy(pix.copyPaste);
211
228
  // Leaving the payment step (a method switch, a terminal status) ends the
212
229
  // after-copy face with it: the next pane starts on the code.
@@ -240,10 +257,8 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
240
257
  deadline={time}
241
258
  justCopied={justCopied}
242
259
  onCopyAgain={() => void writeClipboard()}
243
- onShowQr={() => {
244
- setTab("qr");
245
- setAfterCopy(false);
246
- }}
260
+ // Back to the pane, where the QR is always on screen now.
261
+ onShowQr={() => setAfterCopy(false)}
247
262
  onPreferCard={stage.preferCard}
248
263
  footer={footerLine(copy.verifying)}
249
264
  />
@@ -251,16 +266,9 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
251
266
  }
252
267
  return (
253
268
  <>
254
- {/* The pane's title, at the prototype's scale: 20px, 26px wide. */}
255
- <Box sx={{ "&& > *": { fontSize: "1.25rem", lineHeight: 1.25 }, [PIX_WIDE]: { "&& > *": { fontSize: "1.625rem" } } }}>
256
- <Text variant="heading" size="md" weight="bold" as="h2" style={{ margin: 0 }}>
257
- {copy.heading}
258
- </Text>
259
- </Box>
269
+ <PixTitle />
260
270
  <PixPane
261
271
  pix={pix}
262
- tab={tab}
263
- onTab={setTab}
264
272
  onCopy={() => {
265
273
  // Only a copy the clipboard TOOK moves on: the next face opens on
266
274
  // "Código Pix copiado", and a failed write leaves the code on screen
@@ -287,6 +295,9 @@ export function PixView({
287
295
  const { Text } = useCheckoutComponents();
288
296
  const copy = useCheckoutCopy().screens.pix;
289
297
  const poll = usePixPolling(order, onResolved, pollIntervalMs);
298
+ // The Pagamento step's panel is the card there (FUT-3525); drawing another
299
+ // inside it would be a box in a box.
300
+ const inStep = useCheckoutLayout() !== null;
290
301
  const pix = order.pix;
291
302
  if (!pix) {
292
303
  return (
@@ -307,7 +318,16 @@ export function PixView({
307
318
  display: "flex",
308
319
  flexDirection: "column",
309
320
  gap: 2,
310
- [PIX_WIDE]: { gap: 2.5, p: 3.5, bgcolor: "background.paper", border: "1px solid", borderColor: "divider", borderRadius: 2 },
321
+ [PIX_WIDE]: inStep
322
+ ? { gap: 2.5 }
323
+ : {
324
+ gap: 2.5,
325
+ p: 3.5,
326
+ bgcolor: "background.paper",
327
+ border: "1px solid",
328
+ borderColor: "divider",
329
+ borderRadius: 2,
330
+ },
311
331
  }}
312
332
  >
313
333
  <PixBody order={order} pix={pix} poll={poll} />
@@ -133,17 +133,22 @@ export interface PixPaneCopy {
133
133
  preferCardAction: string;
134
134
  /** The after-copy state's poll line. */
135
135
  verifying: string;
136
- /** The narrow layout's tabs: their group name and the two labels. */
136
+ /** @deprecated Unread since FUT-3525, which took the narrow layout's Copia/QR tabs out. */
137
137
  tabsLabel: string;
138
+ /** @deprecated Unread since FUT-3525 — see {@link PixPaneCopy.tabsLabel}. */
138
139
  copyPasteTab: string;
140
+ /** @deprecated Unread since FUT-3525 — see {@link PixPaneCopy.tabsLabel}. */
139
141
  qrTab: string;
140
- /** The wide layout's two column headings and the word between them. */
142
+ /** The wide layout's two column headings. */
141
143
  qrHeading: string;
144
+ /** The phone's heading over the QR, under the code (FUT-3525). */
145
+ otherDeviceHeading: string;
142
146
  copyPasteHeading: string;
147
+ /** @deprecated Unread since FUT-3525, which took the "ou" between the wide columns out. */
143
148
  or: string;
144
149
  /** Under the QR in the wide layout; `**…**` marks the bold span. */
145
150
  qrInstructions: string;
146
- /** Under the QR in the narrow layout's QR tab. */
151
+ /** Under the QR in the narrow layout (a tab until FUT-3525, hence the name). */
147
152
  qrTabCaption: string;
148
153
  /** Under the copy button: narrow and wide. `**…**` marks the bold span. */
149
154
  copyPasteHint: string;
@@ -266,6 +271,21 @@ export interface HostedHandoverCopy {
266
271
  preparing: string;
267
272
  }
268
273
 
274
+ /**
275
+ * The cart changed after this payment was prepared (FUT-1139): another tab, a
276
+ * price the store moved, a line it stopped selling. The charge prepared for the
277
+ * OLD basket must not be the one paid, so the step says so and offers the one
278
+ * control that brings the payment up to date.
279
+ */
280
+ export interface CartChangedCopy {
281
+ heading: string;
282
+ support: string;
283
+ /** Re-prepares the payment for the cart as it stands now. */
284
+ refreshAction: string;
285
+ /** The heading over a refresh that failed; the host's own sentence is the body. */
286
+ refreshFailed: string;
287
+ }
288
+
269
289
  /** What the browser says when the wire itself failed. */
270
290
  export interface CheckoutTransportCopy {
271
291
  /** A refusal whose envelope carried no sentence of its own. */
@@ -286,6 +306,29 @@ export interface CheckoutValidationCopy {
286
306
  required: string;
287
307
  }
288
308
 
309
+ /**
310
+ * The Pagamento step's own frame (FUT-3525): the order summary beside the
311
+ * payment on a computer, and the phone's header, total and pay bar.
312
+ */
313
+ export interface PaymentLayoutCopy {
314
+ /** Over the method tabs on a computer. */
315
+ payHeading: string;
316
+ /** The summary card's title. */
317
+ summaryTitle: string;
318
+ /** The summary's and the pay bar's label over the amount. */
319
+ total: string;
320
+ /** The phone's caption over the big amount — "Total a pagar · 2 itens". */
321
+ totalDueCaption(items: number): string;
322
+ /** One cart line in the summary — "2× X-Burger". */
323
+ lineLabel(quantity: number, name: string): string;
324
+ /** The phone's payer chip, for a CPF the store already holds — "CPF ✓". A typed one is shown masked. */
325
+ taxIdOnFileShort: string;
326
+ /** The address control's accessible name; its visible word is {@link PayerSummaryCopy.changeAction}. */
327
+ changeAddress: string;
328
+ /** The phone's step line — "Etapa 2 de 3 · Pagamento". */
329
+ stepProgress(step: number, steps: number, label: string): string;
330
+ }
331
+
289
332
  /** Everything above, in the shape the checkout context carries. */
290
333
  export interface CheckoutScreensCopy {
291
334
  method: MethodPickerCopy;
@@ -293,11 +336,13 @@ export interface CheckoutScreensCopy {
293
336
  pix: PixPaneCopy;
294
337
  card: CardPaneCopy;
295
338
  payer: PayerSummaryCopy;
339
+ layout: PaymentLayoutCopy;
296
340
  error: PaymentErrorCopy;
297
341
  wallet: WalletCopy;
298
342
  hosted: HostedHandoverCopy;
299
343
  transport: CheckoutTransportCopy;
300
344
  validation: CheckoutValidationCopy;
345
+ cartChanged: CartChangedCopy;
301
346
  /** The step wrapper's own wait, while the charge is being raised. */
302
347
  generatingPayment: string;
303
348
  /**
@@ -54,6 +54,7 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
54
54
  copyPasteTab: 'Copy and paste',
55
55
  qrTab: 'QR code',
56
56
  qrHeading: 'On your phone',
57
+ otherDeviceHeading: 'Or pay from another device',
57
58
  copyPasteHeading: 'Pix copy and paste',
58
59
  or: 'or',
59
60
  qrInstructions: 'Open your banking app on your phone, choose **Pix › Scan QR code** and point it at the screen.',
@@ -99,6 +100,16 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
99
100
  payingWithSavedDetails: 'Paying with your saved details',
100
101
  changeAction: 'Change',
101
102
  },
103
+ layout: {
104
+ payHeading: 'How would you like to pay?',
105
+ summaryTitle: 'Order summary',
106
+ total: 'Total',
107
+ totalDueCaption: (items) => `Total due · ${items} ${items === 1 ? 'item' : 'items'}`,
108
+ lineLabel: (quantity, name) => `${quantity}× ${name}`,
109
+ taxIdOnFileShort: 'CPF ✓',
110
+ changeAddress: 'Change address',
111
+ stepProgress: (step, steps, label) => `Step ${step} of ${steps} · ${label}`,
112
+ },
102
113
  error: {
103
114
  confirming: 'We are confirming your payment',
104
115
  cannotContinue: 'Could not continue',
@@ -143,6 +154,12 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
143
154
  invalidResponse: 'Invalid response from the server.',
144
155
  offline: 'Could not connect. Check your connection and try again.',
145
156
  },
157
+ cartChanged: {
158
+ heading: 'Your cart changed',
159
+ support: 'This payment was prepared for your previous cart. Update it to pay the right amount.',
160
+ refreshAction: 'Update payment',
161
+ refreshFailed: 'Could not update the payment',
162
+ },
146
163
  validation: {
147
164
  taxIdInvalid: 'That CPF is not valid.',
148
165
  nameRequired: 'Enter your name.',