@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 +1 -1
- package/src/card/fields.tsx +33 -14
- package/src/components/checkout/card-view.tsx +19 -6
- package/src/components/checkout/cart-changed.tsx +24 -3
- package/src/components/checkout/checkout-flow.tsx +63 -79
- package/src/components/checkout/checkout-frame.tsx +175 -0
- package/src/components/checkout/checkout-steps.tsx +34 -66
- 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 +11 -4
- package/src/components/checkout/pix-pane.tsx +197 -161
- package/src/components/checkout/pix-parts.tsx +53 -5
- package/src/components/checkout/pix-view.tsx +37 -17
- package/src/components/checkout/screens-copy.ts +38 -3
- package/src/components/checkout/screens-en-US.ts +12 -0
- package/src/components/checkout/screens-pt-BR.ts +12 -0
- package/src/index.ts +2 -0
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
|
-
import {
|
|
2
|
+
import type { JSX, ReactNode } from "react";
|
|
3
3
|
|
|
4
|
+
import { usePaymentStale } from "./cart-changed";
|
|
4
5
|
import { useCheckoutCopy } from "./copy-context";
|
|
5
6
|
import { ContentCopyIcon } from "./icons";
|
|
6
|
-
import {
|
|
7
|
+
import { StickyPayBar, useCheckoutLayout } from "./payment-step-layout";
|
|
8
|
+
import { Emphasized, PIX_WIDE, PixQr, StaleVeil } from "./pix-parts";
|
|
7
9
|
import type { PixCharge } from "./types";
|
|
8
10
|
import { useCheckoutComponents } from "./ui";
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* The PIX pane before a copy (the 2026-10-06 Pix redesign
|
|
13
|
+
* The PIX pane before a copy (the 2026-10-06 Pix redesign, laid out again by
|
|
14
|
+
* the 2026-10-11 Pagamento redesign, FUT-3525).
|
|
12
15
|
*
|
|
13
|
-
* Narrow — a phone —
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* the QR
|
|
18
|
-
* banking.
|
|
16
|
+
* Narrow — a phone — the code and its button come first and the QR follows,
|
|
17
|
+
* "Ou pague de outro aparelho": the buyer pays on the phone they are holding
|
|
18
|
+
* and cannot scan a QR drawn on that same screen. Wide — a computer — the two
|
|
19
|
+
* stand side by side, the QR on the left. The Copia/QR tabs this replaced hid
|
|
20
|
+
* the QR behind a tap the phone's own buyer never needed.
|
|
19
21
|
*
|
|
20
22
|
* 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.
|
|
23
|
+
* layout needs no measurement: the first paint is already right.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
|
-
export type PixTab = "copy" | "qr";
|
|
26
|
-
|
|
27
26
|
/** A narrow-only element: shown under the container query's threshold. */
|
|
28
27
|
const NARROW_ONLY = { [PIX_WIDE]: { display: "none" } } as const;
|
|
29
28
|
|
|
@@ -32,120 +31,85 @@ function wideOnly(display: string) {
|
|
|
32
31
|
return { display: "none", [PIX_WIDE]: { display } } as const;
|
|
33
32
|
}
|
|
34
33
|
|
|
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 {
|
|
34
|
+
/** The small capitals over each column. Uppercase by CSS only. */
|
|
35
|
+
function ColumnHeading({
|
|
36
|
+
children,
|
|
37
|
+
center,
|
|
38
|
+
sx,
|
|
39
|
+
}: {
|
|
40
|
+
children: ReactNode;
|
|
41
|
+
center?: boolean;
|
|
42
|
+
sx?: object;
|
|
43
|
+
}): JSX.Element {
|
|
48
44
|
const { Text } = useCheckoutComponents();
|
|
49
45
|
return (
|
|
50
|
-
<Box sx={{
|
|
51
|
-
<Text
|
|
46
|
+
<Box sx={{ textAlign: center ? "center" : "left", ...sx }}>
|
|
47
|
+
<Text
|
|
48
|
+
variant="body"
|
|
49
|
+
size="xs"
|
|
50
|
+
weight="bold"
|
|
51
|
+
color="secondary"
|
|
52
|
+
as="p"
|
|
53
|
+
style={{
|
|
54
|
+
textTransform: "uppercase",
|
|
55
|
+
letterSpacing: "0.04em",
|
|
56
|
+
margin: 0,
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
52
59
|
{children}
|
|
53
60
|
</Text>
|
|
54
61
|
</Box>
|
|
55
62
|
);
|
|
56
63
|
}
|
|
57
64
|
|
|
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 {
|
|
65
|
+
function QrPanel({ pix }: { pix: PixCharge }): JSX.Element {
|
|
132
66
|
const { Text } = useCheckoutComponents();
|
|
133
67
|
const copy = useCheckoutCopy().screens.pix;
|
|
68
|
+
const stale = usePaymentStale();
|
|
134
69
|
return (
|
|
135
70
|
<Box
|
|
136
|
-
|
|
137
|
-
role="tabpanel"
|
|
138
|
-
aria-labelledby={paneIds(idBase, "qr").tab}
|
|
71
|
+
data-testid="pix-qr-panel"
|
|
139
72
|
sx={{
|
|
140
|
-
|
|
73
|
+
display: "flex",
|
|
74
|
+
flexDirection: "column",
|
|
141
75
|
alignItems: "center",
|
|
76
|
+
gap: 1.25,
|
|
77
|
+
minWidth: 0,
|
|
142
78
|
textAlign: "center",
|
|
143
|
-
|
|
79
|
+
pt: 2,
|
|
80
|
+
borderTop: "1px solid",
|
|
81
|
+
borderColor: "divider",
|
|
82
|
+
[PIX_WIDE]: {
|
|
83
|
+
gap: 1.75,
|
|
84
|
+
p: 3,
|
|
85
|
+
borderTop: "1px solid",
|
|
86
|
+
borderRadius: 1.75,
|
|
87
|
+
border: "1px solid",
|
|
88
|
+
borderColor: "divider",
|
|
89
|
+
bgcolor: "action.hover",
|
|
90
|
+
},
|
|
144
91
|
}}
|
|
145
92
|
>
|
|
146
|
-
<ColumnHeading center
|
|
147
|
-
|
|
148
|
-
|
|
93
|
+
<ColumnHeading center sx={NARROW_ONLY}>
|
|
94
|
+
{copy.otherDeviceHeading}
|
|
95
|
+
</ColumnHeading>
|
|
96
|
+
<ColumnHeading center sx={wideOnly("block")}>
|
|
97
|
+
{copy.qrHeading}
|
|
98
|
+
</ColumnHeading>
|
|
99
|
+
<Box
|
|
100
|
+
sx={{
|
|
101
|
+
"& svg": { width: 168, height: 168 },
|
|
102
|
+
[PIX_WIDE]: { "& svg": { width: 232, height: 232 } },
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
<StaleVeil stale={stale}>
|
|
106
|
+
<PixQr
|
|
107
|
+
payload={pix.copyPaste}
|
|
108
|
+
size={232}
|
|
109
|
+
alt={copy.qrAlt}
|
|
110
|
+
testId="pix-qr"
|
|
111
|
+
/>
|
|
112
|
+
</StaleVeil>
|
|
149
113
|
</Box>
|
|
150
114
|
<Text variant="body" size="sm" color="secondary" as="p" style={{ margin: 0, maxWidth: 280 }}>
|
|
151
115
|
<Box component="span" sx={NARROW_ONLY}>{copy.qrTabCaption}</Box>
|
|
@@ -157,82 +121,154 @@ function QrPanel({ pix, shown, idBase }: { pix: PixCharge; shown: boolean; idBas
|
|
|
157
121
|
);
|
|
158
122
|
}
|
|
159
123
|
|
|
160
|
-
|
|
161
|
-
|
|
124
|
+
/**
|
|
125
|
+
* The one action on the pane: the prototype's 56px bar (52 wide), whatever
|
|
126
|
+
* the host's lg is — and on the phone's Pagamento step, the sticky pay bar's
|
|
127
|
+
* button (FUT-3525).
|
|
128
|
+
*/
|
|
129
|
+
function CopyButton({ onCopy }: { onCopy: () => void }): JSX.Element {
|
|
130
|
+
const { Button } = useCheckoutComponents();
|
|
162
131
|
const copy = useCheckoutCopy().screens.pix;
|
|
132
|
+
// A changed cart makes this code the OLD order's (FUT-3535): nothing to copy
|
|
133
|
+
// until the step's "Atualizar pagamento" brings a new one.
|
|
134
|
+
const stale = usePaymentStale();
|
|
163
135
|
return (
|
|
164
|
-
<
|
|
165
|
-
<ColumnHeading>{copy.copyPasteHeading}</ColumnHeading>
|
|
166
|
-
{/*
|
|
167
|
-
The box IS the code's frame, so whatever the host draws for inline code
|
|
168
|
-
(a tinted chip, its own border and padding) is undone inside it:
|
|
169
|
-
otherwise a wrapped code reads as a box inside a box.
|
|
170
|
-
*/}
|
|
136
|
+
<StickyPayBar>
|
|
171
137
|
<Box
|
|
172
138
|
sx={{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
borderColor: "divider",
|
|
177
|
-
borderRadius: 1.5,
|
|
178
|
-
bgcolor: "action.hover",
|
|
179
|
-
"&& > *": { bgcolor: "transparent", border: 0, borderRadius: 0, p: 0 },
|
|
139
|
+
"& > *": { minHeight: 56 },
|
|
140
|
+
"& > *, & > * *": { fontWeight: 700 },
|
|
141
|
+
[PIX_WIDE]: { "& > *": { minHeight: 52 } },
|
|
180
142
|
}}
|
|
181
143
|
>
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
144
|
+
<Button
|
|
145
|
+
variant="solid"
|
|
146
|
+
color="primary"
|
|
147
|
+
size="lg"
|
|
148
|
+
fullWidth
|
|
149
|
+
icon={<ContentCopyIcon fontSize="small" />}
|
|
150
|
+
onClick={onCopy}
|
|
151
|
+
disabled={stale}
|
|
152
|
+
dataTestId="pix-copy"
|
|
153
|
+
>
|
|
189
154
|
{copy.copyAction}
|
|
190
155
|
</Button>
|
|
191
156
|
</Box>
|
|
192
|
-
|
|
193
|
-
<Box component="span" sx={{ display: "block", textAlign: "center", ...NARROW_ONLY }}>
|
|
194
|
-
<Emphasized text={copy.copyPasteHint} />
|
|
195
|
-
</Box>
|
|
196
|
-
<Box component="span" sx={wideOnly("block")}>
|
|
197
|
-
<Emphasized text={copy.internetBankingHint} />
|
|
198
|
-
</Box>
|
|
199
|
-
</Text>
|
|
200
|
-
</Box>
|
|
157
|
+
</StickyPayBar>
|
|
201
158
|
);
|
|
202
159
|
}
|
|
203
160
|
|
|
204
|
-
|
|
205
|
-
|
|
161
|
+
function CopyPanel({
|
|
162
|
+
pix,
|
|
163
|
+
onCopy,
|
|
164
|
+
inPanel,
|
|
165
|
+
}: {
|
|
166
|
+
pix: PixCharge;
|
|
167
|
+
onCopy: () => void;
|
|
168
|
+
/** False when the phone's pay bar carries the button, at the pane's foot. */
|
|
169
|
+
inPanel: boolean;
|
|
170
|
+
}): JSX.Element {
|
|
206
171
|
const { Text } = useCheckoutComponents();
|
|
207
172
|
const copy = useCheckoutCopy().screens.pix;
|
|
208
|
-
const
|
|
173
|
+
const stale = usePaymentStale();
|
|
209
174
|
return (
|
|
210
|
-
<Box
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
175
|
+
<Box
|
|
176
|
+
data-testid="pix-copy-panel"
|
|
177
|
+
sx={{
|
|
178
|
+
display: "flex",
|
|
179
|
+
flexDirection: "column",
|
|
180
|
+
gap: 1.75,
|
|
181
|
+
minWidth: 0,
|
|
182
|
+
[PIX_WIDE]: { justifyContent: "center" },
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
<ColumnHeading>{copy.copyPasteHeading}</ColumnHeading>
|
|
186
|
+
{/*
|
|
187
|
+
The box IS the code's frame, so whatever the host draws for inline code
|
|
188
|
+
(a tinted chip, its own border and padding) is undone inside it:
|
|
189
|
+
otherwise a wrapped code reads as a box inside a box.
|
|
190
|
+
*/}
|
|
191
|
+
<StaleVeil stale={stale}>
|
|
192
|
+
<Box
|
|
193
|
+
sx={{
|
|
194
|
+
px: 2,
|
|
195
|
+
py: 1.5,
|
|
196
|
+
border: "1px solid",
|
|
197
|
+
borderColor: "divider",
|
|
198
|
+
borderRadius: 1.5,
|
|
199
|
+
bgcolor: "action.hover",
|
|
200
|
+
"&& > *": { bgcolor: "transparent", border: 0, borderRadius: 0, p: 0 },
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<Text variant="code" size="xs" as="p" data-testid="pix-code" style={{ margin: 0, wordBreak: "break-all", lineHeight: 1.55 }}>
|
|
204
|
+
{pix.copyPaste}
|
|
205
|
+
</Text>
|
|
206
|
+
</Box>
|
|
207
|
+
</StaleVeil>
|
|
208
|
+
{inPanel ? <CopyButton onCopy={onCopy} /> : null}
|
|
209
|
+
<Text
|
|
210
|
+
variant="body"
|
|
211
|
+
size="sm"
|
|
212
|
+
color="secondary"
|
|
213
|
+
as="p"
|
|
214
|
+
style={{ margin: 0 }}
|
|
215
|
+
>
|
|
216
|
+
<Box component="span" sx={{ display: "block", ...NARROW_ONLY }}>
|
|
217
|
+
<Emphasized text={copy.copyPasteHint} />
|
|
218
|
+
</Box>
|
|
219
|
+
<Box component="span" sx={wideOnly("block")}>
|
|
220
|
+
<Emphasized text={copy.internetBankingHint} />
|
|
221
|
+
</Box>
|
|
214
222
|
</Text>
|
|
215
|
-
<Box sx={line} />
|
|
216
223
|
</Box>
|
|
217
224
|
);
|
|
218
225
|
}
|
|
219
226
|
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
/**
|
|
228
|
+
* The pane itself. Narrow, the code comes first — the buyer pays on the phone
|
|
229
|
+
* they are holding and cannot scan a QR drawn on that same screen — and the
|
|
230
|
+
* QR follows under a hairline, for paying from another device. Wide, the two
|
|
231
|
+
* stand side by side: the QR for the phone in the buyer's hand, the code for
|
|
232
|
+
* their internet banking.
|
|
233
|
+
*/
|
|
234
|
+
export function PixPane({
|
|
235
|
+
pix,
|
|
236
|
+
onCopy,
|
|
237
|
+
}: {
|
|
238
|
+
pix: PixCharge;
|
|
239
|
+
onCopy: () => void;
|
|
240
|
+
}): JSX.Element {
|
|
241
|
+
// The phone's Pagamento step pins the button to the bottom of the screen
|
|
242
|
+
// (FUT-3525), so it goes LAST in the pane — under the QR — where a sticky
|
|
243
|
+
// bar rests when the page is scrolled to its end.
|
|
244
|
+
const layout = useCheckoutLayout();
|
|
245
|
+
const barAtFoot = layout !== null && !layout.wide;
|
|
222
246
|
return (
|
|
223
|
-
|
|
224
|
-
|
|
247
|
+
<Box
|
|
248
|
+
sx={{
|
|
249
|
+
display: "grid",
|
|
250
|
+
gap: 2.5,
|
|
251
|
+
[PIX_WIDE]: {
|
|
252
|
+
gridTemplateColumns: "repeat(2, minmax(0, 1fr))",
|
|
253
|
+
gap: 4,
|
|
254
|
+
alignItems: "stretch",
|
|
255
|
+
},
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<Box sx={{ minWidth: 0, [PIX_WIDE]: { order: 2 } }}>
|
|
259
|
+
<CopyPanel pix={pix} onCopy={onCopy} inPanel={!barAtFoot} />
|
|
260
|
+
</Box>
|
|
225
261
|
<Box
|
|
226
262
|
sx={{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
263
|
+
minWidth: 0,
|
|
264
|
+
display: "flex",
|
|
265
|
+
flexDirection: "column",
|
|
266
|
+
[PIX_WIDE]: { order: 1 },
|
|
230
267
|
}}
|
|
231
268
|
>
|
|
232
|
-
<QrPanel pix={pix}
|
|
233
|
-
<OrDivider />
|
|
234
|
-
<CopyPanel pix={pix} shown={tab === "copy"} onCopy={onCopy} idBase={idBase} />
|
|
269
|
+
<QrPanel pix={pix} />
|
|
235
270
|
</Box>
|
|
236
|
-
|
|
271
|
+
{barAtFoot ? <CopyButton onCopy={onCopy} /> : null}
|
|
272
|
+
</Box>
|
|
237
273
|
);
|
|
238
274
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
|
-
import { Fragment, useCallback, useEffect, useRef, useState, type JSX } from "react";
|
|
2
|
+
import { Fragment, useCallback, useEffect, useRef, useState, type JSX, type ReactNode } from "react";
|
|
3
3
|
import QRCode from "react-qr-code";
|
|
4
4
|
|
|
5
|
+
import { useCheckoutCopy } from "./copy-context";
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* The pieces the PIX pane and its after-copy face share (the 2026-10-06 Pix
|
|
7
9
|
* redesign): the container-query switch, the QR, bold spans in a sentence, and
|
|
@@ -9,11 +11,13 @@ import QRCode from "react-qr-code";
|
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
/**
|
|
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
|
|
14
|
+
* The pane switches from one column to two by its OWN width, never the
|
|
15
|
+
* window's: the same checkout is mounted beside an order summary, alone in a
|
|
16
|
+
* column and on a phone, and only the room it was given says whether two
|
|
17
|
+
* columns fit. 680px is what the Pagamento step's payment column leaves it
|
|
18
|
+
* beside the summary card (FUT-3525).
|
|
15
19
|
*/
|
|
16
|
-
export const PIX_WIDE = "@container pixpane (min-width:
|
|
20
|
+
export const PIX_WIDE = "@container pixpane (min-width: 680px)";
|
|
17
21
|
|
|
18
22
|
/** The container the {@link PIX_WIDE} query reads. */
|
|
19
23
|
export const PIX_CONTAINER = { containerType: "inline-size", containerName: "pixpane" } as const;
|
|
@@ -77,3 +81,47 @@ export function useClipboardCopy(text: string): { copy: () => Promise<boolean>;
|
|
|
77
81
|
}, [text]);
|
|
78
82
|
return { copy, justCopied };
|
|
79
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A PIX's QR or code, veiled while the cart has changed under it (FUT-3535):
|
|
87
|
+
* blurred past reading, out of reach of a pointer, a selection and a screen
|
|
88
|
+
* reader, with "Atualize para gerar um novo Pix" over it. The step's warning
|
|
89
|
+
* above says why and carries the control that brings a new code. One tree
|
|
90
|
+
* shape either way, so lifting the veil redraws nothing.
|
|
91
|
+
*/
|
|
92
|
+
export function StaleVeil({ stale, children }: { stale: boolean; children: ReactNode }): JSX.Element {
|
|
93
|
+
const words = useCheckoutCopy().screens.cartChanged.pixVeil;
|
|
94
|
+
return (
|
|
95
|
+
<Box sx={{ position: "relative", minWidth: 0 }} data-testid={stale ? "pix-stale-veil" : undefined}>
|
|
96
|
+
<Box
|
|
97
|
+
aria-hidden={stale || undefined}
|
|
98
|
+
sx={stale ? { filter: "blur(6px)", opacity: 0.5, pointerEvents: "none", userSelect: "none" } : undefined}
|
|
99
|
+
>
|
|
100
|
+
{children}
|
|
101
|
+
</Box>
|
|
102
|
+
{stale ? (
|
|
103
|
+
<Box
|
|
104
|
+
sx={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", p: 1.5 }}
|
|
105
|
+
>
|
|
106
|
+
<Box
|
|
107
|
+
component="p"
|
|
108
|
+
sx={{
|
|
109
|
+
m: 0,
|
|
110
|
+
px: 1.5,
|
|
111
|
+
py: 0.75,
|
|
112
|
+
borderRadius: 999,
|
|
113
|
+
bgcolor: "background.paper",
|
|
114
|
+
boxShadow: 1,
|
|
115
|
+
color: "text.primary",
|
|
116
|
+
fontSize: "0.8125rem",
|
|
117
|
+
fontWeight: 600,
|
|
118
|
+
textAlign: "center",
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{words}
|
|
122
|
+
</Box>
|
|
123
|
+
</Box>
|
|
124
|
+
) : null}
|
|
125
|
+
</Box>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -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} />
|