@12-apps/payments-frontend 3.36.0 → 3.38.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/card-sources.ts +131 -0
- package/src/components/checkout/card-view.tsx +6 -2
- package/src/components/checkout/cart-changed.tsx +50 -18
- package/src/components/checkout/checkout-actions.ts +18 -0
- package/src/components/checkout/checkout-flow.tsx +24 -77
- package/src/components/checkout/google-pay-button.tsx +4 -4
- package/src/components/checkout/list/buyer-inline.tsx +125 -0
- package/src/components/checkout/list/charge-runners.tsx +156 -0
- package/src/components/checkout/list/choice-list.tsx +156 -0
- package/src/components/checkout/list/choice.ts +193 -0
- package/src/components/checkout/list/context.tsx +55 -0
- package/src/components/checkout/list/en-US.ts +61 -0
- package/src/components/checkout/list/list-bar.tsx +186 -0
- package/src/components/checkout/list/list-face.tsx +315 -0
- package/src/components/checkout/list/list-pix.tsx +206 -0
- package/src/components/checkout/list/online-content.tsx +137 -0
- package/src/components/checkout/list/option-row.tsx +123 -0
- package/src/components/checkout/list/payment-list-step.tsx +166 -0
- package/src/components/checkout/list/pt-BR.ts +77 -0
- package/src/components/checkout/list/types.ts +187 -0
- package/src/components/checkout/list/use-capability.ts +53 -0
- package/src/components/checkout/list/use-list-charge.ts +161 -0
- package/src/components/checkout/list/use-list-state.ts +152 -0
- package/src/components/checkout/payment-phase.tsx +119 -0
- 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 +50 -1
- package/src/components/checkout/pix-view.tsx +53 -20
- 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/src/components/checkout/use-card-checkout.ts +11 -78
- package/src/components/checkout/use-checkout-controller.ts +3 -1
- package/src/flows/create-payment-flows.tsx +7 -0
- package/src/flows/types.ts +7 -0
- package/src/index.ts +4 -5
- package/src/locales.ts +13 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CheckoutCartView, CheckoutFlowProps } from "./checkout-flow";
|
|
4
|
+
import { PaymentStep } from "./checkout-steps";
|
|
5
|
+
import { PaymentListStep } from "./list/payment-list-step";
|
|
6
|
+
import { OneClickProvider } from "./one-click";
|
|
7
|
+
import type { CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
8
|
+
import type { useCheckoutController } from "./use-checkout-controller";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Step 2 of `<CheckoutFlow>`, moved out of `./checkout-flow.tsx` at its size
|
|
12
|
+
* gate when the Pagamento step gained a second shape (FUT-3591): the method
|
|
13
|
+
* tabs it always had, or the one list a host opts into.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The pay-bar total override when settling a settlement (else the cart's own totals). */
|
|
17
|
+
export function settlementTotalOverride(
|
|
18
|
+
settlement: SettlementCheckout | null | undefined,
|
|
19
|
+
): { label: string; items: number } | undefined {
|
|
20
|
+
return settlement ? { label: settlement.totalLabel, items: settlement.totalItems } : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Step 2, with the controller's facts and the host's money mapped onto it. */
|
|
24
|
+
function PagamentoStep({
|
|
25
|
+
c,
|
|
26
|
+
cart,
|
|
27
|
+
settlement,
|
|
28
|
+
providerConfig,
|
|
29
|
+
tenantSlug,
|
|
30
|
+
validateApplePayMerchant,
|
|
31
|
+
}: {
|
|
32
|
+
c: ReturnType<typeof useCheckoutController>;
|
|
33
|
+
cart: CheckoutCartView;
|
|
34
|
+
settlement: SettlementCheckout | null | undefined;
|
|
35
|
+
providerConfig: CheckoutProviderConfig | null | undefined;
|
|
36
|
+
tenantSlug: string | undefined;
|
|
37
|
+
validateApplePayMerchant: ((validationURL: string) => Promise<unknown>) | undefined;
|
|
38
|
+
}): JSX.Element {
|
|
39
|
+
return (
|
|
40
|
+
<PaymentStep
|
|
41
|
+
method={c.method}
|
|
42
|
+
onMethodChange={c.setMethod}
|
|
43
|
+
order={c.order}
|
|
44
|
+
buyer={c.buyer}
|
|
45
|
+
creating={c.creating}
|
|
46
|
+
createError={c.createError}
|
|
47
|
+
errorField={c.errorField}
|
|
48
|
+
errorCode={c.errorCode}
|
|
49
|
+
onGenerate={(chosen) => void c.startPayment(chosen)}
|
|
50
|
+
refresh={c.refresh}
|
|
51
|
+
onUseEmail={c.payWithEmail}
|
|
52
|
+
// Set only for a skipped-Dados flow (the controller decides); the payer
|
|
53
|
+
// block hides itself when it is absent.
|
|
54
|
+
onEditBuyer={c.editBuyer}
|
|
55
|
+
providerConfig={providerConfig}
|
|
56
|
+
tenantSlug={tenantSlug}
|
|
57
|
+
// The amount, on the step that asks for it (FUT-1179).
|
|
58
|
+
cartTotals={cart}
|
|
59
|
+
totalOverride={settlementTotalOverride(settlement)}
|
|
60
|
+
discountLines={cart.discountLines}
|
|
61
|
+
// The order around the payment, for the summary (FUT-3525).
|
|
62
|
+
lines={cart.lines}
|
|
63
|
+
delivery={cart.delivery}
|
|
64
|
+
orderNote={cart.orderNote}
|
|
65
|
+
// Retrying a refused card: the failed saved card is not chosen again (FUT-1145).
|
|
66
|
+
freshInstrument={c.freshInstrument}
|
|
67
|
+
// The card path parks an order of its own for a 3-D Secure challenge, so
|
|
68
|
+
// it needs the same basket the flow was mounted for (FUT-1213).
|
|
69
|
+
basket={cart.identity}
|
|
70
|
+
validateApplePayMerchant={validateApplePayMerchant}
|
|
71
|
+
onResolved={c.handleResolved}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Step 2, in whichever shape the host asked for: the one list, or the tabs.
|
|
78
|
+
* The list never arms one-click — it raises nothing on its own, which is the
|
|
79
|
+
* one-click request's ordinary degraded outcome anyway.
|
|
80
|
+
*/
|
|
81
|
+
export function PaymentPhase({
|
|
82
|
+
props,
|
|
83
|
+
c,
|
|
84
|
+
armed,
|
|
85
|
+
}: {
|
|
86
|
+
props: Omit<CheckoutFlowProps, "components">;
|
|
87
|
+
c: ReturnType<typeof useCheckoutController>;
|
|
88
|
+
armed: boolean;
|
|
89
|
+
}): JSX.Element {
|
|
90
|
+
const { cart, settlement, providerConfig, tenantSlug, validateApplePayMerchant, paymentList } = props;
|
|
91
|
+
if (paymentList) {
|
|
92
|
+
return (
|
|
93
|
+
<PaymentListStep
|
|
94
|
+
c={c}
|
|
95
|
+
list={paymentList}
|
|
96
|
+
cart={cart}
|
|
97
|
+
totalOverride={settlementTotalOverride(settlement)}
|
|
98
|
+
providerConfig={providerConfig}
|
|
99
|
+
tenantSlug={tenantSlug}
|
|
100
|
+
taxIdOnFile={props.taxIdOnFile ?? false}
|
|
101
|
+
defaultBuyer={props.defaultBuyer}
|
|
102
|
+
saveBuyerContact={props.saveBuyerContact}
|
|
103
|
+
consentLabel={props.copy.dados.saveProfile}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return (
|
|
108
|
+
<OneClickProvider armed={armed}>
|
|
109
|
+
<PagamentoStep
|
|
110
|
+
c={c}
|
|
111
|
+
cart={cart}
|
|
112
|
+
settlement={settlement}
|
|
113
|
+
providerConfig={providerConfig}
|
|
114
|
+
tenantSlug={tenantSlug}
|
|
115
|
+
validateApplePayMerchant={validateApplePayMerchant}
|
|
116
|
+
/>
|
|
117
|
+
</OneClickProvider>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -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,50 @@ 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
|
+
// An opaque overlay over the blurred code (ADR 0121): it covers
|
|
109
|
+
// what is under it on purpose, so the review probe reads it so.
|
|
110
|
+
data-ui-overlay=""
|
|
111
|
+
sx={{
|
|
112
|
+
m: 0,
|
|
113
|
+
px: 1.5,
|
|
114
|
+
py: 0.75,
|
|
115
|
+
borderRadius: 999,
|
|
116
|
+
bgcolor: "background.paper",
|
|
117
|
+
boxShadow: 1,
|
|
118
|
+
color: "text.primary",
|
|
119
|
+
fontSize: "0.8125rem",
|
|
120
|
+
fontWeight: 600,
|
|
121
|
+
textAlign: "center",
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{words}
|
|
125
|
+
</Box>
|
|
126
|
+
</Box>
|
|
127
|
+
) : null}
|
|
128
|
+
</Box>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -2,6 +2,8 @@ import { Box } from "@mui/material";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, type JSX } from "react";
|
|
3
3
|
|
|
4
4
|
import { useCheckoutCopy } from "./copy-context";
|
|
5
|
+
import { usePaymentListMode } from "./list/context";
|
|
6
|
+
import { ListPixFace } from "./list/list-pix";
|
|
5
7
|
import { PixAfterCopy } from "./pix-after-copy";
|
|
6
8
|
import { PixPane } from "./pix-pane";
|
|
7
9
|
import { useCheckoutLayout } from "./payment-step-layout";
|
|
@@ -221,15 +223,19 @@ function PixTitle(): JSX.Element | null {
|
|
|
221
223
|
);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
226
|
+
/**
|
|
227
|
+
* The clipboard, and the copy that moves the pane on to its after-copy face.
|
|
228
|
+
*
|
|
229
|
+
* Leaving the payment step (a method switch, a terminal status) ends the
|
|
230
|
+
* after-copy face with it: the next pane starts on the code. And a clipboard
|
|
231
|
+
* write that settles after the pane is gone must not reopen the after-copy
|
|
232
|
+
* face over whatever replaced it (it would hide the picker).
|
|
233
|
+
*/
|
|
234
|
+
function useCopyThenWait(
|
|
235
|
+
payload: string,
|
|
236
|
+
setAfterCopy: (afterCopy: boolean) => void,
|
|
237
|
+
): { writeClipboard: () => Promise<boolean>; justCopied: boolean; copyThenWait: () => void } {
|
|
238
|
+
const { copy: writeClipboard, justCopied } = useClipboardCopy(payload);
|
|
233
239
|
const mounted = useRef(true);
|
|
234
240
|
useEffect(() => {
|
|
235
241
|
mounted.current = true;
|
|
@@ -238,6 +244,22 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
|
|
|
238
244
|
setAfterCopy(false);
|
|
239
245
|
};
|
|
240
246
|
}, [setAfterCopy]);
|
|
247
|
+
// Only a copy the clipboard TOOK moves on: the next face opens on "Código
|
|
248
|
+
// Pix copiado", and a failed write leaves the code on screen to select by hand.
|
|
249
|
+
const copyThenWait = (): void => {
|
|
250
|
+
void writeClipboard().then((ok) => {
|
|
251
|
+
if (ok && mounted.current) setAfterCopy(true);
|
|
252
|
+
});
|
|
253
|
+
};
|
|
254
|
+
return { writeClipboard, justCopied, copyThenWait };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; poll: ReturnType<typeof usePixPolling> }): JSX.Element {
|
|
258
|
+
const copy = useCheckoutCopy().screens.pix;
|
|
259
|
+
const stage = usePixStage();
|
|
260
|
+
const listMode = usePaymentListMode();
|
|
261
|
+
const { writeClipboard, justCopied, copyThenWait } = useCopyThenWait(pix.copyPaste, stage.setAfterCopy);
|
|
262
|
+
const { setAfterCopy } = stage;
|
|
241
263
|
|
|
242
264
|
// The locale is the HOST's (FUT-760): it decides what a buyer reads off the
|
|
243
265
|
// clock, so it travels with the sentence it feeds rather than being frozen
|
|
@@ -247,6 +269,27 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
|
|
|
247
269
|
const footerLine = (awaiting: string): JSX.Element => (
|
|
248
270
|
<PixPollFooter error={poll.error} timedOut={poll.timedOut} onCheckAgain={poll.checkAgain} awaiting={awaiting} />
|
|
249
271
|
);
|
|
272
|
+
// Inside the one list (FUT-3592), the code itself is the pane and the bar
|
|
273
|
+
// carries the copy; the wait is said by the bar, so only a STALLED poll
|
|
274
|
+
// draws here.
|
|
275
|
+
if (listMode) {
|
|
276
|
+
return (
|
|
277
|
+
<ListPixFace
|
|
278
|
+
mode={listMode}
|
|
279
|
+
order={order}
|
|
280
|
+
pix={pix}
|
|
281
|
+
time={time}
|
|
282
|
+
afterCopy={stage.afterCopy}
|
|
283
|
+
words={sentences.afterCopy}
|
|
284
|
+
justCopied={justCopied}
|
|
285
|
+
onCopy={copyThenWait}
|
|
286
|
+
onCopyAgain={() => void writeClipboard()}
|
|
287
|
+
onShowQr={() => setAfterCopy(false)}
|
|
288
|
+
onPreferCard={stage.preferCard}
|
|
289
|
+
footer={poll.error || poll.timedOut ? footerLine(copy.verifying) : null}
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
250
293
|
|
|
251
294
|
if (stage.afterCopy) {
|
|
252
295
|
return (
|
|
@@ -267,17 +310,7 @@ function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; p
|
|
|
267
310
|
return (
|
|
268
311
|
<>
|
|
269
312
|
<PixTitle />
|
|
270
|
-
<PixPane
|
|
271
|
-
pix={pix}
|
|
272
|
-
onCopy={() => {
|
|
273
|
-
// Only a copy the clipboard TOOK moves on: the next face opens on
|
|
274
|
-
// "Código Pix copiado", and a failed write leaves the code on screen
|
|
275
|
-
// to select by hand.
|
|
276
|
-
void writeClipboard().then((ok) => {
|
|
277
|
-
if (ok && mounted.current) setAfterCopy(true);
|
|
278
|
-
});
|
|
279
|
-
}}
|
|
280
|
-
/>
|
|
313
|
+
<PixPane pix={pix} onCopy={copyThenWait} />
|
|
281
314
|
<PixFooter deadline={sentences.validUntil(time)}>{footerLine(sentences.awaiting)}</PixFooter>
|
|
282
315
|
</>
|
|
283
316
|
);
|
|
@@ -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.',
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
NEW_CARD,
|
|
5
5
|
detectBrand,
|
|
6
6
|
onlyDigits,
|
|
7
|
-
tokenizerFor,
|
|
8
7
|
validateCardNumber,
|
|
9
8
|
validateCvv,
|
|
10
9
|
validateExpiry,
|
|
@@ -23,6 +22,7 @@ import {
|
|
|
23
22
|
type RefreshBrowserKey,
|
|
24
23
|
} from "./card-instruments";
|
|
25
24
|
import { handOverToChallenge, reportResolved, type ChallengeScope } from "./card-outcome";
|
|
25
|
+
import { useCardPublicKey, useSavedCards } from "./card-sources";
|
|
26
26
|
import { useCheckoutClientApi } from "./client-context";
|
|
27
27
|
import { useCheckoutNavigate } from "./navigate-context";
|
|
28
28
|
import type { CardChainLink } from "./method-capability";
|
|
@@ -34,80 +34,6 @@ import { useCheckoutCopy } from "./copy-context";
|
|
|
34
34
|
|
|
35
35
|
const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
* Saved-card list + current selection, loaded once on mount — scoped to the
|
|
39
|
-
* current store (FUT-697), so only cards the store's active provider can
|
|
40
|
-
* actually charge are offered. The slug arrives as an argument (the host's
|
|
41
|
-
* routing owns it); absent, the list is unscoped, exactly as before FUT-697.
|
|
42
|
-
*/
|
|
43
|
-
function useSavedCards(
|
|
44
|
-
tenantSlug: string | undefined,
|
|
45
|
-
/**
|
|
46
|
-
* The buyer is here because a card was REFUSED (FUT-1145), so the saved card
|
|
47
|
-
* this would otherwise preselect is the one that just failed. The list is
|
|
48
|
-
* still offered — another saved card may well work — but nothing is chosen
|
|
49
|
-
* for them, which puts the form in front of a buyer whose only untried
|
|
50
|
-
* instrument is a new one.
|
|
51
|
-
*/
|
|
52
|
-
freshInstrument: boolean,
|
|
53
|
-
): {
|
|
54
|
-
savedCards: SavedCard[];
|
|
55
|
-
selection: string;
|
|
56
|
-
setSelection: (id: string) => void;
|
|
57
|
-
} {
|
|
58
|
-
const [savedCards, setSavedCards] = useState<SavedCard[]>([]);
|
|
59
|
-
const [selection, setSelection] = useState<string>(NEW_CARD);
|
|
60
|
-
const client = useCheckoutClientApi();
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
let active = true;
|
|
64
|
-
void client.listInstruments(tenantSlug).then((cards) => {
|
|
65
|
-
if (!active) return;
|
|
66
|
-
setSavedCards(cards);
|
|
67
|
-
const first = cards[0];
|
|
68
|
-
if (first && !freshInstrument) setSelection(first.id);
|
|
69
|
-
});
|
|
70
|
-
return () => {
|
|
71
|
-
active = false;
|
|
72
|
-
};
|
|
73
|
-
}, [tenantSlug, client, freshInstrument]);
|
|
74
|
-
|
|
75
|
-
return { savedCards, selection, setSelection };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The store's card public key for this order. The checkout config resolved the
|
|
80
|
-
* initial key server-side (`GET /api/checkout/config`, FUT-697); the SPA
|
|
81
|
-
* fetches through the order-scoped refresh route only when the ACTIVE provider
|
|
82
|
-
* is PagBank — the sole provider that can mint a key on demand — and the
|
|
83
|
-
* config arrived without one. A `null` key never means "mock": stub permission
|
|
84
|
-
* travels separately as `mockTokenization`.
|
|
85
|
-
*/
|
|
86
|
-
function useCardPublicKey(
|
|
87
|
-
orderId: string,
|
|
88
|
-
config: CardTokenizationConfig,
|
|
89
|
-
): {
|
|
90
|
-
publicKey: string | null;
|
|
91
|
-
setPublicKey: (key: string) => void;
|
|
92
|
-
} {
|
|
93
|
-
const [publicKey, setPublicKey] = useState<string | null>(config.publicKey);
|
|
94
|
-
const client = useCheckoutClientApi();
|
|
95
|
-
const refreshable =
|
|
96
|
-
config.provider !== null && tokenizerFor(config.provider) === "pagbank-sdk";
|
|
97
|
-
|
|
98
|
-
useEffect(() => {
|
|
99
|
-
if (config.publicKey !== null || !refreshable) return undefined;
|
|
100
|
-
let active = true;
|
|
101
|
-
void client.refreshBrowserKey({ orderId }).then((result) => {
|
|
102
|
-
if (active && result.ok && result.data.publicKey) setPublicKey(result.data.publicKey);
|
|
103
|
-
});
|
|
104
|
-
return () => {
|
|
105
|
-
active = false;
|
|
106
|
-
};
|
|
107
|
-
}, [orderId, config.publicKey, refreshable, client]);
|
|
108
|
-
|
|
109
|
-
return { publicKey, setPublicKey };
|
|
110
|
-
}
|
|
111
37
|
|
|
112
38
|
/** Everything the card view renders — all checkout state + the submit handler. */
|
|
113
39
|
export interface CardCheckout {
|
|
@@ -137,6 +63,12 @@ export interface CardCheckout {
|
|
|
137
63
|
pollTimedOut: boolean;
|
|
138
64
|
/** Ask now and restart the wait — the buyer's "verificar de novo". */
|
|
139
65
|
pollCheckAgain: () => void;
|
|
66
|
+
/**
|
|
67
|
+
* The order-scoped key refresh has not answered yet (`./card-sources.ts`): a
|
|
68
|
+
* charge started now would tokenize a new card with no key. A screen that
|
|
69
|
+
* starts the charge itself waits for this to clear.
|
|
70
|
+
*/
|
|
71
|
+
keyPending: boolean;
|
|
140
72
|
handlePay: () => Promise<void>;
|
|
141
73
|
}
|
|
142
74
|
|
|
@@ -160,6 +92,7 @@ type CardSubmit = Pick<
|
|
|
160
92
|
| "pollError"
|
|
161
93
|
| "pollTimedOut"
|
|
162
94
|
| "pollCheckAgain"
|
|
95
|
+
| "keyPending"
|
|
163
96
|
| "handlePay"
|
|
164
97
|
>;
|
|
165
98
|
|
|
@@ -246,7 +179,7 @@ function useCardWait(
|
|
|
246
179
|
* poll for the async confirmation, bubbling the terminal status up via
|
|
247
180
|
* {@link onResolved}.
|
|
248
181
|
*/
|
|
249
|
-
function useCardSubmit(
|
|
182
|
+
export function useCardSubmit(
|
|
250
183
|
order: CheckoutOrder,
|
|
251
184
|
buyer: BuyerInfo,
|
|
252
185
|
providerConfig: CardTokenizationConfig,
|
|
@@ -264,7 +197,7 @@ function useCardSubmit(
|
|
|
264
197
|
const [errorCode, setErrorCode] = useState<string | null>(null);
|
|
265
198
|
// The active key: resolved by the checkout config, overridden by the
|
|
266
199
|
// rotated-key self-heal for the rest of the session.
|
|
267
|
-
const { publicKey, setPublicKey } = useCardPublicKey(order.orderId, providerConfig);
|
|
200
|
+
const { publicKey, setPublicKey, keyPending } = useCardPublicKey(order.orderId, providerConfig);
|
|
268
201
|
const client = useCheckoutClientApi();
|
|
269
202
|
const navigate = useCheckoutNavigate();
|
|
270
203
|
const cardCopy = useCheckoutCopy().card;
|
|
@@ -323,7 +256,7 @@ function useCardSubmit(
|
|
|
323
256
|
else setSubmitted(true);
|
|
324
257
|
};
|
|
325
258
|
|
|
326
|
-
return { submitting, submitted, error, errorCode, ...wait, handlePay };
|
|
259
|
+
return { submitting, submitted, error, errorCode, ...wait, keyPending, handlePay };
|
|
327
260
|
}
|
|
328
261
|
|
|
329
262
|
/**
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
resumeSurface,
|
|
6
6
|
useCheckoutNav,
|
|
7
7
|
useCreateFailure,
|
|
8
|
+
useDropOrder,
|
|
8
9
|
useGoToPayment,
|
|
9
10
|
useResumedCheckout,
|
|
10
11
|
useRetryAction,
|
|
@@ -135,6 +136,7 @@ export function useCheckoutController(
|
|
|
135
136
|
return next;
|
|
136
137
|
});
|
|
137
138
|
}, [clearError, tenantSlug]);
|
|
139
|
+
const dropOrder = useDropOrder(setOrder, tenantSlug, clearError);
|
|
138
140
|
const goToPayment = useGoToPayment({
|
|
139
141
|
buyer, buyerFields, taxIdOnFile, saveProfile, saveBuyerContact, validation, failure, setStep,
|
|
140
142
|
});
|
|
@@ -170,6 +172,6 @@ export function useCheckoutController(
|
|
|
170
172
|
...resumeSurface(resume, confirming),
|
|
171
173
|
createError: failure.message, errorField: failure.field, errorCode: failure.code,
|
|
172
174
|
goToMenu: onExitToMenu, back, editBuyer,
|
|
173
|
-
goToPayment, startPayment, refresh, payWithEmail, handleResolved, retry, completed,
|
|
175
|
+
goToPayment, startPayment, refresh, payWithEmail, handleResolved, retry, completed, dropOrder,
|
|
174
176
|
};
|
|
175
177
|
}
|
|
@@ -68,6 +68,11 @@ function buildRuntime(config: PaymentFlowsConfig): FlowsRuntime {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The host's one-list Pagamento (FUT-3591), when it opted in — a hook, read every render. */
|
|
72
|
+
function useHostPaymentList(runtime: FlowsRuntime) {
|
|
73
|
+
return runtime.config.usePaymentList?.();
|
|
74
|
+
}
|
|
75
|
+
|
|
71
76
|
/** The whole three-step flow, with the config fetched and availability decided. */
|
|
72
77
|
function buildCheckout(
|
|
73
78
|
runtime: FlowsRuntime,
|
|
@@ -80,6 +85,7 @@ function buildCheckout(
|
|
|
80
85
|
const cart = runtime.config.useCart();
|
|
81
86
|
const defaults = runtime.config.useBuyerDefaults?.() ?? {};
|
|
82
87
|
const hostSettlement = runtime.config.useSettlement?.() ?? null;
|
|
88
|
+
const paymentList = useHostPaymentList(runtime);
|
|
83
89
|
const { config, pending } = useResolvedConfig(runtime);
|
|
84
90
|
const availability = runtime.useAvailability();
|
|
85
91
|
const tenantSlug = runtime.useTenantSlug();
|
|
@@ -108,6 +114,7 @@ function buildCheckout(
|
|
|
108
114
|
confirmationExtra={runtime.config.confirmation?.extra}
|
|
109
115
|
confirmationHero={runtime.config.confirmation?.hero}
|
|
110
116
|
validateApplePayMerchant={ports.validateApplePayMerchant}
|
|
117
|
+
paymentList={paymentList}
|
|
111
118
|
/>
|
|
112
119
|
);
|
|
113
120
|
}
|