@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.
@@ -0,0 +1,290 @@
1
+ import { Box } from "@mui/material";
2
+ import type { JSX, ReactNode } from "react";
3
+
4
+ import { formatCpf } from "../../card";
5
+
6
+ import { useCheckoutCopy } from "./copy-context";
7
+ import { PersonOutlineIcon, PlaceOutlinedIcon } from "./icons";
8
+ import { useCheckoutComponents } from "./ui";
9
+
10
+ /**
11
+ * What the Pagamento step says about the ORDER around the payment (FUT-3525):
12
+ * its lines and total, who pays, where it goes and the note. On a computer
13
+ * it is a card beside the payment column ({@link OrderSummaryCard}); on a
14
+ * phone the total heads the page and the rest are chips
15
+ * ({@link PhoneOrderSummary}).
16
+ */
17
+
18
+ /** One cart line, as the host words it — the package does no money math. */
19
+ export interface CheckoutSummaryLine {
20
+ key: string;
21
+ quantity: number;
22
+ name: string;
23
+ /** The line's total, formatted by the host. */
24
+ totalLabel: string;
25
+ }
26
+
27
+ /** Where a delivery goes, and the host's way back to change it. */
28
+ export interface CheckoutDeliverySummary {
29
+ /** "Rua Augusta, 1500 · ap 42" — street, number and complement. */
30
+ line1: string;
31
+ /** "Consolação, São Paulo" — district and city. Optional. */
32
+ line2?: string;
33
+ /** Back to the host's address step. */
34
+ onEdit: () => void;
35
+ }
36
+
37
+ /** Everything the summary shows. Each part is optional and hides itself when absent. */
38
+ export interface OrderSummaryParts {
39
+ totalLabel: string;
40
+ totalItems: number;
41
+ /** The host's discount, fee and delivery lines under the total (FUT-246). */
42
+ discountLines?: ReactNode;
43
+ lines?: CheckoutSummaryLine[];
44
+ /** Who is paying — only for a flow whose Dados step was skipped (FUT-465). */
45
+ payer?: { name?: string; taxId?: string; onEdit: () => void };
46
+ delivery?: CheckoutDeliverySummary;
47
+ /** The host's note control, mounted as it is. */
48
+ orderNote?: ReactNode;
49
+ }
50
+
51
+ /** Read by a screen reader, not drawn. */
52
+ const VISUALLY_HIDDEN = {
53
+ position: "absolute",
54
+ width: "1px",
55
+ height: "1px",
56
+ overflow: "hidden",
57
+ clip: "rect(0 0 0 0)",
58
+ whiteSpace: "nowrap",
59
+ } as const;
60
+
61
+ /** "Alterar" as the prototype's top-aligned link on the card's edge, not a padded button. */
62
+ const EDIT_SX = { flex: "none", alignSelf: "flex-start", mt: -0.75, mr: -1 } as const;
63
+
64
+ const ROW_SX = { display: "flex", gap: 1.5, alignItems: "flex-start" } as const;
65
+ const ICON_SX = {
66
+ fontSize: 20,
67
+ color: "text.secondary",
68
+ flex: "0 0 auto",
69
+ mt: 0.25,
70
+ } as const;
71
+
72
+ export function NoteIcon({ size = 20 }: { size?: number }): JSX.Element {
73
+ return (
74
+ <Box
75
+ component="svg"
76
+ aria-hidden
77
+ viewBox="0 0 24 24"
78
+ sx={{
79
+ width: size,
80
+ height: size,
81
+ flex: "0 0 auto",
82
+ fill: "none",
83
+ stroke: "currentColor",
84
+ strokeWidth: 2,
85
+ color: "text.secondary",
86
+ }}
87
+ >
88
+ <path d="M4 4h16v12H8l-4 4z" />
89
+ </Box>
90
+ );
91
+ }
92
+
93
+ /** The amount, the one number the whole step is about. */
94
+ export function TotalAmount({
95
+ totalLabel,
96
+ big,
97
+ }: {
98
+ totalLabel: string;
99
+ big: boolean;
100
+ }): JSX.Element {
101
+ const { Text } = useCheckoutComponents();
102
+ return (
103
+ <Box
104
+ sx={{
105
+ "&& > *": {
106
+ fontSize: big ? "2.5rem" : "1.625rem",
107
+ lineHeight: 1.1,
108
+ letterSpacing: big ? "-0.02em" : 0,
109
+ },
110
+ }}
111
+ >
112
+ <Text
113
+ variant="heading"
114
+ size="md"
115
+ weight="bold"
116
+ color={big ? undefined : "primary"}
117
+ as="span"
118
+ data-testid="pay-bar-total"
119
+ >
120
+ {totalLabel}
121
+ </Text>
122
+ </Box>
123
+ );
124
+ }
125
+
126
+ function PayerRow({
127
+ payer,
128
+ }: {
129
+ payer: NonNullable<OrderSummaryParts["payer"]>;
130
+ }): JSX.Element {
131
+ const { Button, Text } = useCheckoutComponents();
132
+ const copy = useCheckoutCopy().screens.payer;
133
+ const cpf = payer.taxId?.trim()
134
+ ? copy.taxId(formatCpf(payer.taxId))
135
+ : copy.taxIdAlreadyKnown;
136
+ return (
137
+ <Box
138
+ data-testid="checkout-payer"
139
+ sx={ROW_SX}
140
+ aria-label={
141
+ payer.name ? copy.payingAs(payer.name) : copy.payingWithSavedDetails
142
+ }
143
+ role="group"
144
+ >
145
+ <PersonOutlineIcon sx={ICON_SX} />
146
+ <Box sx={{ flex: 1, minWidth: 0 }}>
147
+ <Text variant="body" size="sm" as="p" data-testid="checkout-payer-name">
148
+ {payer.name ?? copy.payingWithSavedDetails}
149
+ </Text>
150
+ <Text variant="body" size="xs" as="p" data-testid="checkout-payer-cpf">
151
+ {cpf}
152
+ </Text>
153
+ </Box>
154
+ <Box sx={EDIT_SX}>
155
+ <Button
156
+ variant="text"
157
+ color="primary"
158
+ size="sm"
159
+ onClick={payer.onEdit}
160
+ dataTestId="checkout-payer-edit"
161
+ >
162
+ {copy.changeAction}
163
+ </Button>
164
+ </Box>
165
+ </Box>
166
+ );
167
+ }
168
+
169
+ function AddressRow({
170
+ delivery,
171
+ }: {
172
+ delivery: CheckoutDeliverySummary;
173
+ }): JSX.Element {
174
+ const { Button, Text } = useCheckoutComponents();
175
+ const copy = useCheckoutCopy().screens;
176
+ return (
177
+ <Box data-testid="checkout-address" sx={ROW_SX}>
178
+ <PlaceOutlinedIcon sx={ICON_SX} />
179
+ <Box sx={{ flex: 1, minWidth: 0 }}>
180
+ <Text
181
+ variant="body"
182
+ size="sm"
183
+ as="p"
184
+ data-testid="checkout-address-line1"
185
+ >
186
+ {delivery.line1}
187
+ </Text>
188
+ {delivery.line2 ? (
189
+ <Text variant="body" size="xs" as="p">
190
+ {delivery.line2}
191
+ </Text>
192
+ ) : null}
193
+ </Box>
194
+ <Box sx={EDIT_SX}>
195
+ <Button
196
+ variant="text"
197
+ color="primary"
198
+ size="sm"
199
+ onClick={delivery.onEdit}
200
+ dataTestId="checkout-address-edit"
201
+ >
202
+ <span aria-hidden>{copy.payer.changeAction}</span>
203
+ <Box component="span" sx={VISUALLY_HIDDEN}>
204
+ {copy.layout.changeAddress}
205
+ </Box>
206
+ </Button>
207
+ </Box>
208
+ </Box>
209
+ );
210
+ }
211
+
212
+ const DIVIDER = <Box sx={{ height: "1px", bgcolor: "divider" }} />;
213
+
214
+ /** The cart's lines, "2× X-Burger … R$ 36,00", when the host passed them. */
215
+ function SummaryLines({ lines }: { lines: CheckoutSummaryLine[] }): JSX.Element {
216
+ const { Text } = useCheckoutComponents();
217
+ const copy = useCheckoutCopy().screens.layout;
218
+ return (
219
+ <Box component="ul" data-testid="checkout-summary-lines" sx={{ m: 0, p: 0, listStyle: "none", display: "flex", flexDirection: "column", gap: 1 }}>
220
+ {lines.map((line) => (
221
+ <Box component="li" key={line.key} sx={{ display: "flex", justifyContent: "space-between", gap: 2 }}>
222
+ <Text variant="body" size="sm" as="span">
223
+ {copy.lineLabel(line.quantity, line.name)}
224
+ </Text>
225
+ <Text variant="body" size="sm" as="span">
226
+ {line.totalLabel}
227
+ </Text>
228
+ </Box>
229
+ ))}
230
+ </Box>
231
+ );
232
+ }
233
+
234
+ /** "Total" and the amount on one line, the host's money rows under it. */
235
+ function SummaryTotal({ totalLabel, discountLines }: { totalLabel: string; discountLines?: ReactNode }): JSX.Element {
236
+ const { Text } = useCheckoutComponents();
237
+ const copy = useCheckoutCopy().screens.layout;
238
+ return (
239
+ <Box data-testid="payment-step-total" sx={{ display: "flex", flexDirection: "column", gap: 0.5 }}>
240
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 2 }}>
241
+ <Text variant="body" size="md" weight="semibold" as="span">
242
+ {copy.total}
243
+ </Text>
244
+ <TotalAmount totalLabel={totalLabel} big={false} />
245
+ </Box>
246
+ {discountLines}
247
+ </Box>
248
+ );
249
+ }
250
+
251
+ /** The host's note control, on a row of its own with the note icon. */
252
+ function NoteRow({ note }: { note: ReactNode }): JSX.Element {
253
+ return (
254
+ <Box sx={{ ...ROW_SX, alignItems: "center" }}>
255
+ <NoteIcon />
256
+ <Box sx={{ flex: 1, minWidth: 0, "& .MuiButton-root, & button": { px: 0 } }}>{note}</Box>
257
+ </Box>
258
+ );
259
+ }
260
+
261
+ /**
262
+ * The computer's summary card, beside the payment (FUT-3525, layout A).
263
+ * Sticky, so the total and the address stay in view while the buyer fills
264
+ * the card form.
265
+ */
266
+ export function OrderSummaryCard({ parts }: { parts: OrderSummaryParts }): JSX.Element {
267
+ const { Text } = useCheckoutComponents();
268
+ const copy = useCheckoutCopy().screens.layout;
269
+ const lines = parts.lines ?? [];
270
+ const hasWho = Boolean(parts.payer || parts.delivery || parts.orderNote);
271
+ return (
272
+ <Box
273
+ component="aside"
274
+ aria-label={copy.summaryTitle}
275
+ data-testid="checkout-summary"
276
+ sx={{ position: "sticky", top: 24, display: "flex", flexDirection: "column", gap: 2, p: 2.75, bgcolor: "background.paper", border: "1px solid", borderColor: "divider", borderRadius: 2.5 }}
277
+ >
278
+ <Text variant="heading" size="md" weight="bold" as="h2">
279
+ {copy.summaryTitle}
280
+ </Text>
281
+ {lines.length > 0 ? <SummaryLines lines={lines} /> : null}
282
+ {lines.length > 0 ? DIVIDER : null}
283
+ <SummaryTotal totalLabel={parts.totalLabel} discountLines={parts.discountLines} />
284
+ {hasWho ? DIVIDER : null}
285
+ {parts.payer ? <PayerRow payer={parts.payer} /> : null}
286
+ {parts.delivery ? <AddressRow delivery={parts.delivery} /> : null}
287
+ {parts.orderNote ? <NoteRow note={parts.orderNote} /> : null}
288
+ </Box>
289
+ );
290
+ }
@@ -129,7 +129,7 @@ function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAf
129
129
  const { Button } = useCheckoutComponents();
130
130
  const copy = useCheckoutCopy().screens.pix;
131
131
  return (
132
- <Box sx={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 1.25, "& button": { minHeight: 48, height: "100%" }, "& button, & button *": { fontWeight: 700 }, [PIX_WIDE]: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1.5, "& button": { height: "auto" } } }}>
132
+ <Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))", gap: 1.25, "& button": { minHeight: 48, height: "100%" }, "& button, & button *": { fontWeight: 700 }, [PIX_WIDE]: { display: "flex", flexWrap: "wrap", alignItems: "center", gap: 1.5, "& button": { height: "auto" } } }}>
133
133
  {/* Narrow, a grid cell; wide, its own width beside the link (the prototype's row). */}
134
134
  <Box sx={{ minWidth: 0, [PIX_WIDE]: { flex: "none", "& > *": { width: "auto" } } }}>
135
135
  <Button variant="outline" color="neutral" size="md" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopyAgain} dataTestId="pix-copy-again">