@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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/src/card/types.ts +6 -0
  3. package/src/components/checkout/card-sources.ts +140 -0
  4. package/src/components/checkout/card-view.tsx +6 -2
  5. package/src/components/checkout/cart-changed.tsx +26 -15
  6. package/src/components/checkout/checkout-actions.ts +18 -0
  7. package/src/components/checkout/checkout-flow.tsx +24 -77
  8. package/src/components/checkout/google-pay-button.tsx +4 -4
  9. package/src/components/checkout/list/buyer-inline.tsx +125 -0
  10. package/src/components/checkout/list/charge-runners.tsx +175 -0
  11. package/src/components/checkout/list/choice-list.tsx +159 -0
  12. package/src/components/checkout/list/choice.ts +213 -0
  13. package/src/components/checkout/list/context.tsx +55 -0
  14. package/src/components/checkout/list/en-US.ts +63 -0
  15. package/src/components/checkout/list/list-bar.tsx +186 -0
  16. package/src/components/checkout/list/list-face.tsx +338 -0
  17. package/src/components/checkout/list/list-pix.tsx +206 -0
  18. package/src/components/checkout/list/online-content.tsx +144 -0
  19. package/src/components/checkout/list/option-row.tsx +125 -0
  20. package/src/components/checkout/list/payment-list-step.tsx +166 -0
  21. package/src/components/checkout/list/pt-BR.ts +79 -0
  22. package/src/components/checkout/list/types.ts +218 -0
  23. package/src/components/checkout/list/use-capability.ts +58 -0
  24. package/src/components/checkout/list/use-list-charge.ts +186 -0
  25. package/src/components/checkout/list/use-list-state.ts +152 -0
  26. package/src/components/checkout/payment-phase.tsx +119 -0
  27. package/src/components/checkout/pix-parts.tsx +3 -0
  28. package/src/components/checkout/pix-view.tsx +53 -20
  29. package/src/components/checkout/transport.ts +3 -1
  30. package/src/components/checkout/types.ts +9 -9
  31. package/src/components/checkout/use-card-checkout.ts +11 -78
  32. package/src/components/checkout/use-checkout-controller.ts +3 -1
  33. package/src/flows/create-payment-flows.tsx +7 -0
  34. package/src/flows/types.ts +7 -0
  35. package/src/index.ts +4 -5
  36. 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
- function PixBody({ order, pix, poll }: { order: CheckoutOrder; pix: PixCharge; poll: ReturnType<typeof usePixPolling> }): JSX.Element {
225
- const copy = useCheckoutCopy().screens.pix;
226
- const stage = usePixStage();
227
- const { copy: writeClipboard, justCopied } = useClipboardCopy(pix.copyPaste);
228
- // Leaving the payment step (a method switch, a terminal status) ends the
229
- // after-copy face with it: the next pane starts on the code.
230
- const { setAfterCopy } = stage;
231
- // A clipboard write that settles after the pane is gone must not reopen the
232
- // after-copy face over whatever replaced it (it would hide the picker).
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
- return result.ok ? result.data : [];
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
- * charge, an order-in-payment, a buyer identity or a provider protocol looks
7
- * like to the screens in this folder. The host-side halves — the cart, the
8
- * catalog, order CREATION — stay in the host app and reach the flow through
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
- * backend's `WalletType` — a wallet is not a fourth method: it is another way
20
- * of producing the CARD instrument, so it rides the CARD tile and the CARD
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
  }
@@ -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
- CheckoutFlow,
59
- type CheckoutCartView,
60
- type CheckoutFlowProps,
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