@12-apps/payments-frontend 3.43.1 → 3.45.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 +2 -2
- package/src/components/checkout/apple-pay-button.tsx +84 -36
- package/src/components/checkout/checkout-flow.tsx +3 -2
- package/src/components/checkout/google-pay-button.tsx +6 -0
- package/src/components/checkout/list/buyer-inline.tsx +22 -14
- package/src/components/checkout/list/charge-runners.tsx +47 -23
- package/src/components/checkout/list/choice-list.tsx +25 -7
- package/src/components/checkout/list/choice.ts +4 -6
- package/src/components/checkout/list/cpf-top.tsx +98 -0
- package/src/components/checkout/list/en-US.ts +8 -0
- package/src/components/checkout/list/list-bar.tsx +29 -44
- package/src/components/checkout/list/list-face.tsx +110 -104
- package/src/components/checkout/list/list-top.tsx +99 -0
- package/src/components/checkout/list/online-content.tsx +4 -0
- package/src/components/checkout/list/option-row.tsx +55 -32
- package/src/components/checkout/list/payer-line.tsx +39 -0
- package/src/components/checkout/list/payment-list-step.tsx +50 -5
- package/src/components/checkout/list/pt-BR.ts +8 -0
- package/src/components/checkout/list/summary-column.tsx +120 -0
- package/src/components/checkout/list/top-cpf.ts +94 -0
- package/src/components/checkout/list/types.ts +89 -13
- package/src/components/checkout/list/use-capability.ts +113 -35
- package/src/components/checkout/list/use-list-charge.ts +11 -9
- package/src/components/checkout/list/use-list-state.ts +37 -4
- package/src/components/checkout/list/wallet-block.tsx +235 -0
- package/src/components/checkout/list/wallet-press.ts +129 -0
- package/src/components/checkout/payment-phase.tsx +1 -0
- package/src/components/checkout/payment-summary.tsx +2 -2
- 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 +2 -0
- package/src/flows/create-payment-flows.tsx +11 -2
- package/src/flows/types.ts +7 -2
- package/src/index.ts +1 -1
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { Box, Divider } from "@mui/material";
|
|
2
|
+
import { useTheme, type Theme } from "@mui/material/styles";
|
|
3
|
+
import { useEffect, useId, useRef, type JSX } from "react";
|
|
4
|
+
|
|
5
|
+
import { ChargeFailure } from "../card-view";
|
|
6
|
+
import { useCheckoutCopy } from "../copy-context";
|
|
7
|
+
import { UNRESOLVED_CODE } from "../failure-codes";
|
|
8
|
+
import type { GooglePaymentsClient } from "../google-pay-button";
|
|
9
|
+
import { useCheckoutComponents } from "../ui";
|
|
10
|
+
|
|
11
|
+
import type { ListChargeFailure } from "./charge-runners";
|
|
12
|
+
import { GroupHeading } from "./option-row";
|
|
13
|
+
import type { PaymentListCopy } from "./types";
|
|
14
|
+
import type { ListCapability, ListWallet } from "./use-capability";
|
|
15
|
+
|
|
16
|
+
/** The prototype's 44px wallet button (L3), on the theme's spacing scale. */
|
|
17
|
+
const WALLET_HEIGHT = 5.5;
|
|
18
|
+
|
|
19
|
+
/** Why the buttons are held, as each button carries it: disabled, and described by the reason. */
|
|
20
|
+
interface Held {
|
|
21
|
+
/** The reason's element id, or `null` when nothing holds them. */
|
|
22
|
+
describedBy: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The pill's edge: none on a light page (the black pill is its own edge); a
|
|
27
|
+
* hairline at night, where black on a dark surface has none; and a visible
|
|
28
|
+
* one while held, when the pill is dimmed (FUT-3685 review).
|
|
29
|
+
*/
|
|
30
|
+
const pillEdge = (held: boolean) => (t: Theme): string => {
|
|
31
|
+
if (held) return `1px solid ${t.palette.text.secondary}`;
|
|
32
|
+
return t.palette.mode === "dark" ? `1px solid ${t.palette.divider}` : "1px solid transparent";
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The look every wallet pill shares — the prototype's 44px black pill (L3).
|
|
37
|
+
* The edge is an OUTLINE drawn just inside the pill: it paints over Google's
|
|
38
|
+
* own element and moves nothing, so both pills keep one size and their labels
|
|
39
|
+
* one baseline.
|
|
40
|
+
*/
|
|
41
|
+
const pill = (held: boolean) => ({
|
|
42
|
+
height: (t: Theme) => t.spacing(WALLET_HEIGHT),
|
|
43
|
+
width: "100%",
|
|
44
|
+
border: 0,
|
|
45
|
+
outline: pillEdge(held),
|
|
46
|
+
outlineOffset: "-1px",
|
|
47
|
+
borderRadius: (t: Theme) => t.spacing(WALLET_HEIGHT / 2),
|
|
48
|
+
overflow: "hidden",
|
|
49
|
+
} as const);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Google Pay's own button (FUT-471's brand rule: the buyer presses Google's
|
|
53
|
+
* button, drawn by `createButton`, never a look-alike) — black, "pay", a pill.
|
|
54
|
+
* Its press opens the sheet SYNCHRONOUSLY, inside the tap, through `onPress`.
|
|
55
|
+
* Held, Google's element is marked disabled and described by the reason —
|
|
56
|
+
* it stays in the accessibility tree — and a press does nothing.
|
|
57
|
+
*/
|
|
58
|
+
function GoogleWalletButton({ client, onPress, held }: { client: GooglePaymentsClient; onPress: () => void; held: Held }): JSX.Element {
|
|
59
|
+
const mount = useRef<HTMLDivElement | null>(null);
|
|
60
|
+
const latest = useRef(onPress);
|
|
61
|
+
latest.current = onPress;
|
|
62
|
+
const locale = useCheckoutCopy().screens.wallet.googlePay.buttonLocale;
|
|
63
|
+
const theme = useTheme();
|
|
64
|
+
// Google's own parameter, in CSS pixels: half the button's height is a pill.
|
|
65
|
+
const radius = Number.parseFloat(theme.spacing(WALLET_HEIGHT / 2));
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const element = mount.current;
|
|
68
|
+
if (!element) return undefined;
|
|
69
|
+
element.replaceChildren(
|
|
70
|
+
client.createButton({
|
|
71
|
+
onClick: () => latest.current(),
|
|
72
|
+
buttonSizeMode: "fill",
|
|
73
|
+
buttonLocale: locale,
|
|
74
|
+
buttonColor: "black",
|
|
75
|
+
buttonType: "pay",
|
|
76
|
+
buttonRadius: radius,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
return () => element.replaceChildren();
|
|
80
|
+
}, [client, locale, radius]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const button = mount.current?.querySelector("button");
|
|
83
|
+
if (!button) return;
|
|
84
|
+
if (held.describedBy) {
|
|
85
|
+
button.setAttribute("aria-disabled", "true");
|
|
86
|
+
button.setAttribute("aria-describedby", held.describedBy);
|
|
87
|
+
} else {
|
|
88
|
+
button.removeAttribute("aria-disabled");
|
|
89
|
+
button.removeAttribute("aria-describedby");
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
const isHeld = held.describedBy !== null;
|
|
93
|
+
return (
|
|
94
|
+
<Box
|
|
95
|
+
ref={mount}
|
|
96
|
+
data-testid="list-google-pay"
|
|
97
|
+
data-held={isHeld ? "true" : undefined}
|
|
98
|
+
sx={{ ...pill(isHeld), pointerEvents: isHeld ? "none" : undefined, "& > *": { width: "100%", height: "100%" } }}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Apple's button: the `-apple-pay-button` appearance Safari draws in the
|
|
105
|
+
* buyer's own language (Apple's Human Interface Guidelines), black, "pay".
|
|
106
|
+
* Only a browser that has an `ApplePaySession` ever renders it; one that does
|
|
107
|
+
* not know the appearance (a test browser with a stubbed session) gets the
|
|
108
|
+
* accessible name drawn as text on the same black pill instead.
|
|
109
|
+
*/
|
|
110
|
+
function AppleWalletButton({ label, onPress, held }: { label: string; onPress: () => void; held: Held }): JSX.Element {
|
|
111
|
+
const isHeld = held.describedBy !== null;
|
|
112
|
+
return (
|
|
113
|
+
<Box
|
|
114
|
+
component="button"
|
|
115
|
+
type="button"
|
|
116
|
+
aria-label={label}
|
|
117
|
+
disabled={isHeld}
|
|
118
|
+
aria-describedby={held.describedBy ?? undefined}
|
|
119
|
+
onClick={onPress}
|
|
120
|
+
data-testid="list-apple-pay"
|
|
121
|
+
sx={{
|
|
122
|
+
WebkitAppearance: "-apple-pay-button",
|
|
123
|
+
"-apple-pay-button-type": "pay",
|
|
124
|
+
"-apple-pay-button-style": "black",
|
|
125
|
+
...pill(isHeld),
|
|
126
|
+
cursor: isHeld ? "default" : "pointer",
|
|
127
|
+
bgcolor: "common.black",
|
|
128
|
+
color: "common.white",
|
|
129
|
+
typography: "body1",
|
|
130
|
+
fontWeight: 600,
|
|
131
|
+
"&:focus-visible": { outline: (t) => `${t.spacing(0.25)} solid ${t.palette.primary.main}`, outlineOffset: (t) => t.spacing(0.25) },
|
|
132
|
+
"@supports (-webkit-appearance: -apple-pay-button)": { "& > span": { display: "none" } },
|
|
133
|
+
}}
|
|
134
|
+
>
|
|
135
|
+
<span aria-hidden>{label}</span>
|
|
136
|
+
</Box>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** One wallet's button, or a held pill while the host re-prices it. */
|
|
141
|
+
function WalletButton({ wallet, cap, copy, onGoogle, onApple, held }: {
|
|
142
|
+
wallet: ListWallet;
|
|
143
|
+
cap: ListCapability;
|
|
144
|
+
copy: PaymentListCopy;
|
|
145
|
+
onGoogle: () => void;
|
|
146
|
+
onApple: () => void;
|
|
147
|
+
held: Held;
|
|
148
|
+
}): JSX.Element {
|
|
149
|
+
if (wallet === "GOOGLE_PAY" && cap.googlePay) return <GoogleWalletButton client={cap.googlePay.client} onPress={onGoogle} held={held} />;
|
|
150
|
+
if (wallet === "APPLE_PAY") return <AppleWalletButton label={copy.wallets.applePay} onPress={onApple} held={held} />;
|
|
151
|
+
// Google while the host re-prices: no sheet can open, so no Google button either — a plain held pill.
|
|
152
|
+
return <HeldWalletButton label={copy.wallets.googlePay} held={held} />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A held wallet while the host re-prices: the pill, disabled, named, described by the reason. */
|
|
156
|
+
function HeldWalletButton({ label, held }: { label: string; held: Held }): JSX.Element {
|
|
157
|
+
return (
|
|
158
|
+
<Box
|
|
159
|
+
component="button"
|
|
160
|
+
type="button"
|
|
161
|
+
disabled
|
|
162
|
+
aria-describedby={held.describedBy ?? undefined}
|
|
163
|
+
data-testid="list-wallet-held"
|
|
164
|
+
sx={{ ...pill(true), bgcolor: "common.black", color: "common.white", typography: "body1", fontWeight: 600 }}
|
|
165
|
+
>
|
|
166
|
+
{label}
|
|
167
|
+
</Box>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* "PAGAMENTO RÁPIDO" (FUT-3685, C5, C6, E4, E5, L3, L5): the ready wallets'
|
|
173
|
+
* buttons above the list, then "ou escolha como pagar". Nothing at all — no
|
|
174
|
+
* heading, no buttons, no divider — when no wallet is ready (E4); one wallet
|
|
175
|
+
* takes the full width (E5); two sit side by side from the theme's `sm`
|
|
176
|
+
* (L5) and stack below it (L3).
|
|
177
|
+
*
|
|
178
|
+
* The buttons are not part of the list's radio group (R1): pressing one opens
|
|
179
|
+
* its sheet and never changes the chosen row. `held` is why they cannot act
|
|
180
|
+
* yet — the CPF the provider needs (E3), the host re-pricing — said under them.
|
|
181
|
+
*/
|
|
182
|
+
export function WalletBlock({ cap, copy, held, failure, onGoogle, onApple }: {
|
|
183
|
+
cap: ListCapability;
|
|
184
|
+
copy: PaymentListCopy;
|
|
185
|
+
held: string | null;
|
|
186
|
+
/** The last wallet charge's refusal; an UNRESOLVED one removes the buttons (FUT-563). */
|
|
187
|
+
failure: ListChargeFailure | null;
|
|
188
|
+
onGoogle: () => void;
|
|
189
|
+
onApple: () => void;
|
|
190
|
+
}): JSX.Element | null {
|
|
191
|
+
const { Text } = useCheckoutComponents();
|
|
192
|
+
const id = useId();
|
|
193
|
+
const reasonId = `${id}-held`;
|
|
194
|
+
if (cap.wallets.length === 0) return null;
|
|
195
|
+
const two = cap.wallets.length > 1;
|
|
196
|
+
const unresolved = failure?.code === UNRESOLVED_CODE;
|
|
197
|
+
return (
|
|
198
|
+
<Box data-testid="list-wallets" sx={{ display: "flex", flexDirection: "column", gap: 1.25 }}>
|
|
199
|
+
<Box role="group" aria-labelledby={id} sx={{ display: "flex", flexDirection: "column", gap: 1.25 }}>
|
|
200
|
+
<GroupHeading id={id}>{copy.wallets.heading}</GroupHeading>
|
|
201
|
+
{unresolved ? null : (
|
|
202
|
+
<Box
|
|
203
|
+
data-testid="list-wallet-buttons"
|
|
204
|
+
data-held={held ? "true" : undefined}
|
|
205
|
+
sx={{
|
|
206
|
+
display: "grid",
|
|
207
|
+
gap: 1.25,
|
|
208
|
+
gridTemplateColumns: { xs: "minmax(0, 1fr)", sm: two ? "repeat(2, minmax(0, 1fr))" : "minmax(0, 1fr)" },
|
|
209
|
+
opacity: (t) => (held ? t.palette.action.disabledOpacity : 1),
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{cap.wallets.map((wallet) => (
|
|
213
|
+
<WalletButton key={wallet} wallet={wallet} cap={cap} copy={copy} onGoogle={onGoogle} onApple={onApple} held={{ describedBy: held ? reasonId : null }} />
|
|
214
|
+
))}
|
|
215
|
+
</Box>
|
|
216
|
+
)}
|
|
217
|
+
{held ? (
|
|
218
|
+
// The Text slot takes no id; the reason's own box carries the one the buttons point at.
|
|
219
|
+
<Box id={reasonId} data-testid="list-wallets-held">
|
|
220
|
+
<Text variant="body" size="sm" color="secondary" as="p" style={{ margin: 0 }}>
|
|
221
|
+
{held}
|
|
222
|
+
</Text>
|
|
223
|
+
</Box>
|
|
224
|
+
) : null}
|
|
225
|
+
{failure ? <ChargeFailure message={failure.message} unresolved={unresolved} /> : null}
|
|
226
|
+
</Box>
|
|
227
|
+
<Divider data-testid="list-wallets-or">
|
|
228
|
+
{/* The body style the hints use: a caption tone fell under 4.5:1 on the surface. */}
|
|
229
|
+
<Text variant="body" size="xs" color="secondary" as="span">
|
|
230
|
+
{copy.wallets.or}
|
|
231
|
+
</Text>
|
|
232
|
+
</Divider>
|
|
233
|
+
</Box>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A wallet button's press (FUT-3685, C5, C6): the wallet's sheet opens HERE,
|
|
3
|
+
* synchronously, inside the buyer's tap — the browser opens Google's sheet and
|
|
4
|
+
* Safari starts an Apple Pay session only from inside one — priced at the
|
|
5
|
+
* host's amount DUE (`totalCents`, K3). The order is raised in parallel by the
|
|
6
|
+
* list's charge, and the runner charges the sheet's key only against an order
|
|
7
|
+
* that asks the provider for the same amount (R4).
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, type MutableRefObject } from "react";
|
|
10
|
+
|
|
11
|
+
import { startApplePaySession } from "../apple-pay-button";
|
|
12
|
+
import { useCheckoutCopy } from "../copy-context";
|
|
13
|
+
import { paymentDataRequest, sheetDismissed } from "../google-pay-button";
|
|
14
|
+
|
|
15
|
+
import type { ListChargeFailure, WalletSheet } from "./charge-runners";
|
|
16
|
+
import type { ListCapability } from "./use-capability";
|
|
17
|
+
import type { ListCharge } from "./use-list-charge";
|
|
18
|
+
|
|
19
|
+
/** What a press needs from the list face. */
|
|
20
|
+
export interface WalletPressInput {
|
|
21
|
+
cap: ListCapability;
|
|
22
|
+
charge: ListCharge;
|
|
23
|
+
/** The host's amount due, in cents — what the sheet shows. */
|
|
24
|
+
cents: number | undefined;
|
|
25
|
+
/** The amount the bar names for an online payment, frozen at the press. */
|
|
26
|
+
total: string;
|
|
27
|
+
/** Something stops a press: a reason the block shows, or a charge already under way. */
|
|
28
|
+
blocked: boolean;
|
|
29
|
+
/** Before the sheet opens: clear the last refusal, keep the asked details. */
|
|
30
|
+
before: () => void;
|
|
31
|
+
/** A sheet that could not open, in the wallet's own words. */
|
|
32
|
+
onFailed: (failure: ListChargeFailure) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Set synchronously by a press, cleared when the charge is back to idle: a
|
|
35
|
+
* second tap before React re-renders (a double tap) opens no second sheet.
|
|
36
|
+
*/
|
|
37
|
+
pressed: MutableRefObject<boolean>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Open a wallet's sheet, synchronously. A sheet that throws instead — Safari's
|
|
42
|
+
* `InvalidAccessError` for a session it will not begin, a pay.js that throws
|
|
43
|
+
* outright — starts no charge, so nothing would ever clear the press guard:
|
|
44
|
+
* both buttons would be dead (FUT-3685 review). So the guard is released, and
|
|
45
|
+
* the buyer is told the wallet could not start; the next press tries again.
|
|
46
|
+
*/
|
|
47
|
+
function openSheet<T>(input: WalletPressInput, open: () => T, cannotStart: string): T | null {
|
|
48
|
+
try {
|
|
49
|
+
return open();
|
|
50
|
+
} catch {
|
|
51
|
+
input.pressed.current = false;
|
|
52
|
+
input.onFailed({ message: cannotStart, code: null });
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Google Pay's press: `loadPaymentData` inside the tap, then the charge. */
|
|
58
|
+
export function useGooglePayPress(input: WalletPressInput, sheet: MutableRefObject<WalletSheet | null>): () => void {
|
|
59
|
+
const cannotStart = useCheckoutCopy().screens.wallet.googlePay.cannotComplete;
|
|
60
|
+
return () => {
|
|
61
|
+
const google = input.cap.googlePay;
|
|
62
|
+
const cents = input.cents;
|
|
63
|
+
if (!google || cents === undefined || input.blocked || input.pressed.current) return;
|
|
64
|
+
input.pressed.current = true;
|
|
65
|
+
input.before();
|
|
66
|
+
const loading = openSheet(input, () => google.client.loadPaymentData(paymentDataRequest(google.params, { totalCents: cents })), cannotStart);
|
|
67
|
+
if (!loading) return;
|
|
68
|
+
// A sheet that rejects later reaches the runner, which says so and settles the charge.
|
|
69
|
+
const answer = loading.then(
|
|
70
|
+
(data) => data.paymentMethodData.tokenizationData.token,
|
|
71
|
+
(error: unknown) => {
|
|
72
|
+
if (sheetDismissed(error)) return null;
|
|
73
|
+
throw error;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
sheet.current = { type: "GOOGLE_PAY", answer, cents };
|
|
77
|
+
input.charge.start("GOOGLE_PAY", input.total);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Apple Pay's press: the session is CREATED and BEGUN synchronously, in the
|
|
83
|
+
* tap — Safari's rule — and the order is raised while the buyer looks at the
|
|
84
|
+
* sheet. A merchant validation that cannot run says why, beside the list.
|
|
85
|
+
*/
|
|
86
|
+
export function useApplePayPress(input: WalletPressInput, sheet: MutableRefObject<WalletSheet | null>): () => void {
|
|
87
|
+
const copy = useCheckoutCopy().screens.wallet.applePay;
|
|
88
|
+
return () => {
|
|
89
|
+
const apple = input.cap.applePay;
|
|
90
|
+
const cents = input.cents;
|
|
91
|
+
if (!apple || cents === undefined || input.blocked || input.pressed.current) return;
|
|
92
|
+
input.pressed.current = true;
|
|
93
|
+
input.before();
|
|
94
|
+
const started = openSheet(
|
|
95
|
+
input,
|
|
96
|
+
() =>
|
|
97
|
+
startApplePaySession({
|
|
98
|
+
Session: apple.Session,
|
|
99
|
+
cents,
|
|
100
|
+
copy,
|
|
101
|
+
validateMerchant: apple.validateMerchant,
|
|
102
|
+
onError: (message) => input.onFailed({ message, code: null }),
|
|
103
|
+
}),
|
|
104
|
+
copy.cannotComplete,
|
|
105
|
+
);
|
|
106
|
+
if (!started) return;
|
|
107
|
+
sheet.current = { type: "APPLE_PAY", answer: started.answer, cents, complete: started.complete };
|
|
108
|
+
input.charge.start("APPLE_PAY", input.total);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* A sheet left open with nobody to charge it: the raise behind it was refused,
|
|
114
|
+
* and the list is back to its rows. Apple's sheet waits for an outcome, so it
|
|
115
|
+
* is told this one failed; nothing is charged.
|
|
116
|
+
*/
|
|
117
|
+
export function useCloseOrphanSheet(phase: ListCharge["phase"], sheet: MutableRefObject<WalletSheet | null>, pressed: MutableRefObject<boolean>): void {
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (phase !== "idle") return;
|
|
120
|
+
// Back to idle: a new press may open a sheet again.
|
|
121
|
+
pressed.current = false;
|
|
122
|
+
const open = sheet.current;
|
|
123
|
+
sheet.current = null;
|
|
124
|
+
if (!open || open.claimed) return;
|
|
125
|
+
void open.answer.then((key) => {
|
|
126
|
+
if (key !== null) open.complete?.(false);
|
|
127
|
+
}, () => undefined);
|
|
128
|
+
}, [phase, sheet, pressed]);
|
|
129
|
+
}
|
|
@@ -212,7 +212,7 @@ function AddressRow({
|
|
|
212
212
|
const DIVIDER = <Box sx={{ height: "1px", bgcolor: "divider" }} />;
|
|
213
213
|
|
|
214
214
|
/** The cart's lines, "2× X-Burger … R$ 36,00", when the host passed them. */
|
|
215
|
-
function SummaryLines({ lines }: { lines: CheckoutSummaryLine[] }): JSX.Element {
|
|
215
|
+
export function SummaryLines({ lines }: { lines: CheckoutSummaryLine[] }): JSX.Element {
|
|
216
216
|
const { Text } = useCheckoutComponents();
|
|
217
217
|
const copy = useCheckoutCopy().screens.layout;
|
|
218
218
|
return (
|
|
@@ -232,7 +232,7 @@ function SummaryLines({ lines }: { lines: CheckoutSummaryLine[] }): JSX.Element
|
|
|
232
232
|
}
|
|
233
233
|
|
|
234
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 {
|
|
235
|
+
export function SummaryTotal({ totalLabel, discountLines }: { totalLabel: string; discountLines?: ReactNode }): JSX.Element {
|
|
236
236
|
const { Text } = useCheckoutComponents();
|
|
237
237
|
const copy = useCheckoutCopy().screens.layout;
|
|
238
238
|
return (
|
|
@@ -234,6 +234,12 @@ export interface WalletCopy {
|
|
|
234
234
|
cannotStart: string;
|
|
235
235
|
/** It started and failed. */
|
|
236
236
|
cannotComplete: string;
|
|
237
|
+
/**
|
|
238
|
+
* The sheet was authorized and the payment did not go through — the
|
|
239
|
+
* amount moved, or the charge was refused (FUT-3685). Absent ⇒
|
|
240
|
+
* `cannotComplete`. Both packs carry it.
|
|
241
|
+
*/
|
|
242
|
+
paymentFailed?: string;
|
|
237
243
|
payAction: string;
|
|
238
244
|
};
|
|
239
245
|
googlePay: {
|
|
@@ -124,6 +124,7 @@ export const EN_US_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
124
124
|
orderTotal: 'Order total',
|
|
125
125
|
cannotStart: 'Could not start Apple Pay in this store. Pay by card instead.',
|
|
126
126
|
cannotComplete: 'Could not start Apple Pay. Try again, or pay by card.',
|
|
127
|
+
paymentFailed: 'Could not complete the payment with Apple Pay. Try again, or pay by card.',
|
|
127
128
|
payAction: 'Pay with Apple Pay',
|
|
128
129
|
},
|
|
129
130
|
googlePay: {
|
|
@@ -113,6 +113,8 @@ export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
113
113
|
cannotStart: 'Não foi possível iniciar o Apple Pay nesta loja. Pague com cartão.',
|
|
114
114
|
cannotComplete:
|
|
115
115
|
'Não foi possível iniciar o Apple Pay. Tente novamente ou pague com cartão.',
|
|
116
|
+
paymentFailed:
|
|
117
|
+
'Não foi possível concluir o pagamento com o Apple Pay. Tente novamente ou pague com cartão.',
|
|
116
118
|
payAction: 'Pagar com Apple Pay',
|
|
117
119
|
},
|
|
118
120
|
googlePay: {
|
|
@@ -68,6 +68,16 @@ function buildRuntime(config: PaymentFlowsConfig): FlowsRuntime {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/** The host's confirmation slots, as `CheckoutFlow` takes them. */
|
|
72
|
+
function confirmationProps(confirmation: PaymentFlowsConfig["confirmation"]) {
|
|
73
|
+
return {
|
|
74
|
+
confirmationExtra: confirmation?.extra,
|
|
75
|
+
confirmationHero: confirmation?.hero,
|
|
76
|
+
confirmationFooter: confirmation?.footer,
|
|
77
|
+
backActionEmphasis: confirmation?.backActionEmphasis,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
/** The host's one-list Pagamento (FUT-3591), when it opted in — a hook, read every render. */
|
|
72
82
|
function useHostPaymentList(runtime: FlowsRuntime) {
|
|
73
83
|
return runtime.config.usePaymentList?.();
|
|
@@ -111,8 +121,7 @@ function buildCheckout(
|
|
|
111
121
|
settlement={settlement ?? hostSettlement}
|
|
112
122
|
providerConfig={config}
|
|
113
123
|
tenantSlug={tenantSlug}
|
|
114
|
-
|
|
115
|
-
confirmationHero={runtime.config.confirmation?.hero}
|
|
124
|
+
{...confirmationProps(runtime.config.confirmation)}
|
|
116
125
|
validateApplePayMerchant={ports.validateApplePayMerchant}
|
|
117
126
|
paymentList={paymentList}
|
|
118
127
|
/>
|
package/src/flows/types.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* a `tokensByProvider` key. A host never types one.
|
|
21
21
|
*/
|
|
22
22
|
import type { CheckoutPaymentList } from "../components/checkout/list/types";
|
|
23
|
+
import type { BackActionEmphasis } from "../components/checkout/payment-status-parts";
|
|
23
24
|
import type { ComponentType, ReactNode } from "react";
|
|
24
25
|
|
|
25
26
|
import type { SavedCard } from "../card";
|
|
@@ -153,9 +154,13 @@ export interface PaymentFlowsConfig extends CheckoutPipelineConfig {
|
|
|
153
154
|
copy: CheckoutCopyFE;
|
|
154
155
|
/**
|
|
155
156
|
* Host content under the paid receipt (the storefront's PWA install invite),
|
|
156
|
-
*
|
|
157
|
+
* the host's own illustration in place of the paid icon (`hero`), content
|
|
158
|
+
* AFTER the confirmation's actions (`footer`), and whether the way out leads
|
|
159
|
+
* the paid screen (`backActionEmphasis`) — `CheckoutFlowProps`'
|
|
160
|
+
* `confirmationExtra`/`Hero`/`Footer` and `backActionEmphasis`, so a host on
|
|
161
|
+
* the factory composes the same confirmation as one mounting the flow.
|
|
157
162
|
*/
|
|
158
|
-
confirmation?: { extra?: ReactNode; hero?: ReactNode };
|
|
163
|
+
confirmation?: { extra?: ReactNode; hero?: ReactNode; footer?: ReactNode; backActionEmphasis?: BackActionEmphasis };
|
|
159
164
|
/**
|
|
160
165
|
* Warnings the host reports (Sentry). Default SILENT — never `console`: a
|
|
161
166
|
* library that writes to a buyer's console tells them nothing and tells the
|
package/src/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export { PT_BR_CHECKOUT_PAYMENT_COPY } from './components/checkout-payment-pt-BR
|
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
57
|
export { CheckoutFlow, type CheckoutCartView, type CheckoutFlowProps } from './components/checkout/checkout-flow';
|
|
58
58
|
// The Pagamento step as ONE list (FUT-3591/3592) — opt-in via `CheckoutFlowProps.paymentList`.
|
|
59
|
-
export type { CheckoutDoorOption, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, CheckoutPixBank, PaymentListCopy, PixBankCopy, PixBankId, PixBankName } from './components/checkout/list/types';
|
|
59
|
+
export type { CheckoutDoorOption, CheckoutDoorRowControls, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, CheckoutPixBank, PaymentListCopy, PixBankCopy, PixBankId, PixBankName } from './components/checkout/list/types';
|
|
60
60
|
export { PT_BR_PAYMENT_LIST_COPY } from './components/checkout/list/pt-BR';
|
|
61
61
|
export type { BackActionEmphasis } from './components/checkout/payment-status-parts';
|
|
62
62
|
export type { CheckoutDeliverySummary, CheckoutSummaryLine } from './components/checkout/payment-summary';
|