@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 +1 -1
- package/src/card/fields.tsx +33 -14
- package/src/components/checkout/card-view.tsx +24 -10
- package/src/components/checkout/cart-changed.tsx +273 -0
- package/src/components/checkout/checkout-flow.tsx +65 -81
- package/src/components/checkout/checkout-frame.tsx +175 -0
- package/src/components/checkout/checkout-steps.tsx +57 -83
- package/src/components/checkout/icons.tsx +5 -0
- package/src/components/checkout/method-tabs.tsx +249 -0
- package/src/components/checkout/payment-status-parts.tsx +13 -3
- package/src/components/checkout/payment-step-frame.tsx +206 -0
- package/src/components/checkout/payment-step-layout.tsx +169 -14
- package/src/components/checkout/payment-summary-phone.tsx +158 -0
- package/src/components/checkout/payment-summary.tsx +290 -0
- package/src/components/checkout/pix-after-copy.tsx +1 -1
- package/src/components/checkout/pix-pane.tsx +172 -147
- package/src/components/checkout/pix-parts.tsx +6 -4
- package/src/components/checkout/pix-view.tsx +37 -17
- package/src/components/checkout/screens-copy.ts +48 -3
- package/src/components/checkout/screens-en-US.ts +17 -0
- package/src/components/checkout/screens-pt-BR.ts +17 -0
- package/src/components/checkout/start-payment.ts +48 -19
- package/src/components/checkout/use-checkout-controller.ts +6 -2
- package/src/index.ts +3 -0
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
|
-
import {
|
|
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 —
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the QR
|
|
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
|
|
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
|
-
/**
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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={{
|
|
51
|
-
<Text
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
role="tabpanel"
|
|
138
|
-
aria-labelledby={paneIds(idBase, "qr").tab}
|
|
69
|
+
data-testid="pix-qr-panel"
|
|
139
70
|
sx={{
|
|
140
|
-
|
|
71
|
+
display: "flex",
|
|
72
|
+
flexDirection: "column",
|
|
141
73
|
alignItems: "center",
|
|
74
|
+
gap: 1.25,
|
|
75
|
+
minWidth: 0,
|
|
142
76
|
textAlign: "center",
|
|
143
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
|
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
|
-
{
|
|
187
|
-
<
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
minWidth: 0,
|
|
253
|
+
display: "flex",
|
|
254
|
+
flexDirection: "column",
|
|
255
|
+
[PIX_WIDE]: { order: 1 },
|
|
230
256
|
}}
|
|
231
257
|
>
|
|
232
|
-
<QrPanel pix={pix}
|
|
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
|
|
13
|
-
* window's: the same checkout is mounted
|
|
14
|
-
* phone, and only the room it was given says whether two
|
|
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:
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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]:
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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.',
|