@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
|
@@ -20,6 +20,22 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { ReactNode } from "react";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* What a door option's `content` function is handed (FUT-3715).
|
|
25
|
+
*
|
|
26
|
+
* `choose` is exactly what the row's radio does; calling it on the chosen row
|
|
27
|
+
* changes nothing. The list reports the choice through `onChoiceChange` as it
|
|
28
|
+
* does for the radio — from an effect, after the render — so a host that
|
|
29
|
+
* answers in the same tap keeps its answer across that render. The list never
|
|
30
|
+
* moves keyboard focus here: where focus goes is the host's call.
|
|
31
|
+
*/
|
|
32
|
+
export interface CheckoutDoorRowControls {
|
|
33
|
+
/** This option is the list's current choice. */
|
|
34
|
+
chosen: boolean;
|
|
35
|
+
/** Choose this option, as its radio would. */
|
|
36
|
+
choose: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
/**
|
|
24
40
|
* One of the host's ways to pay at the door — "Dinheiro", "Maquininha".
|
|
25
41
|
*
|
|
@@ -41,10 +57,25 @@ export interface CheckoutDoorOption {
|
|
|
41
57
|
hideHintWhileChosen?: boolean;
|
|
42
58
|
/**
|
|
43
59
|
* What opens INSIDE the row when it is chosen (the troco question, the
|
|
44
|
-
* tender chips, the brand lines). Rendered only while chosen
|
|
45
|
-
* indent: it starts where the row's own
|
|
60
|
+
* tender chips, the brand lines). Rendered only while chosen — unless
|
|
61
|
+
* `showContentWhileIdle` — with no left indent: it starts where the row's own
|
|
62
|
+
* content starts.
|
|
63
|
+
*
|
|
64
|
+
* As a function it is handed the row's own controls, so a control inside the
|
|
65
|
+
* content can choose the row in the same tap that answers it (the troco
|
|
66
|
+
* chips, FUT-3715). A plain node behaves exactly as before. The function is
|
|
67
|
+
* called during the row's render, not mounted as a component: keep it one
|
|
68
|
+
* shape for the option's life, and return a component of your own if the
|
|
69
|
+
* content needs hooks.
|
|
70
|
+
*/
|
|
71
|
+
content?: ReactNode | ((row: CheckoutDoorRowControls) => ReactNode);
|
|
72
|
+
/**
|
|
73
|
+
* Render `content` while the option is NOT chosen too — for an option whose
|
|
74
|
+
* answers are themselves the way to choose it (FUT-3715). The unchosen row
|
|
75
|
+
* keeps its unchosen frame; the content keeps its box and place in both
|
|
76
|
+
* states, so choosing the row moves nothing.
|
|
46
77
|
*/
|
|
47
|
-
|
|
78
|
+
showContentWhileIdle?: boolean;
|
|
48
79
|
/** The bar button's verb while this option is chosen — "Enviar pedido". */
|
|
49
80
|
action: string;
|
|
50
81
|
/** The amount beside the verb. Absent ⇒ the cart's own total. */
|
|
@@ -62,7 +93,12 @@ export interface CheckoutDoorOption {
|
|
|
62
93
|
onSend: () => void;
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* The online ways to pay the list may offer, as the host is told about them.
|
|
98
|
+
* `"GOOGLE_PAY"` is never reported any more: the wallets are buttons above the
|
|
99
|
+
* list, not rows (FUT-3685). It stays in the union so a host's exhaustive
|
|
100
|
+
* switch still compiles.
|
|
101
|
+
*/
|
|
66
102
|
export type CheckoutOnlineChoice = "PIX" | "CARD" | "GOOGLE_PAY" | "HAND_OFF";
|
|
67
103
|
|
|
68
104
|
/** What the buyer has chosen in the list. */
|
|
@@ -104,15 +140,14 @@ export interface CheckoutPaymentList {
|
|
|
104
140
|
/**
|
|
105
141
|
* The amount an ONLINE payment will ask the provider for, in integer cents —
|
|
106
142
|
* the cart's total, or what is left of it after a balance the host holds
|
|
107
|
-
* (store credit). It is what
|
|
108
|
-
* sheet must open INSIDE the tap, before any order exists to read a
|
|
109
|
-
* from. Never given, or zero (a balance covers everything) ⇒ no
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* takes a balance off the charge MUST answer `amountDueCents`.
|
|
143
|
+
* (store credit). It is what the wallets' sheets are priced with (FUT-3685),
|
|
144
|
+
* since a sheet must open INSIDE the tap, before any order exists to read a
|
|
145
|
+
* price from. Never given, or zero (a balance covers everything) ⇒ no wallet
|
|
146
|
+
* buttons. Once given, a moment without it (the host re-pricing) keeps their
|
|
147
|
+
* place, held — plainly, with `copy.reasons.repricing` — until it returns. A
|
|
148
|
+
* key is refused if the order raised behind the sheet asks the provider for
|
|
149
|
+
* another amount (`CheckoutOrder.amountDueCents`, else its total) — so a host
|
|
150
|
+
* that takes a balance off the charge MUST answer `amountDueCents`.
|
|
116
151
|
*/
|
|
117
152
|
totalCents?: number;
|
|
118
153
|
/**
|
|
@@ -168,6 +203,17 @@ export interface CheckoutPaymentList {
|
|
|
168
203
|
pixBank?: CheckoutPixBank;
|
|
169
204
|
/** Says a short sentence in the host's own toast — "Código Pix copiado". */
|
|
170
205
|
notify?: (message: string) => void;
|
|
206
|
+
/**
|
|
207
|
+
* The host lays out the computer's two columns itself (FUT-3685, L4): its
|
|
208
|
+
* step cards on the left, and this element on the right — the list portals
|
|
209
|
+
* the "Resumo do pedido" column into it (the lines, the money rows, "Total",
|
|
210
|
+
* `summary`, the extras and the pay button), so no bar is drawn at the
|
|
211
|
+
* bottom. `null` while the element is not mounted yet: the bar waits rather
|
|
212
|
+
* than flash. Leave it OUT (undefined) on a phone, or to keep the list's own
|
|
213
|
+
* layout — the step and the column side by side once the flow's frame is
|
|
214
|
+
* wide, the bar otherwise. The Pix screen and the hand-off keep their bar.
|
|
215
|
+
*/
|
|
216
|
+
summaryTarget?: HTMLElement | null;
|
|
171
217
|
}
|
|
172
218
|
|
|
173
219
|
/** The six banks the Pix face offers to open, in the prototype's order. */
|
|
@@ -240,7 +286,26 @@ export interface PaymentListCopy {
|
|
|
240
286
|
/** "Digite os dados do cartão". */
|
|
241
287
|
hintNew: string;
|
|
242
288
|
};
|
|
289
|
+
/**
|
|
290
|
+
* @deprecated Google Pay is no longer a row (FUT-3685): it is a button in
|
|
291
|
+
* the "PAGAMENTO RÁPIDO" block — see {@link PaymentListCopy.wallets}. Kept so
|
|
292
|
+
* a host's own pack still type-checks; nothing draws it.
|
|
293
|
+
*/
|
|
243
294
|
googlePay: { label: string; hint: string };
|
|
295
|
+
/**
|
|
296
|
+
* The "PAGAMENTO RÁPIDO" block above the list (FUT-3685, T5–T8): the wallet
|
|
297
|
+
* buttons' accessible names and the words around them.
|
|
298
|
+
*/
|
|
299
|
+
wallets: {
|
|
300
|
+
/** "Pagamento rápido" — drawn in capitals by CSS. */
|
|
301
|
+
heading: string;
|
|
302
|
+
/** "Pagar com Google Pay". */
|
|
303
|
+
googlePay: string;
|
|
304
|
+
/** "Pagar com Apple Pay". */
|
|
305
|
+
applePay: string;
|
|
306
|
+
/** "ou escolha como pagar" — the divider between the wallets and the rows. */
|
|
307
|
+
or: string;
|
|
308
|
+
};
|
|
244
309
|
handOff: {
|
|
245
310
|
label: string;
|
|
246
311
|
/** "Você paga na página da InfinitePay e volta" — `null` when the store published no name. */
|
|
@@ -258,6 +323,17 @@ export interface PaymentListCopy {
|
|
|
258
323
|
email: string;
|
|
259
324
|
/** "(o PagBank pede para pagar pelo app)" — `null` when no name was published. */
|
|
260
325
|
why: (provider: string | null) => string;
|
|
326
|
+
/**
|
|
327
|
+
* The line under "CPF" at the top of Pagamento (FUT-3685, T3) — "o PagBank
|
|
328
|
+
* pede para pagar pelo app"; `null` when no name was published.
|
|
329
|
+
*/
|
|
330
|
+
cpfHint: (provider: string | null) => string;
|
|
331
|
+
/**
|
|
332
|
+
* The link under the CPF opened by "Alterar" over a CPF on file, closing it
|
|
333
|
+
* again — "Manter o CPF salvo" (FUT-3685). Both packs carry it; absent ⇒ no
|
|
334
|
+
* link (an empty field keeps the saved CPF either way).
|
|
335
|
+
*/
|
|
336
|
+
keepSavedCpf?: string;
|
|
261
337
|
cpfIncomplete: string;
|
|
262
338
|
phoneIncomplete: string;
|
|
263
339
|
};
|
|
@@ -1,58 +1,136 @@
|
|
|
1
|
-
import { useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
+
import { applePaySessionClass, applePaySupported, type ApplePaySessionClass } from "../apple-pay-button";
|
|
3
4
|
import { useGooglePayClient, type GooglePayGatewayParams, type GooglePaymentsClient } from "../google-pay-button";
|
|
4
|
-
import { cardPathAvailable, googlePayConfig, offeredMethods } from "../method-capability";
|
|
5
|
+
import { applePayDeclared, cardPathAvailable, googlePayConfig, offeredMethods } from "../method-capability";
|
|
5
6
|
import { methodChosenAtProvider } from "../providers/registry";
|
|
6
|
-
import type { CheckoutProviderConfig } from "../types";
|
|
7
|
+
import type { CheckoutProviderConfig, PaymentMethod } from "../types";
|
|
7
8
|
|
|
8
9
|
import type { OnlineCapability } from "./choice";
|
|
9
10
|
|
|
10
|
-
/**
|
|
11
|
+
/** Apple Pay, ready to start a session inside the tap. */
|
|
12
|
+
export interface ListApplePay {
|
|
13
|
+
Session: ApplePaySessionClass;
|
|
14
|
+
/** The host's merchant-validation port (server-side). */
|
|
15
|
+
validateMerchant: (validationURL: string) => Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** What the store's chain lets THIS browser offer in the list, the wallets included. */
|
|
11
19
|
export interface ListCapability extends OnlineCapability {
|
|
12
|
-
/** Google Pay, ready to open its sheet — `null` when
|
|
20
|
+
/** Google Pay, ready to open its sheet — `null` when it is not offered, or while the host re-prices. */
|
|
13
21
|
googlePay: { client: GooglePaymentsClient; params: GooglePayGatewayParams } | null;
|
|
22
|
+
/** Apple Pay, ready to start its session — `null` when it is not offered, or while the host re-prices. */
|
|
23
|
+
applePay: ListApplePay | null;
|
|
24
|
+
/**
|
|
25
|
+
* The wallets the "PAGAMENTO RÁPIDO" block draws, in the prototype's order.
|
|
26
|
+
* A wallet drawn while the host re-prices has its button held
|
|
27
|
+
* (`googlePay`/`applePay` is `null` for that moment).
|
|
28
|
+
*/
|
|
29
|
+
wallets: ListWallet[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ListWallet = "GOOGLE_PAY" | "APPLE_PAY";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The host priced the sheet once and, for a moment, has no price (re-pricing:
|
|
36
|
+
* the credit was toggled) — a wallet keeps its place, held, until the price
|
|
37
|
+
* returns. An explicit zero (a balance covers everything) withdraws it.
|
|
38
|
+
*/
|
|
39
|
+
function useRepricing(totalCents: number | undefined): { priced: boolean; repricing: boolean } {
|
|
40
|
+
const priced = totalCents !== undefined && totalCents > 0;
|
|
41
|
+
const [wasPriced, setWasPriced] = useState(false);
|
|
42
|
+
if (priced && !wasPriced) setWasPriced(true);
|
|
43
|
+
if (totalCents === 0 && wasPriced) setWasPriced(false);
|
|
44
|
+
return { priced, repricing: totalCents === undefined && wasPriced };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Apple Pay's session class, when the chain head declares the wallet and this device can pay (R5). */
|
|
48
|
+
function appleSession(config: CheckoutProviderConfig | null, port: ListApplePay["validateMerchant"] | undefined): ListApplePay | null {
|
|
49
|
+
if (!port || !applePayDeclared(config) || !applePaySupported()) return null;
|
|
50
|
+
const Session = applePaySessionClass();
|
|
51
|
+
return Session ? { Session, validateMerchant: port } : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Whether the card is offered and available on THIS page — what a wallet is another way of producing. */
|
|
55
|
+
function cardHere(handOff: boolean, offered: PaymentMethod[] | null, cardUnavailable: boolean): boolean {
|
|
56
|
+
return !handOff && !cardUnavailable && (offered === null || offered.includes("CARD"));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The wallets the block draws: each ready one, while the card is here and the sheet is (or was) priced. */
|
|
60
|
+
function walletsShown(shown: boolean, google: unknown, apple: unknown): ListWallet[] {
|
|
61
|
+
if (!shown) return [];
|
|
62
|
+
const wallets: ListWallet[] = [];
|
|
63
|
+
if (google) wallets.push("GOOGLE_PAY");
|
|
64
|
+
if (apple) wallets.push("APPLE_PAY");
|
|
65
|
+
return wallets;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Google Pay's client and the head's gateway parameters, when both are there. */
|
|
69
|
+
function googleSheet(config: CheckoutProviderConfig | null, client: GooglePaymentsClient | null): ListCapability["googlePay"] {
|
|
70
|
+
const params = googlePayConfig(config);
|
|
71
|
+
return params && client ? { client, params } : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** {@link useListCapability}'s answer, from what it read. */
|
|
75
|
+
function capabilityOf(
|
|
76
|
+
config: CheckoutProviderConfig | null,
|
|
77
|
+
client: GooglePaymentsClient | null,
|
|
78
|
+
price: { priced: boolean; repricing: boolean },
|
|
79
|
+
port: ListApplePay["validateMerchant"] | undefined,
|
|
80
|
+
): ListCapability {
|
|
81
|
+
const offered = offeredMethods(config);
|
|
82
|
+
const cardUnavailable = !cardPathAvailable(config);
|
|
83
|
+
const handOff = methodChosenAtProvider(config?.chain?.[0]?.checkoutScreen, config);
|
|
84
|
+
const card = cardHere(handOff, offered, cardUnavailable);
|
|
85
|
+
const google = googleSheet(config, client);
|
|
86
|
+
const apple = appleSession(config, port);
|
|
87
|
+
const live = card && price.priced;
|
|
88
|
+
return {
|
|
89
|
+
handOff,
|
|
90
|
+
offered,
|
|
91
|
+
cardUnavailable,
|
|
92
|
+
wallets: walletsShown(card && (price.priced || price.repricing), google, apple),
|
|
93
|
+
googlePay: live ? google : null,
|
|
94
|
+
applePay: live ? apple : null,
|
|
95
|
+
};
|
|
14
96
|
}
|
|
15
97
|
|
|
16
98
|
/**
|
|
17
99
|
* The capability reads the tabs layout already makes (`method-capability.ts`,
|
|
18
|
-
* `providers/registry.ts`), plus the
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
100
|
+
* `providers/registry.ts`), plus the two wallets of the "PAGAMENTO RÁPIDO"
|
|
101
|
+
* block (FUT-3685, R5). A wallet is ready when the chain head declares it AND
|
|
102
|
+
* this device can pay with it — Google's `isReadyToPay`, Apple's
|
|
103
|
+
* `canMakePayments` — AND the card is offered and available here, since a
|
|
104
|
+
* wallet is another way of producing the card (so a Pix-only store never shows
|
|
105
|
+
* the block). Apple also needs the host's merchant-validation port: without it
|
|
106
|
+
* the session could only abort.
|
|
22
107
|
*
|
|
23
|
-
*
|
|
108
|
+
* Both sheets open inside the tap, before any order exists, so they are priced
|
|
24
109
|
* with the host's `totalCents` — the amount DUE. A host that never priced it,
|
|
25
|
-
* or prices it at zero (a balance
|
|
26
|
-
* Once priced, a moment without a price (the host re-pricing — the credit was
|
|
27
|
-
* toggled) keeps the row, and the list holds a chosen Google Pay's button,
|
|
28
|
-
* plainly, until the price returns.
|
|
29
|
-
*
|
|
30
|
-
* Apple Pay is not a row here: its sheet must open synchronously inside the
|
|
31
|
-
* tap, before any order could be raised for it. The tabs layout keeps it.
|
|
110
|
+
* or prices it at zero (a balance covers everything), offers no wallet.
|
|
32
111
|
*/
|
|
33
112
|
export function useListCapability(
|
|
34
113
|
config: CheckoutProviderConfig | null,
|
|
35
|
-
/** The host's price for the
|
|
114
|
+
/** The host's price for the sheets — no price (or zero), no wallet. */
|
|
36
115
|
totalCents: number | undefined,
|
|
116
|
+
validateApplePayMerchant?: ListApplePay["validateMerchant"],
|
|
37
117
|
): ListCapability {
|
|
38
118
|
const declared = googlePayConfig(config) !== null;
|
|
39
119
|
// `null` tells the client hook not to load pay.js at all.
|
|
40
120
|
const client = useGooglePayClient(declared ? undefined : null, "TEST");
|
|
41
|
-
const priced
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
}, [config, client, priced, repricing]);
|
|
121
|
+
const { priced, repricing } = useRepricing(totalCents);
|
|
122
|
+
// The port is read at the press, through a ref: a host passing a new
|
|
123
|
+
// function each render must not rebuild the capability (and the wallets'
|
|
124
|
+
// buttons) every time. Only whether there IS one changes what is offered.
|
|
125
|
+
const port = useRef(validateApplePayMerchant);
|
|
126
|
+
port.current = validateApplePayMerchant;
|
|
127
|
+
const hasPort = validateApplePayMerchant !== undefined;
|
|
128
|
+
const stablePort = useCallback((validationURL: string): Promise<unknown> => {
|
|
129
|
+
const current = port.current;
|
|
130
|
+
return current ? current(validationURL) : Promise.reject(new Error("no merchant validation port"));
|
|
131
|
+
}, []);
|
|
132
|
+
return useMemo(
|
|
133
|
+
() => capabilityOf(config, client, { priced, repricing }, hasPort ? stablePort : undefined),
|
|
134
|
+
[config, client, priced, repricing, hasPort, stablePort],
|
|
135
|
+
);
|
|
58
136
|
}
|
|
@@ -20,8 +20,8 @@ export const CHARGE_WINDOW_MS = 5_000;
|
|
|
20
20
|
*/
|
|
21
21
|
export type ListChargePhase = "idle" | "counting" | "sending" | "waiting";
|
|
22
22
|
|
|
23
|
-
/** What a charge runs against: the card the buyer chose, or
|
|
24
|
-
export type ListChargeKind = "CARD" | "GOOGLE_PAY";
|
|
23
|
+
/** What a charge runs against: the card the buyer chose, or a wallet's sheet. */
|
|
24
|
+
export type ListChargeKind = "CARD" | "GOOGLE_PAY" | "APPLE_PAY";
|
|
25
25
|
|
|
26
26
|
/** The controller's slice this needs. */
|
|
27
27
|
interface RaisePorts {
|
|
@@ -41,7 +41,7 @@ export interface ListCharge {
|
|
|
41
41
|
kind: ListChargeKind | null;
|
|
42
42
|
/** The amount named when the button was pressed — the bar shows THIS, never a later one. */
|
|
43
43
|
pressedTotal: string | null;
|
|
44
|
-
/** Start a card charge (with its 5-second bar) or a
|
|
44
|
+
/** Start a card charge (with its 5-second bar) or a wallet one (straight away: its sheet is the confirmation). Only from idle. */
|
|
45
45
|
start: (kind: ListChargeKind, total: string) => void;
|
|
46
46
|
/** Cancelar, during the bar: nothing has been sent, and nothing will be. */
|
|
47
47
|
cancel: () => void;
|
|
@@ -131,11 +131,12 @@ export function useListCharge(ports: RaisePorts): ListCharge {
|
|
|
131
131
|
setPressedTotal(null);
|
|
132
132
|
}, []);
|
|
133
133
|
|
|
134
|
-
const send = useCallback(() => {
|
|
134
|
+
const send = useCallback((wallet: boolean) => {
|
|
135
135
|
timer.current = null;
|
|
136
136
|
// Checked again at the last moment: a hold raised between two renders
|
|
137
|
-
// must not let the bar's own timer charge past it.
|
|
138
|
-
|
|
137
|
+
// must not let the bar's own timer charge past it. A wallet's press is not
|
|
138
|
+
// the bar's: its sheet is already open, and the block held it itself.
|
|
139
|
+
if (!wallet && !allowed.current) return cancel();
|
|
139
140
|
setPhase("sending");
|
|
140
141
|
const { order, cartTotal, dueCents } = latest.current;
|
|
141
142
|
if (reusable(order, cartTotal, dueCents) && order) {
|
|
@@ -152,12 +153,13 @@ export function useListCharge(ports: RaisePorts): ListCharge {
|
|
|
152
153
|
const start = useCallback(
|
|
153
154
|
(next: ListChargeKind, total: string) => {
|
|
154
155
|
if (phaseRef.current !== "idle") return;
|
|
155
|
-
|
|
156
|
+
const wallet = next !== "CARD";
|
|
157
|
+
phaseRef.current = wallet ? "sending" : "counting";
|
|
156
158
|
setKind(next);
|
|
157
159
|
setPressedTotal(total);
|
|
158
|
-
if (
|
|
160
|
+
if (wallet) return send(true);
|
|
159
161
|
setPhase("counting");
|
|
160
|
-
timer.current = setTimeout(send, CHARGE_WINDOW_MS);
|
|
162
|
+
timer.current = setTimeout(() => send(false), CHARGE_WINDOW_MS);
|
|
161
163
|
},
|
|
162
164
|
[send],
|
|
163
165
|
);
|
|
@@ -3,10 +3,12 @@ import { useCallback, useEffect, useMemo, useRef, useState, type Dispatch, type
|
|
|
3
3
|
import { NEW_CARD, type CardDetails, type CardFieldErrors, type SavedCard } from "../../../card";
|
|
4
4
|
import { useSavedCardList } from "../card-sources";
|
|
5
5
|
import { useCheckoutCopy } from "../copy-context";
|
|
6
|
-
import type { BuyerInfo, CheckoutCustomerField, CheckoutProviderConfig } from "../types";
|
|
6
|
+
import type { BuyerInfo, CheckoutCustomerField, CheckoutProviderConfig, PaymentMethod } from "../types";
|
|
7
7
|
|
|
8
8
|
import { askedFields, cardComplete, lastUsedCard, onlineRows, preselection, raisedMethod, type OnlineCapability, type OnlineRow } from "./choice";
|
|
9
|
+
import { isTaxId, topCpfField } from "./top-cpf";
|
|
9
10
|
import type { CheckoutListChoice } from "./types";
|
|
11
|
+
import type { ListWallet } from "./use-capability";
|
|
10
12
|
|
|
11
13
|
const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
|
|
12
14
|
|
|
@@ -31,8 +33,15 @@ export interface ListState {
|
|
|
31
33
|
choice: CheckoutListChoice | null;
|
|
32
34
|
choose: (next: CheckoutListChoice) => void;
|
|
33
35
|
card: ListCard;
|
|
34
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The buyer's own details the chosen online option asks for INSIDE its row
|
|
38
|
+
* (M11) — the hand-off provider's mobile (K4). Never the CPF: that is `top`.
|
|
39
|
+
*/
|
|
35
40
|
asked: CheckoutCustomerField[];
|
|
41
|
+
/** The CPF asked at the top of Pagamento, or `null` (FUT-3685, E1/E2). */
|
|
42
|
+
top: CheckoutCustomerField | null;
|
|
43
|
+
/** Every online method the list offers, wallets included — what the top CPF is decided over (R2). */
|
|
44
|
+
methods: PaymentMethod[];
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/**
|
|
@@ -100,6 +109,10 @@ export function useListState(input: {
|
|
|
100
109
|
/** The buyer's last online method, from the host (FUT-3611). */
|
|
101
110
|
lastMethod?: "PIX" | "CARD" | null;
|
|
102
111
|
onChoiceChange?: (choice: CheckoutListChoice | null) => void;
|
|
112
|
+
/** The wallets the block above the list draws — each raises a CARD order. */
|
|
113
|
+
wallets?: readonly ListWallet[];
|
|
114
|
+
/** "Alterar" on the payer line revealed the CPF at the top (E2 → E1). */
|
|
115
|
+
revealed?: boolean;
|
|
103
116
|
}): ListState {
|
|
104
117
|
const { capability } = input;
|
|
105
118
|
const rows = useMemo(() => onlineRows(capability), [capability]);
|
|
@@ -147,9 +160,29 @@ export function useListState(input: {
|
|
|
147
160
|
method: raisedMethod(choice.method, capability.offered),
|
|
148
161
|
arrivedWith: input.arrivedWith,
|
|
149
162
|
taxIdOnFile: input.taxIdOnFile,
|
|
150
|
-
})
|
|
163
|
+
}).filter((field) => !isTaxId(field))
|
|
151
164
|
: [],
|
|
152
165
|
[choice, input.config, capability.offered, input.arrivedWith, input.taxIdOnFile],
|
|
153
166
|
);
|
|
154
|
-
|
|
167
|
+
const methods = useMemo(() => offeredOnline(rows, capability.offered, input.wallets ?? []), [rows, capability.offered, input.wallets]);
|
|
168
|
+
const top = useMemo(
|
|
169
|
+
() =>
|
|
170
|
+
topCpfField({
|
|
171
|
+
chain: input.config?.chain,
|
|
172
|
+
methods,
|
|
173
|
+
arrivedWith: input.arrivedWith,
|
|
174
|
+
taxIdOnFile: input.taxIdOnFile,
|
|
175
|
+
revealed: input.revealed ?? false,
|
|
176
|
+
}),
|
|
177
|
+
[input.config, methods, input.arrivedWith, input.taxIdOnFile, input.revealed],
|
|
178
|
+
);
|
|
179
|
+
return { rows, choice, choose, card, asked, top, methods };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The chain methods the list's rows and wallets would raise, each once. */
|
|
183
|
+
function offeredOnline(rows: readonly OnlineRow[], offered: PaymentMethod[] | null, wallets: readonly ListWallet[]): PaymentMethod[] {
|
|
184
|
+
const methods = new Set<PaymentMethod>();
|
|
185
|
+
for (const row of rows) if (!row.disabled) methods.add(raisedMethod(row.method, offered));
|
|
186
|
+
if (wallets.length > 0) methods.add("CARD");
|
|
187
|
+
return [...methods];
|
|
155
188
|
}
|