@12-apps/payments-frontend 3.36.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/components/checkout/cart-changed.tsx +24 -3
- package/src/components/checkout/pix-after-copy.tsx +10 -3
- package/src/components/checkout/pix-pane.tsx +33 -22
- package/src/components/checkout/pix-parts.tsx +47 -1
- package/src/components/checkout/screens-copy.ts +6 -0
- package/src/components/checkout/screens-en-US.ts +1 -0
- package/src/components/checkout/screens-pt-BR.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.37.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.",
|
|
@@ -199,6 +199,23 @@ export function useRefreshOnCartChanged(errorCode: string | null): void {
|
|
|
199
199
|
}, [errorCode]);
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/** Where a refresh stands, for a step that may not have one (a settlement). */
|
|
203
|
+
function refreshStatus(refresh: OrderRefresh | undefined): { pending: boolean; error: string | null } {
|
|
204
|
+
return { pending: refresh?.pending ?? false, error: refresh?.error ?? null };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const PaymentStaleContext = createContext(false);
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether the payment on screen was prepared for a cart that has since changed
|
|
211
|
+
* (FUT-3535). A PIX pane reads it to veil its QR and code and to stop the copy
|
|
212
|
+
* controls: the amount is IN the code, so a buyer who pays it now pays the old
|
|
213
|
+
* order. False outside a {@link CartChangedBoundary}.
|
|
214
|
+
*/
|
|
215
|
+
export function usePaymentStale(): boolean {
|
|
216
|
+
return useContext(PaymentStaleContext);
|
|
217
|
+
}
|
|
218
|
+
|
|
202
219
|
/**
|
|
203
220
|
* The payment pane, with the step's warning above it when the cart moved, and
|
|
204
221
|
* the refresh handed down to the card form. Nothing changes on screen while the
|
|
@@ -222,15 +239,19 @@ export function CartChangedBoundary({
|
|
|
222
239
|
}): JSX.Element {
|
|
223
240
|
const changed = useBasketChanged(order, basket) && !creating;
|
|
224
241
|
const run = paymentRefresh(order, onGenerate, refresh?.run);
|
|
225
|
-
const pending = refresh
|
|
226
|
-
const error = refresh?.error ?? null;
|
|
242
|
+
const { pending, error } = refreshStatus(refresh);
|
|
227
243
|
// ONE tree shape whether or not the warning shows: wrapping the pane only
|
|
228
244
|
// while it does would remount the card form the moment the refresh lands,
|
|
229
245
|
// dropping what the buyer typed and the refusal that explains the new total.
|
|
230
246
|
return (
|
|
231
247
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
|
232
248
|
{(changed || error) && run ? <CartChangedNotice onRefresh={run} pending={pending} error={error} /> : null}
|
|
233
|
-
|
|
249
|
+
{/* Stale while the warning stands, so the veil goes with it the moment the
|
|
250
|
+
new order lands. A PIX refresh mints a new code, which takes the old
|
|
251
|
+
one off the screen first, so a failed one leaves nothing to copy. */}
|
|
252
|
+
<PaymentStaleContext.Provider value={changed && run !== null}>
|
|
253
|
+
<PaymentRefreshProvider refresh={run}>{children}</PaymentRefreshProvider>
|
|
254
|
+
</PaymentStaleContext.Provider>
|
|
234
255
|
</Box>
|
|
235
256
|
);
|
|
236
257
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
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 { CheckIcon, ContentCopyIcon, CreditCardIcon, QrCodeIcon, ScheduleIcon } from "./icons";
|
|
6
|
-
import { Emphasized, PIX_WIDE, PixQr } from "./pix-parts";
|
|
7
|
+
import { Emphasized, PIX_WIDE, PixQr, StaleVeil } from "./pix-parts";
|
|
7
8
|
import type { PixAfterCopyCopy } from "./screens-copy";
|
|
8
9
|
import type { PixCharge } from "./types";
|
|
9
10
|
import { useCheckoutComponents } from "./ui";
|
|
@@ -128,11 +129,14 @@ interface PixAfterCopyProps {
|
|
|
128
129
|
function Actions({ justCopied, onCopyAgain, onShowQr, onPreferCard }: Pick<PixAfterCopyProps, "justCopied" | "onCopyAgain" | "onShowQr" | "onPreferCard">): JSX.Element {
|
|
129
130
|
const { Button } = useCheckoutComponents();
|
|
130
131
|
const copy = useCheckoutCopy().screens.pix;
|
|
132
|
+
// The code already copied is the OLD order's once the cart changed
|
|
133
|
+
// (FUT-3535): copying it again would only hand the buyer the wrong amount.
|
|
134
|
+
const stale = usePaymentStale();
|
|
131
135
|
return (
|
|
132
136
|
<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
137
|
{/* Narrow, a grid cell; wide, its own width beside the link (the prototype's row). */}
|
|
134
138
|
<Box sx={{ minWidth: 0, [PIX_WIDE]: { flex: "none", "& > *": { width: "auto" } } }}>
|
|
135
|
-
<Button variant="outline" color="neutral" size="md" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopyAgain} dataTestId="pix-copy-again">
|
|
139
|
+
<Button variant="outline" color="neutral" size="md" fullWidth icon={<ContentCopyIcon fontSize="small" />} onClick={onCopyAgain} disabled={stale} dataTestId="pix-copy-again">
|
|
136
140
|
{justCopied ? copy.copiedAction : copy.copyAgainAction}
|
|
137
141
|
</Button>
|
|
138
142
|
</Box>
|
|
@@ -156,6 +160,7 @@ export function PixAfterCopy(props: PixAfterCopyProps): JSX.Element {
|
|
|
156
160
|
const { Button, Text } = useCheckoutComponents();
|
|
157
161
|
const copy = useCheckoutCopy().screens.pix;
|
|
158
162
|
const { pix, words, totalLabel, deadline, onPreferCard, footer } = props;
|
|
163
|
+
const stale = usePaymentStale();
|
|
159
164
|
return (
|
|
160
165
|
<Box data-testid="pix-after-copy" sx={{ display: "flex", flexWrap: "wrap", gap: 4, alignItems: "flex-start" }}>
|
|
161
166
|
<Box sx={{ flex: "999 1 420px", minWidth: 0, display: "flex", flexDirection: "column", gap: 2.5 }}>
|
|
@@ -175,7 +180,9 @@ export function PixAfterCopy(props: PixAfterCopyProps): JSX.Element {
|
|
|
175
180
|
<Text variant="body" size="xs" weight="bold" color="secondary" as="p" style={{ margin: 0, textTransform: "uppercase", letterSpacing: "0.04em" }}>
|
|
176
181
|
{copy.notPaidYet}
|
|
177
182
|
</Text>
|
|
178
|
-
<
|
|
183
|
+
<StaleVeil stale={stale}>
|
|
184
|
+
<PixQr payload={pix.copyPaste} size={168} alt={copy.qrAlt} />
|
|
185
|
+
</StaleVeil>
|
|
179
186
|
<Text variant="body" size="xs" color="secondary" as="p" style={{ margin: 0, textAlign: "center" }}>
|
|
180
187
|
{copy.scanFromPhone}
|
|
181
188
|
</Text>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Box } from "@mui/material";
|
|
2
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
7
|
import { StickyPayBar, useCheckoutLayout } from "./payment-step-layout";
|
|
7
|
-
import { Emphasized, PIX_WIDE, PixQr } from "./pix-parts";
|
|
8
|
+
import { Emphasized, PIX_WIDE, PixQr, StaleVeil } from "./pix-parts";
|
|
8
9
|
import type { PixCharge } from "./types";
|
|
9
10
|
import { useCheckoutComponents } from "./ui";
|
|
10
11
|
|
|
@@ -64,6 +65,7 @@ function ColumnHeading({
|
|
|
64
65
|
function QrPanel({ pix }: { pix: PixCharge }): JSX.Element {
|
|
65
66
|
const { Text } = useCheckoutComponents();
|
|
66
67
|
const copy = useCheckoutCopy().screens.pix;
|
|
68
|
+
const stale = usePaymentStale();
|
|
67
69
|
return (
|
|
68
70
|
<Box
|
|
69
71
|
data-testid="pix-qr-panel"
|
|
@@ -100,12 +102,14 @@ function QrPanel({ pix }: { pix: PixCharge }): JSX.Element {
|
|
|
100
102
|
[PIX_WIDE]: { "& svg": { width: 232, height: 232 } },
|
|
101
103
|
}}
|
|
102
104
|
>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
<StaleVeil stale={stale}>
|
|
106
|
+
<PixQr
|
|
107
|
+
payload={pix.copyPaste}
|
|
108
|
+
size={232}
|
|
109
|
+
alt={copy.qrAlt}
|
|
110
|
+
testId="pix-qr"
|
|
111
|
+
/>
|
|
112
|
+
</StaleVeil>
|
|
109
113
|
</Box>
|
|
110
114
|
<Text variant="body" size="sm" color="secondary" as="p" style={{ margin: 0, maxWidth: 280 }}>
|
|
111
115
|
<Box component="span" sx={NARROW_ONLY}>{copy.qrTabCaption}</Box>
|
|
@@ -125,6 +129,9 @@ function QrPanel({ pix }: { pix: PixCharge }): JSX.Element {
|
|
|
125
129
|
function CopyButton({ onCopy }: { onCopy: () => void }): JSX.Element {
|
|
126
130
|
const { Button } = useCheckoutComponents();
|
|
127
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();
|
|
128
135
|
return (
|
|
129
136
|
<StickyPayBar>
|
|
130
137
|
<Box
|
|
@@ -141,6 +148,7 @@ function CopyButton({ onCopy }: { onCopy: () => void }): JSX.Element {
|
|
|
141
148
|
fullWidth
|
|
142
149
|
icon={<ContentCopyIcon fontSize="small" />}
|
|
143
150
|
onClick={onCopy}
|
|
151
|
+
disabled={stale}
|
|
144
152
|
dataTestId="pix-copy"
|
|
145
153
|
>
|
|
146
154
|
{copy.copyAction}
|
|
@@ -162,6 +170,7 @@ function CopyPanel({
|
|
|
162
170
|
}): JSX.Element {
|
|
163
171
|
const { Text } = useCheckoutComponents();
|
|
164
172
|
const copy = useCheckoutCopy().screens.pix;
|
|
173
|
+
const stale = usePaymentStale();
|
|
165
174
|
return (
|
|
166
175
|
<Box
|
|
167
176
|
data-testid="pix-copy-panel"
|
|
@@ -179,21 +188,23 @@ function CopyPanel({
|
|
|
179
188
|
(a tinted chip, its own border and padding) is undone inside it:
|
|
180
189
|
otherwise a wrapped code reads as a box inside a box.
|
|
181
190
|
*/}
|
|
182
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
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>
|
|
197
208
|
{inPanel ? <CopyButton onCopy={onCopy} /> : null}
|
|
198
209
|
<Text
|
|
199
210
|
variant="body"
|
|
@@ -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
|
|
@@ -79,3 +81,47 @@ export function useClipboardCopy(text: string): { copy: () => Promise<boolean>;
|
|
|
79
81
|
}, [text]);
|
|
80
82
|
return { copy, justCopied };
|
|
81
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
|
+
}
|
|
@@ -284,6 +284,12 @@ export interface CartChangedCopy {
|
|
|
284
284
|
refreshAction: string;
|
|
285
285
|
/** The heading over a refresh that failed; the host's own sentence is the body. */
|
|
286
286
|
refreshFailed: string;
|
|
287
|
+
/**
|
|
288
|
+
* Drawn over a PIX's QR and code while they are veiled (FUT-3535): the code
|
|
289
|
+
* on screen asks for the old amount, so it cannot be copied or scanned until
|
|
290
|
+
* the payment is updated.
|
|
291
|
+
*/
|
|
292
|
+
pixVeil: string;
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
/** What the browser says when the wire itself failed. */
|
|
@@ -159,6 +159,7 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
159
159
|
support: 'This payment was prepared for your previous cart. Update it to pay the right amount.',
|
|
160
160
|
refreshAction: 'Update payment',
|
|
161
161
|
refreshFailed: 'Could not update the payment',
|
|
162
|
+
pixVeil: 'Update to get a new Pix code',
|
|
162
163
|
},
|
|
163
164
|
validation: {
|
|
164
165
|
taxIdInvalid: 'That CPF is not valid.',
|
|
@@ -145,6 +145,7 @@ export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
145
145
|
support: 'O pagamento foi preparado para o carrinho anterior. Atualize para pagar o valor certo.',
|
|
146
146
|
refreshAction: 'Atualizar pagamento',
|
|
147
147
|
refreshFailed: 'Não foi possível atualizar o pagamento',
|
|
148
|
+
pixVeil: 'Atualize para gerar um novo Pix',
|
|
148
149
|
},
|
|
149
150
|
validation: {
|
|
150
151
|
taxIdInvalid: 'CPF inválido.',
|