@12-apps/payments-frontend 3.37.0 → 3.39.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/types.ts +6 -0
- package/src/components/checkout/card-sources.ts +140 -0
- package/src/components/checkout/card-view.tsx +6 -2
- package/src/components/checkout/cart-changed.tsx +26 -15
- 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 +175 -0
- package/src/components/checkout/list/choice-list.tsx +159 -0
- package/src/components/checkout/list/choice.ts +213 -0
- package/src/components/checkout/list/context.tsx +55 -0
- package/src/components/checkout/list/en-US.ts +63 -0
- package/src/components/checkout/list/list-bar.tsx +186 -0
- package/src/components/checkout/list/list-face.tsx +338 -0
- package/src/components/checkout/list/list-pix.tsx +206 -0
- package/src/components/checkout/list/online-content.tsx +144 -0
- package/src/components/checkout/list/option-row.tsx +125 -0
- package/src/components/checkout/list/payment-list-step.tsx +166 -0
- package/src/components/checkout/list/pt-BR.ts +79 -0
- package/src/components/checkout/list/types.ts +218 -0
- package/src/components/checkout/list/use-capability.ts +58 -0
- package/src/components/checkout/list/use-list-charge.ts +186 -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-parts.tsx +3 -0
- package/src/components/checkout/pix-view.tsx +53 -20
- package/src/components/checkout/transport.ts +3 -1
- package/src/components/checkout/types.ts +9 -9
- 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,152 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
|
|
3
|
+
import { NEW_CARD, type CardDetails, type CardFieldErrors, type SavedCard } from "../../../card";
|
|
4
|
+
import { useSavedCardList } from "../card-sources";
|
|
5
|
+
import { useCheckoutCopy } from "../copy-context";
|
|
6
|
+
import type { BuyerInfo, CheckoutCustomerField, CheckoutProviderConfig } from "../types";
|
|
7
|
+
|
|
8
|
+
import { askedFields, cardComplete, lastUsedCard, onlineRows, preselection, raisedMethod, type OnlineCapability, type OnlineRow } from "./choice";
|
|
9
|
+
import type { CheckoutListChoice } from "./types";
|
|
10
|
+
|
|
11
|
+
const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
|
|
12
|
+
|
|
13
|
+
/** The card the buyer pays with, as the list holds it before any order exists. */
|
|
14
|
+
export interface ListCard {
|
|
15
|
+
saved: SavedCard[];
|
|
16
|
+
/** A saved card's id, `NEW_CARD` for the fields, or `null` when none is chosen. */
|
|
17
|
+
selection: string | null;
|
|
18
|
+
setSelection: (selection: string | null) => void;
|
|
19
|
+
card: CardDetails;
|
|
20
|
+
setCard: Dispatch<SetStateAction<CardDetails>>;
|
|
21
|
+
fieldErrors: CardFieldErrors;
|
|
22
|
+
setFieldErrors: Dispatch<SetStateAction<CardFieldErrors>>;
|
|
23
|
+
saveCard: boolean;
|
|
24
|
+
setSaveCard: (save: boolean) => void;
|
|
25
|
+
complete: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Everything the one list holds, and nothing it raises. */
|
|
29
|
+
export interface ListState {
|
|
30
|
+
rows: OnlineRow[];
|
|
31
|
+
choice: CheckoutListChoice | null;
|
|
32
|
+
choose: (next: CheckoutListChoice) => void;
|
|
33
|
+
card: ListCard;
|
|
34
|
+
/** The buyer's own details the chosen online option must ask for (M11). */
|
|
35
|
+
asked: CheckoutCustomerField[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The card the buyer is typing or has picked. It outlives every order raised
|
|
40
|
+
* under it: the charge reads it when the 5-second bar fills, and a refusal
|
|
41
|
+
* brings the buyer back to it as they left it.
|
|
42
|
+
*/
|
|
43
|
+
function useListCard(saved: SavedCard[]): ListCard {
|
|
44
|
+
const fieldCopy = useCheckoutCopy().card.fields;
|
|
45
|
+
const [selection, setSelection] = useState<string | null>(null);
|
|
46
|
+
const [card, setCard] = useState<CardDetails>(EMPTY_CARD);
|
|
47
|
+
const [fieldErrors, setFieldErrors] = useState<CardFieldErrors>({});
|
|
48
|
+
const [saveCard, setSaveCard] = useState(false);
|
|
49
|
+
const complete = useMemo(() => cardComplete(card, fieldCopy), [card, fieldCopy]);
|
|
50
|
+
return { saved, selection, setSelection, card, setCard, fieldErrors, setFieldErrors, saveCard, setSaveCard, complete };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Preselect ONCE, when the saved cards answer — never over a choice the buyer
|
|
55
|
+
* already made, and never again after they made one (M7).
|
|
56
|
+
*/
|
|
57
|
+
function usePreselect(
|
|
58
|
+
loaded: boolean,
|
|
59
|
+
rows: readonly OnlineRow[],
|
|
60
|
+
saved: readonly SavedCard[],
|
|
61
|
+
fresh: boolean,
|
|
62
|
+
apply: (choice: CheckoutListChoice, selection: string | null) => void,
|
|
63
|
+
touched: { current: boolean },
|
|
64
|
+
): void {
|
|
65
|
+
const done = useRef(false);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (done.current || touched.current || !loaded) return;
|
|
68
|
+
done.current = true;
|
|
69
|
+
const pick = preselection(rows, saved, fresh);
|
|
70
|
+
if (pick) apply(pick.choice, pick.selection);
|
|
71
|
+
}, [loaded, rows, saved, fresh, apply, touched]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The card chosen again: the one used last (else the head of the saved list) —
|
|
76
|
+
* or, for a buyer back from a refused card (FUT-1145), no saved card at all
|
|
77
|
+
* (that one may be the card that failed), or the fields when nothing is saved.
|
|
78
|
+
*/
|
|
79
|
+
function reopenedSelection(cards: readonly SavedCard[], fresh: boolean): string | null {
|
|
80
|
+
const head = lastUsedCard(cards);
|
|
81
|
+
if (!head) return NEW_CARD;
|
|
82
|
+
return fresh ? null : head.id;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The one list's state (FUT-3591). Picking an option changes THIS and nothing
|
|
87
|
+
* else: no order, no charge, no request — the bar's button is the only thing
|
|
88
|
+
* that raises (M7). The host hears about every change through
|
|
89
|
+
* `onChoiceChange`, which is how its door options learn they are chosen.
|
|
90
|
+
*/
|
|
91
|
+
export function useListState(input: {
|
|
92
|
+
config: CheckoutProviderConfig | null;
|
|
93
|
+
capability: OnlineCapability;
|
|
94
|
+
tenantSlug: string | undefined;
|
|
95
|
+
/** Back here after a refused card (FUT-1145): no saved card is selected for them. */
|
|
96
|
+
fresh: boolean;
|
|
97
|
+
arrivedWith: BuyerInfo | undefined;
|
|
98
|
+
taxIdOnFile: boolean;
|
|
99
|
+
onChoiceChange?: (choice: CheckoutListChoice | null) => void;
|
|
100
|
+
}): ListState {
|
|
101
|
+
const { capability } = input;
|
|
102
|
+
const rows = useMemo(() => onlineRows(capability), [capability]);
|
|
103
|
+
const { cards, loaded } = useSavedCardList(input.tenantSlug);
|
|
104
|
+
const card = useListCard(cards);
|
|
105
|
+
const [choice, setChoice] = useState<CheckoutListChoice | null>(null);
|
|
106
|
+
const touched = useRef(false);
|
|
107
|
+
const { setSelection } = card;
|
|
108
|
+
const apply = useCallback(
|
|
109
|
+
(next: CheckoutListChoice, selection: string | null) => {
|
|
110
|
+
setChoice(next);
|
|
111
|
+
if (selection !== null) setSelection(selection);
|
|
112
|
+
},
|
|
113
|
+
[setSelection],
|
|
114
|
+
);
|
|
115
|
+
usePreselect(loaded, rows, cards, input.fresh, apply, touched);
|
|
116
|
+
const choose = useCallback(
|
|
117
|
+
(next: CheckoutListChoice) => {
|
|
118
|
+
touched.current = true;
|
|
119
|
+
setChoice((previous) => {
|
|
120
|
+
// Coming back to the card from another option starts on the head of
|
|
121
|
+
// the saved list again (FN:420), or on the fields when there is none;
|
|
122
|
+
// what was typed in them is kept.
|
|
123
|
+
const reopensCard = next.kind === "online" && next.method === "CARD" && !(previous?.kind === "online" && previous.method === "CARD");
|
|
124
|
+
if (reopensCard) setSelection(reopenedSelection(cards, input.fresh));
|
|
125
|
+
return next;
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
[cards, setSelection, input.fresh],
|
|
129
|
+
);
|
|
130
|
+
// A choice the chain no longer offers (a config that resolved to a hand-off
|
|
131
|
+
// store after the card was preselected) is no choice at all.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (choice?.kind === "online" && !rows.some((row) => row.method === choice.method && !row.disabled)) setChoice(null);
|
|
134
|
+
}, [choice, rows]);
|
|
135
|
+
const { onChoiceChange } = input;
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
onChoiceChange?.(choice);
|
|
138
|
+
}, [choice, onChoiceChange]);
|
|
139
|
+
const asked = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
choice?.kind === "online"
|
|
142
|
+
? askedFields({
|
|
143
|
+
chain: input.config?.chain,
|
|
144
|
+
method: raisedMethod(choice.method, capability.offered),
|
|
145
|
+
arrivedWith: input.arrivedWith,
|
|
146
|
+
taxIdOnFile: input.taxIdOnFile,
|
|
147
|
+
})
|
|
148
|
+
: [],
|
|
149
|
+
[choice, input.config, capability.offered, input.arrivedWith, input.taxIdOnFile],
|
|
150
|
+
);
|
|
151
|
+
return { rows, choice, choose, card, asked };
|
|
152
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -105,6 +105,9 @@ export function StaleVeil({ stale, children }: { stale: boolean; children: React
|
|
|
105
105
|
>
|
|
106
106
|
<Box
|
|
107
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=""
|
|
108
111
|
sx={{
|
|
109
112
|
m: 0,
|
|
110
113
|
px: 1.5,
|
|
@@ -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
|
);
|
|
@@ -308,7 +308,9 @@ export function createCheckoutClient(transport: CheckoutTransport): CheckoutClie
|
|
|
308
308
|
// offered for a Stone charge it would fail (or misroute).
|
|
309
309
|
const query = tenantSlug ? `?tenantSlug=${encodeURIComponent(tenantSlug)}` : "";
|
|
310
310
|
const result = await call<SavedCard[]>(`/cards${query}`, { method: "GET" });
|
|
311
|
-
|
|
311
|
+
// A route that answers `null` (or no list at all) has no saved card —
|
|
312
|
+
// every screen that reads this maps over it.
|
|
313
|
+
return result.ok && Array.isArray(result.data) ? result.data : [];
|
|
312
314
|
},
|
|
313
315
|
|
|
314
316
|
beginVault: () => call<BuyerVaultSession>("/cards/begin", { method: "POST" }),
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
* The buyer checkout's payment vocabulary (FUT-7 / FUT-44 / FUT-58, moved into
|
|
3
3
|
* the library by FUT-564).
|
|
4
4
|
*
|
|
5
|
-
* Pure types with no runtime imports. Everything here is PAYMENT: what a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the ports declared on `CheckoutFlowProps`.
|
|
5
|
+
* Pure types with no runtime imports. Everything here is PAYMENT: what a charge, an
|
|
6
|
+
* order-in-payment, a buyer identity or a provider protocol looks like to the screens in
|
|
7
|
+
* this folder. The host-side halves — the cart, the catalog, order CREATION — stay in the
|
|
8
|
+
* host app and reach the flow through the ports declared on `CheckoutFlowProps`.
|
|
10
9
|
*/
|
|
11
10
|
|
|
12
11
|
import type { CheckoutDecline, CheckoutDeclineReason } from "./decline";
|
|
@@ -15,10 +14,9 @@ import type { CheckoutDecline, CheckoutDeclineReason } from "./decline";
|
|
|
15
14
|
export type PaymentMethod = "PIX" | "CARD";
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
|
-
* The digital wallets a chain entry can charge (FUT-471/472). Mirrors the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* charge.
|
|
17
|
+
* The digital wallets a chain entry can charge (FUT-471/472). Mirrors the backend's
|
|
18
|
+
* `WalletType` — a wallet is not a fourth method: it is another way of producing the
|
|
19
|
+
* CARD instrument, so it rides the CARD tile and the CARD charge.
|
|
22
20
|
*/
|
|
23
21
|
export type CheckoutWalletType = "GOOGLE_PAY" | "APPLE_PAY";
|
|
24
22
|
|
|
@@ -114,6 +112,8 @@ export interface CheckoutOrder {
|
|
|
114
112
|
appliedDiscounts: CheckoutDiscount[];
|
|
115
113
|
/** Pre-formatted BRL grand total (e.g. `R$ 42,90`). */
|
|
116
114
|
totalLabel: string;
|
|
115
|
+
/** Cents the provider is asked for when a balance the host holds pays part of {@link totalCents}; absent ⇒ the total. */
|
|
116
|
+
amountDueCents?: number;
|
|
117
117
|
/** Present only when {@link method} is `"PIX"`. */
|
|
118
118
|
pix?: PixCharge;
|
|
119
119
|
/**
|
|
@@ -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
|
}
|
package/src/flows/types.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* `chain[].provider`, looked up in the tokenizer registry and used verbatim as
|
|
20
20
|
* a `tokensByProvider` key. A host never types one.
|
|
21
21
|
*/
|
|
22
|
+
import type { CheckoutPaymentList } from "../components/checkout/list/types";
|
|
22
23
|
import type { ComponentType, ReactNode } from "react";
|
|
23
24
|
|
|
24
25
|
import type { SavedCard } from "../card";
|
|
@@ -115,6 +116,12 @@ export interface PaymentFlowsConfig extends CheckoutPipelineConfig {
|
|
|
115
116
|
useBuyerDefaults?(): { buyer?: BuyerInfo; taxIdOnFile?: boolean; pending?: boolean };
|
|
116
117
|
/** Present ⇒ this checkout settles a settlement rather than the cart. */
|
|
117
118
|
useSettlement?(): SettlementCheckout | null;
|
|
119
|
+
/**
|
|
120
|
+
* The Pagamento step as ONE list (FUT-3591) — `CheckoutFlowProps.paymentList`,
|
|
121
|
+
* read through a HOOK because the host's door options follow its own state.
|
|
122
|
+
* Absent, or answering `undefined`, keeps the method tabs.
|
|
123
|
+
*/
|
|
124
|
+
usePaymentList?(): CheckoutPaymentList | undefined;
|
|
118
125
|
|
|
119
126
|
/** Design-system slots, filled ONCE instead of per screen. */
|
|
120
127
|
components?: Partial<CheckoutComponents>;
|
package/src/index.ts
CHANGED
|
@@ -54,11 +54,10 @@ export { PT_BR_CHECKOUT_PAYMENT_COPY } from './components/checkout-payment-pt-BR
|
|
|
54
54
|
// mounted by a host in one line. See ADOPTING.md §3 for the ports and the
|
|
55
55
|
// design-system slot contract.
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
|
-
export {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
} from './components/checkout/checkout-flow';
|
|
57
|
+
export { CheckoutFlow, type CheckoutCartView, type CheckoutFlowProps } from './components/checkout/checkout-flow';
|
|
58
|
+
// The Pagamento step as ONE list (FUT-3591/3592) — opt-in via `CheckoutFlowProps.paymentList`.
|
|
59
|
+
export type { CheckoutDoorOption, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, PaymentListCopy } from './components/checkout/list/types';
|
|
60
|
+
export { PT_BR_PAYMENT_LIST_COPY } from './components/checkout/list/pt-BR';
|
|
62
61
|
export type { BackActionEmphasis } from './components/checkout/payment-status-parts';
|
|
63
62
|
export type { CheckoutDeliverySummary, CheckoutSummaryLine } from './components/checkout/payment-summary';
|
|
64
63
|
|