@12-apps/payments-frontend 3.45.1 → 3.46.1
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/card/pagbank-test-cards.ts +17 -1
- package/src/card/three-ds.ts +166 -0
- package/src/card/tokenize.ts +6 -1
- package/src/components/checkout/client-context.tsx +1 -0
- package/src/components/checkout/debit-charge.ts +60 -0
- package/src/components/checkout/debit-types.ts +31 -0
- package/src/components/checkout/list/bank-en-US.ts +1 -0
- package/src/components/checkout/list/bank-pt-BR.ts +1 -0
- package/src/components/checkout/list/charge-runners.tsx +5 -0
- package/src/components/checkout/list/choice-list.tsx +17 -8
- package/src/components/checkout/list/choice.ts +6 -0
- package/src/components/checkout/list/debit-copy.ts +32 -0
- package/src/components/checkout/list/en-US.ts +7 -0
- package/src/components/checkout/list/list-face.tsx +10 -1
- package/src/components/checkout/list/list-pix.tsx +1 -1
- package/src/components/checkout/list/online-content.tsx +28 -12
- package/src/components/checkout/list/payment-list-step.tsx +13 -1
- package/src/components/checkout/list/pix-banks.tsx +18 -2
- package/src/components/checkout/list/pt-BR.ts +7 -0
- package/src/components/checkout/list/types.ts +14 -1
- package/src/components/checkout/list/use-capability.ts +16 -4
- package/src/components/checkout/list/use-list-state.ts +26 -7
- package/src/components/checkout/method-capability.ts +4 -2
- package/src/components/checkout/transport.ts +16 -5
- package/src/components/checkout/types.ts +4 -4
- package/src/components/checkout/use-card-checkout.ts +34 -13
- package/src/components/checkout/wallet-pane.tsx +1 -0
- package/src/components/platform/HomologacaoEvidenceCard.tsx +82 -27
- package/src/components/platform/en-US.ts +5 -5
- package/src/components/platform/pt-BR.ts +5 -5
- package/src/index.ts +1 -1
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState, type JSX, type ReactNode } f
|
|
|
2
2
|
|
|
3
3
|
import type { CheckoutBasketIdentity } from "../basket";
|
|
4
4
|
import { CartChangedBoundary } from "../cart-changed";
|
|
5
|
+
import { useCheckoutClientApi } from "../client-context";
|
|
5
6
|
import { displayTotals } from "../checkout-totals";
|
|
6
7
|
import type { CheckoutSummaryLine } from "../payment-summary";
|
|
7
8
|
import { usePaymentPixStage } from "../payment-step-layout";
|
|
@@ -144,6 +145,16 @@ function ListFaceFor({ props, state, charge, cap, onHandOff, reveal }: {
|
|
|
144
145
|
);
|
|
145
146
|
}
|
|
146
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Everything a debit row needs besides the provider's claim (FUT-3754): the
|
|
150
|
+
* host's billing address, its words for the row, and a client that can ask for
|
|
151
|
+
* the 3-D Secure session. Missing any one, there is no row.
|
|
152
|
+
*/
|
|
153
|
+
function useDebitReady(list: CheckoutPaymentList): boolean {
|
|
154
|
+
const client = useCheckoutClientApi();
|
|
155
|
+
return Boolean(list.billingAddress) && list.copy.debit !== undefined && client.threeDSecureSession !== undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
147
158
|
/**
|
|
148
159
|
* The Pagamento step as ONE list (FUT-3591, FUT-3592) — see `./types.ts`.
|
|
149
160
|
*
|
|
@@ -156,7 +167,8 @@ function ListFaceFor({ props, state, charge, cap, onHandOff, reveal }: {
|
|
|
156
167
|
export function PaymentListStep(props: PaymentListStepProps): JSX.Element {
|
|
157
168
|
const { c, list } = props;
|
|
158
169
|
const config = props.providerConfig ?? null;
|
|
159
|
-
const
|
|
170
|
+
const debitReady = useDebitReady(list);
|
|
171
|
+
const cap = useListCapability(config, list.totalCents, props.validateApplePayMerchant, debitReady);
|
|
160
172
|
const reveal = useReveal(c, props.defaultBuyer);
|
|
161
173
|
const { revealed } = reveal;
|
|
162
174
|
const state = useListState({
|
|
@@ -172,7 +172,23 @@ function Grid({ bank, state, stale }: { bank: CheckoutPixBank; state: PixBankSta
|
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
/** The
|
|
175
|
+
/** The one line under the bank being opened, where the checklist was (FUT-3612). */
|
|
176
|
+
function AppHint({ text }: { text: string }): JSX.Element {
|
|
177
|
+
const { Text } = useCheckoutComponents();
|
|
178
|
+
return (
|
|
179
|
+
<Text variant="body" size="sm" color="secondary" as="p" style={{ margin: 0 }} data-testid="pix-bank-hint">
|
|
180
|
+
{text}
|
|
181
|
+
</Text>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The grid, or the bank being opened and the line that says what to do there. */
|
|
176
186
|
export function PixBanks({ bank, state, stale }: { bank: CheckoutPixBank; state: PixBankState; stale: boolean }): JSX.Element {
|
|
177
|
-
|
|
187
|
+
if (!state.chosen) return <Grid bank={bank} state={state} stale={stale} />;
|
|
188
|
+
return (
|
|
189
|
+
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
190
|
+
<Opening bank={bank} state={state} chosen={state.chosen} />
|
|
191
|
+
{bank.copy.appHint ? <AppHint text={bank.copy.appHint} /> : null}
|
|
192
|
+
</Box>
|
|
193
|
+
);
|
|
178
194
|
}
|
|
@@ -27,6 +27,13 @@ export const PT_BR_PAYMENT_LIST_COPY: PaymentListCopy = {
|
|
|
27
27
|
hintSaved: (card) => `${card} ou outro cartão`,
|
|
28
28
|
hintNew: "Digite os dados do cartão",
|
|
29
29
|
},
|
|
30
|
+
debit: {
|
|
31
|
+
label: "Cartão de débito",
|
|
32
|
+
hint: "O banco pode pedir confirmação no app",
|
|
33
|
+
notSupported: "Esse cartão não pode ser usado no débito aqui. Tente no crédito ou no Pix.",
|
|
34
|
+
refused: "O banco não confirmou o pagamento no débito. Tente outro cartão ou o Pix.",
|
|
35
|
+
failed: "O banco não confirmou o débito agora. Tente de novo.",
|
|
36
|
+
},
|
|
30
37
|
googlePay: { label: "Google Pay", hint: "Com o cartão salvo no seu Google" },
|
|
31
38
|
wallets: {
|
|
32
39
|
heading: "Pagamento rápido",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { ReactNode } from "react";
|
|
22
22
|
|
|
23
|
+
import type { CheckoutBillingAddress, PaymentListDebitCopy } from "./debit-copy";
|
|
24
|
+
|
|
25
|
+
export type { CheckoutBillingAddress, PaymentListDebitCopy };
|
|
26
|
+
|
|
23
27
|
/**
|
|
24
28
|
* What a door option's `content` function is handed (FUT-3715).
|
|
25
29
|
*
|
|
@@ -103,7 +107,7 @@ export type CheckoutOnlineChoice = "PIX" | "CARD" | "GOOGLE_PAY" | "HAND_OFF";
|
|
|
103
107
|
|
|
104
108
|
/** What the buyer has chosen in the list. */
|
|
105
109
|
export type CheckoutListChoice =
|
|
106
|
-
| { kind: "online"; method: CheckoutOnlineChoice }
|
|
110
|
+
| { kind: "online"; method: CheckoutOnlineChoice; funding?: "DEBIT" }
|
|
107
111
|
| { kind: "door"; id: string };
|
|
108
112
|
|
|
109
113
|
/**
|
|
@@ -214,6 +218,8 @@ export interface CheckoutPaymentList {
|
|
|
214
218
|
* wide, the bar otherwise. The Pix screen and the hand-off keep their bar.
|
|
215
219
|
*/
|
|
216
220
|
summaryTarget?: HTMLElement | null;
|
|
221
|
+
/** The billing address a DEBIT card's 3-D Secure needs (FUT-3754); none ⇒ no debit row. */
|
|
222
|
+
billingAddress?: CheckoutBillingAddress | null;
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
/** The six banks the Pix face offers to open, in the prototype's order. */
|
|
@@ -264,6 +270,11 @@ export interface PixBankCopy {
|
|
|
264
270
|
notOpened: (bank: PixBankName) => string;
|
|
265
271
|
/** "Tentar de novo". */
|
|
266
272
|
retry: string;
|
|
273
|
+
/**
|
|
274
|
+
* The one line under the bank being opened, in place of the checklist —
|
|
275
|
+
* "Abra o app do seu banco e cole em Pix copia e cola." Absent ⇒ no line.
|
|
276
|
+
*/
|
|
277
|
+
appHint?: string;
|
|
267
278
|
/** The toasts. */
|
|
268
279
|
copied: string;
|
|
269
280
|
copiedPick: string;
|
|
@@ -286,6 +297,8 @@ export interface PaymentListCopy {
|
|
|
286
297
|
/** "Digite os dados do cartão". */
|
|
287
298
|
hintNew: string;
|
|
288
299
|
};
|
|
300
|
+
/** The DEBIT card row's words (FUT-3754). A pack without them offers no debit row. */
|
|
301
|
+
debit?: PaymentListDebitCopy;
|
|
289
302
|
/**
|
|
290
303
|
* @deprecated Google Pay is no longer a row (FUT-3685): it is a button in
|
|
291
304
|
* the "PAGAMENTO RÁPIDO" block — see {@link PaymentListCopy.wallets}. Kept so
|
|
@@ -71,12 +71,21 @@ function googleSheet(config: CheckoutProviderConfig | null, client: GooglePaymen
|
|
|
71
71
|
return params && client ? { client, params } : null;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The debit row (FUT-3754): the card is here, the head declares debit, and the
|
|
76
|
+
* host gave the address PagBank's 3-D Secure refuses to run without.
|
|
77
|
+
*/
|
|
78
|
+
function debitOffered(config: CheckoutProviderConfig | null, card: boolean, hasBillingAddress: boolean): boolean {
|
|
79
|
+
return card && hasBillingAddress && config?.chain?.[0]?.debit === true;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
/** {@link useListCapability}'s answer, from what it read. */
|
|
75
83
|
function capabilityOf(
|
|
76
84
|
config: CheckoutProviderConfig | null,
|
|
77
85
|
client: GooglePaymentsClient | null,
|
|
78
86
|
price: { priced: boolean; repricing: boolean },
|
|
79
87
|
port: ListApplePay["validateMerchant"] | undefined,
|
|
88
|
+
hasBillingAddress: boolean,
|
|
80
89
|
): ListCapability {
|
|
81
90
|
const offered = offeredMethods(config);
|
|
82
91
|
const cardUnavailable = !cardPathAvailable(config);
|
|
@@ -89,6 +98,7 @@ function capabilityOf(
|
|
|
89
98
|
handOff,
|
|
90
99
|
offered,
|
|
91
100
|
cardUnavailable,
|
|
101
|
+
debit: debitOffered(config, card, hasBillingAddress),
|
|
92
102
|
wallets: walletsShown(card && (price.priced || price.repricing), google, apple),
|
|
93
103
|
googlePay: live ? google : null,
|
|
94
104
|
applePay: live ? apple : null,
|
|
@@ -114,10 +124,12 @@ export function useListCapability(
|
|
|
114
124
|
/** The host's price for the sheets — no price (or zero), no wallet. */
|
|
115
125
|
totalCents: number | undefined,
|
|
116
126
|
validateApplePayMerchant?: ListApplePay["validateMerchant"],
|
|
127
|
+
/** The host gave the billing address a debit card's 3-D Secure needs. */
|
|
128
|
+
hasBillingAddress = false,
|
|
117
129
|
): ListCapability {
|
|
118
|
-
const declared = googlePayConfig(config)
|
|
130
|
+
const declared = googlePayConfig(config);
|
|
119
131
|
// `null` tells the client hook not to load pay.js at all.
|
|
120
|
-
const client = useGooglePayClient(declared ? undefined : null, "TEST");
|
|
132
|
+
const client = useGooglePayClient(declared ? undefined : null, declared?.environment ?? "TEST");
|
|
121
133
|
const { priced, repricing } = useRepricing(totalCents);
|
|
122
134
|
// The port is read at the press, through a ref: a host passing a new
|
|
123
135
|
// function each render must not rebuild the capability (and the wallets'
|
|
@@ -130,7 +142,7 @@ export function useListCapability(
|
|
|
130
142
|
return current ? current(validationURL) : Promise.reject(new Error("no merchant validation port"));
|
|
131
143
|
}, []);
|
|
132
144
|
return useMemo(
|
|
133
|
-
() => capabilityOf(config, client, { priced, repricing }, hasPort ? stablePort : undefined),
|
|
134
|
-
[config, client, priced, repricing, hasPort, stablePort],
|
|
145
|
+
() => capabilityOf(config, client, { priced, repricing }, hasPort ? stablePort : undefined, hasBillingAddress),
|
|
146
|
+
[config, client, priced, repricing, hasPort, stablePort, hasBillingAddress],
|
|
135
147
|
);
|
|
136
148
|
}
|
|
@@ -92,6 +92,25 @@ function reopenedSelection(cards: readonly SavedCard[], fresh: boolean): string
|
|
|
92
92
|
return fresh ? null : head.id;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The card selection a choice opens on, or `undefined` when it opens no card.
|
|
97
|
+
* Coming back to the card from another option starts on the head of the saved
|
|
98
|
+
* list again (FN:420), or on the fields when there is none; what was typed in
|
|
99
|
+
* them is kept. The debit row always types one: a saved card is charged as
|
|
100
|
+
* credit (FUT-3754).
|
|
101
|
+
*/
|
|
102
|
+
function cardOpenedBy(
|
|
103
|
+
previous: CheckoutListChoice | null,
|
|
104
|
+
next: CheckoutListChoice,
|
|
105
|
+
cards: readonly SavedCard[],
|
|
106
|
+
fresh: boolean,
|
|
107
|
+
): string | null | undefined {
|
|
108
|
+
if (next.kind !== "online" || next.method !== "CARD") return undefined;
|
|
109
|
+
const sameRow = previous?.kind === "online" && previous.method === "CARD" && previous.funding === next.funding;
|
|
110
|
+
if (sameRow) return undefined;
|
|
111
|
+
return next.funding === "DEBIT" ? NEW_CARD : reopenedSelection(cards, fresh);
|
|
112
|
+
}
|
|
113
|
+
|
|
95
114
|
/**
|
|
96
115
|
* The one list's state (FUT-3591). Picking an option changes THIS and nothing
|
|
97
116
|
* else: no order, no charge, no request — the bar's button is the only thing
|
|
@@ -133,20 +152,20 @@ export function useListState(input: {
|
|
|
133
152
|
(next: CheckoutListChoice) => {
|
|
134
153
|
touched.current = true;
|
|
135
154
|
setChoice((previous) => {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// what was typed in them is kept.
|
|
139
|
-
const reopensCard = next.kind === "online" && next.method === "CARD" && !(previous?.kind === "online" && previous.method === "CARD");
|
|
140
|
-
if (reopensCard) setSelection(reopenedSelection(cards, input.fresh));
|
|
155
|
+
const selection = cardOpenedBy(previous, next, cards, input.fresh);
|
|
156
|
+
if (selection !== undefined) setSelection(selection);
|
|
141
157
|
return next;
|
|
142
158
|
});
|
|
143
159
|
},
|
|
144
160
|
[cards, setSelection, input.fresh],
|
|
145
161
|
);
|
|
146
162
|
// A choice the chain no longer offers (a config that resolved to a hand-off
|
|
147
|
-
// store after the card was preselected) is no choice at all.
|
|
163
|
+
// store after the card was preselected) is no choice at all. Matched on the
|
|
164
|
+
// funding too: a debit row that went away (its billing address with it) must
|
|
165
|
+
// not leave a choice the card charge would run as CREDIT (FUT-3754).
|
|
148
166
|
useEffect(() => {
|
|
149
|
-
if (choice?.kind
|
|
167
|
+
if (choice?.kind !== "online") return;
|
|
168
|
+
if (!rows.some((row) => row.method === choice.method && row.funding === choice.funding && !row.disabled)) setChoice(null);
|
|
150
169
|
}, [choice, rows]);
|
|
151
170
|
const { onChoiceChange } = input;
|
|
152
171
|
useEffect(() => {
|
|
@@ -158,13 +158,15 @@ function toCardLink(link: CheckoutChainLink): CardChainLink {
|
|
|
158
158
|
*/
|
|
159
159
|
export function googlePayConfig(
|
|
160
160
|
config: CheckoutProviderConfig | null,
|
|
161
|
-
): { gateway: string; gatewayMerchantId: string } | null {
|
|
161
|
+
): { gateway: string; gatewayMerchantId: string; environment: "TEST" | "PRODUCTION" } | null {
|
|
162
162
|
const head = config?.chain?.[0];
|
|
163
163
|
if (!head?.methods.includes("CARD")) return null;
|
|
164
164
|
if (!head.wallets?.includes("GOOGLE_PAY")) return null;
|
|
165
165
|
const params = head.googlePay;
|
|
166
166
|
if (!params?.gatewayMerchantId) return null;
|
|
167
|
-
|
|
167
|
+
// The connection's own environment (FUT-3756): a production store must never
|
|
168
|
+
// mint Google's test tokens. Absent ⇒ TEST, as before it was published.
|
|
169
|
+
return { gateway: params.gateway, gatewayMerchantId: params.gatewayMerchantId, environment: params.environment ?? "TEST" };
|
|
168
170
|
}
|
|
169
171
|
|
|
170
172
|
/**
|
|
@@ -20,6 +20,7 @@ import type { BuyerVaultSession, VaultedCardDisplay } from "@12-apps/payments-ba
|
|
|
20
20
|
import type { SavedCard } from "../../card";
|
|
21
21
|
import { err, ok, type Result } from "../../result";
|
|
22
22
|
|
|
23
|
+
import type { ThreeDSecureSessionView } from "./debit-types";
|
|
23
24
|
import type { CheckoutTransportCopy } from "./screens-copy";
|
|
24
25
|
|
|
25
26
|
import type {
|
|
@@ -127,6 +128,11 @@ export interface CheckoutClient {
|
|
|
127
128
|
*/
|
|
128
129
|
completeVault(input: CompleteVaultInput): Promise<Result<VaultedCardDisplay>>;
|
|
129
130
|
refreshBrowserKey(input: { orderId: string }): Promise<Result<{ publicKey: string | null }>>;
|
|
131
|
+
/**
|
|
132
|
+
* `POST /3ds-session` (FUT-3754): the 3-D Secure session a debit card is
|
|
133
|
+
* authenticated on. Optional — a host without the row offers no debit.
|
|
134
|
+
*/
|
|
135
|
+
threeDSecureSession?(input: { orderId: string }): Promise<Result<ThreeDSecureSessionView>>;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/** Envelope the API routes return: `{ data }` on success, `{ error }` on failure. */
|
|
@@ -192,6 +198,11 @@ function returnedSettlement(): Record<string, string> {
|
|
|
192
198
|
};
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
/** A POST naming the payable — the body every payable-scoped row takes. */
|
|
202
|
+
function orderBody(input: { orderId: string }): RequestInit {
|
|
203
|
+
return { method: "POST", body: JSON.stringify({ orderId: input.orderId }) };
|
|
204
|
+
}
|
|
205
|
+
|
|
195
206
|
/** The ambient `fetch`, wrapped so it is never invoked detached from its global. */
|
|
196
207
|
function ambientFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
197
208
|
return globalThis.fetch(input, init);
|
|
@@ -211,6 +222,8 @@ function flatChargeBody(input: ChargeCardInput): string {
|
|
|
211
222
|
saveCard: input.saveCard,
|
|
212
223
|
cardMeta: input.cardMeta,
|
|
213
224
|
taxId: input.taxId,
|
|
225
|
+
...(input.funding ? { funding: input.funding } : {}),
|
|
226
|
+
...(input.authenticationId ? { authenticationId: input.authenticationId } : {}),
|
|
214
227
|
});
|
|
215
228
|
}
|
|
216
229
|
|
|
@@ -321,10 +334,8 @@ export function createCheckoutClient(transport: CheckoutTransport): CheckoutClie
|
|
|
321
334
|
body: completeVaultBody(input),
|
|
322
335
|
}),
|
|
323
336
|
|
|
324
|
-
refreshBrowserKey: (input) =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
body: JSON.stringify({ orderId: input.orderId }),
|
|
328
|
-
}),
|
|
337
|
+
refreshBrowserKey: (input) => call<{ publicKey: string | null }>("/refresh-key", orderBody(input)),
|
|
338
|
+
|
|
339
|
+
threeDSecureSession: (input) => call<ThreeDSecureSessionView>("/3ds-session", orderBody(input)),
|
|
329
340
|
};
|
|
330
341
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* this folder. The host-side halves — the cart, the catalog, order CREATION — stay in the
|
|
8
8
|
* host app and reach the flow through the ports declared on `CheckoutFlowProps`.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
import type { DebitChargeFields, DebitDeclaration } from "./debit-types";
|
|
11
11
|
import type { CheckoutDecline, CheckoutDeclineReason } from "./decline";
|
|
12
12
|
|
|
13
13
|
/** Payment methods offered at checkout. Mirrors `Payment.method` (FUT-42). */
|
|
@@ -215,7 +215,7 @@ export interface SavedCardMeta {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/** Input to charge a tokenized card against an order. */
|
|
218
|
-
export interface ChargeCardInput {
|
|
218
|
+
export interface ChargeCardInput extends DebitChargeFields {
|
|
219
219
|
orderId: string;
|
|
220
220
|
/** A fresh token, or a saved card's id for reuse. */
|
|
221
221
|
token: string;
|
|
@@ -271,7 +271,7 @@ export interface CheckoutCustomerField {
|
|
|
271
271
|
* answers it. The same protocol facts as the head, stated per provider —
|
|
272
272
|
* which is what lets the card path mint an instrument for each of them.
|
|
273
273
|
*/
|
|
274
|
-
export interface CheckoutChainLink {
|
|
274
|
+
export interface CheckoutChainLink extends DebitDeclaration {
|
|
275
275
|
provider: string;
|
|
276
276
|
/**
|
|
277
277
|
* The provider's own name as a BUYER should read it ("InfinitePay"), which
|
|
@@ -322,7 +322,7 @@ export interface CheckoutChainLink {
|
|
|
322
322
|
* — the button must not render, because a token minted against a missing id
|
|
323
323
|
* charges nobody.
|
|
324
324
|
*/
|
|
325
|
-
googlePay?: { gateway: string; gatewayMerchantId: string | null } | null;
|
|
325
|
+
googlePay?: { gateway: string; gatewayMerchantId: string | null; environment?: "TEST" | "PRODUCTION" } | null;
|
|
326
326
|
/**
|
|
327
327
|
* The buyer screen THIS provider's flow needs (FUT-596) — an opaque id the
|
|
328
328
|
* adapter declares and `providers/registry.ts` resolves to a component.
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
type CardTokenizationConfig,
|
|
15
15
|
type SavedCard,
|
|
16
16
|
} from "../../card";
|
|
17
|
-
import { ok, type Result } from "../../result";
|
|
17
|
+
import { err, ok, type Result } from "../../result";
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
20
|
resolveNewCardToken,
|
|
@@ -27,7 +27,9 @@ import { useCheckoutClientApi } from "./client-context";
|
|
|
27
27
|
import { useCheckoutNavigate } from "./navigate-context";
|
|
28
28
|
import type { CardChainLink } from "./method-capability";
|
|
29
29
|
import { useOneClickArmed, useOneClickPay } from "./one-click";
|
|
30
|
-
import type { BuyerInfo, CheckoutOrder, OnCheckoutResolved, OrderStatus } from "./types";
|
|
30
|
+
import type { BuyerInfo, ChargeCardInput, CheckoutOrder, OnCheckoutResolved, OrderStatus } from "./types";
|
|
31
|
+
import { authenticateDebit, type DebitAuthentication } from "./debit-charge";
|
|
32
|
+
import type { CheckoutClient } from "./transport";
|
|
31
33
|
import { usePaymentPolling } from "./use-payment-polling";
|
|
32
34
|
import type { CardCopy } from "../../card/copy";
|
|
33
35
|
import { useCheckoutCopy } from "./copy-context";
|
|
@@ -80,6 +82,32 @@ interface CardFormState {
|
|
|
80
82
|
saveCard: boolean;
|
|
81
83
|
validate: () => CardFieldErrors;
|
|
82
84
|
setFieldErrors: React.Dispatch<React.SetStateAction<CardFieldErrors>>;
|
|
85
|
+
/** Charge the card as DEBIT, after its 3-D Secure (FUT-3754). */
|
|
86
|
+
debit?: DebitAuthentication;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The charge's body. A DEBIT card (FUT-3754) is 3-D Secure authenticated first
|
|
91
|
+
* and charged with the head's own instrument only — the authentication is that
|
|
92
|
+
* provider's — so no per-provider map, and nothing saved.
|
|
93
|
+
*/
|
|
94
|
+
async function chargeBodyOf(
|
|
95
|
+
form: CardFormState,
|
|
96
|
+
instruments: CardInstruments,
|
|
97
|
+
ctx: { client: CheckoutClient; order: CheckoutOrder; buyer: BuyerInfo },
|
|
98
|
+
): Promise<Result<ChargeCardInput>> {
|
|
99
|
+
const body: ChargeCardInput = {
|
|
100
|
+
orderId: ctx.order.orderId,
|
|
101
|
+
token: instruments.token,
|
|
102
|
+
tokensByProvider: instruments.tokensByProvider,
|
|
103
|
+
saveCard: form.usingNewCard && form.saveCard,
|
|
104
|
+
cardMeta: instruments.cardMeta,
|
|
105
|
+
taxId: ctx.buyer.taxId,
|
|
106
|
+
};
|
|
107
|
+
if (!form.debit) return ok(body);
|
|
108
|
+
const authenticated = await authenticateDebit({ ...ctx, encryptedCard: instruments.token, debit: form.debit });
|
|
109
|
+
if (!authenticated.ok) return authenticated;
|
|
110
|
+
return ok({ ...body, tokensByProvider: undefined, saveCard: false, funding: "DEBIT", authenticationId: authenticated.data });
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
/** The submit slice of {@link useCardCheckout}. */
|
|
@@ -223,20 +251,13 @@ export function useCardSubmit(
|
|
|
223
251
|
refreshKey: client.refreshBrowserKey,
|
|
224
252
|
copy: cardCopy,
|
|
225
253
|
});
|
|
226
|
-
|
|
227
|
-
|
|
254
|
+
const body = resolved.ok ? await chargeBodyOf(form, resolved.data, { client, order, buyer }) : err<ChargeCardInput>(resolved.error);
|
|
255
|
+
if (!body.ok) {
|
|
256
|
+
setError(body.error);
|
|
228
257
|
setSubmitting(false);
|
|
229
258
|
return;
|
|
230
259
|
}
|
|
231
|
-
|
|
232
|
-
const charged = await client.charge({
|
|
233
|
-
orderId: order.orderId,
|
|
234
|
-
token: resolved.data.token,
|
|
235
|
-
tokensByProvider: resolved.data.tokensByProvider,
|
|
236
|
-
saveCard: form.usingNewCard && form.saveCard,
|
|
237
|
-
cardMeta: resolved.data.cardMeta,
|
|
238
|
-
taxId: buyer.taxId,
|
|
239
|
-
});
|
|
260
|
+
const charged = await client.charge(body.data);
|
|
240
261
|
if (!charged.ok) {
|
|
241
262
|
setError(charged.error); // transport/validation problem — stay on the form
|
|
242
263
|
// An UNRESOLVED charge is not a decline: some provider may be holding the
|
|
@@ -128,6 +128,7 @@ function WalletButtons({
|
|
|
128
128
|
<GooglePayButton
|
|
129
129
|
order={props.order}
|
|
130
130
|
params={googlePay}
|
|
131
|
+
environment={googlePay.environment}
|
|
131
132
|
onKey={(key) => void wallet.payWithKey("GOOGLE_PAY", key)}
|
|
132
133
|
onError={onSheetError}
|
|
133
134
|
onReady={() => setGoogleReady(true)}
|
|
@@ -4,12 +4,16 @@ import { Alert, Box, Button, LinearProgress, Stack, Typography } from '@mui/mate
|
|
|
4
4
|
import { useState, type ReactNode } from 'react';
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
|
+
BrowserCardFailure,
|
|
7
8
|
BrowserCardResult,
|
|
9
|
+
DebitTestData,
|
|
8
10
|
EvidenceSummary,
|
|
9
11
|
PaymentEnvironment,
|
|
12
|
+
ThreeDSecureSession,
|
|
10
13
|
} from '@12-apps/payments-backend';
|
|
11
14
|
|
|
12
|
-
import { maskTestCard, PAGBANK_TEST_CARDS } from '../../card/pagbank-test-cards';
|
|
15
|
+
import { maskTestCard, PAGBANK_3DS_TEST_CARD, PAGBANK_TEST_CARDS } from '../../card/pagbank-test-cards';
|
|
16
|
+
import { authenticatePagBankDebit, type ThreeDSFailure } from '../../card/three-ds';
|
|
13
17
|
import { encryptPagBankCard } from '../../card/tokenize';
|
|
14
18
|
import { CARD_SX, TAP_SX } from './ConnectEnvironmentCard';
|
|
15
19
|
import type { PlatformHomologacaoCopy } from './copy';
|
|
@@ -21,7 +25,8 @@ import { PlatformCopyProvider, usePlatformCopy } from './copy-context';
|
|
|
21
25
|
*
|
|
22
26
|
* The one thing this card does itself is the part that has to happen in a
|
|
23
27
|
* browser: encrypting PagBank's official TEST cards with the seller's public
|
|
24
|
-
* key, through the same SDK call the checkout makes
|
|
28
|
+
* key, through the same SDK call the checkout makes, and authenticating the
|
|
29
|
+
* 3DS test card with PagBank's SDK for the debit scenario (FUT-3755). Everything else is the
|
|
25
30
|
* host's — each callback is a call to its own mounted routes — and what the
|
|
26
31
|
* file says is decided by the server, from what was recorded. This card shows
|
|
27
32
|
* the counts it answered and never infers a result of its own.
|
|
@@ -47,8 +52,18 @@ export interface HomologacaoEvidenceCardProps {
|
|
|
47
52
|
connectOutcome?: 'connected' | 'failed' | null;
|
|
48
53
|
/** Start the authorization: the host mints the state and navigates away. */
|
|
49
54
|
onConnect: () => Promise<void>;
|
|
50
|
-
/**
|
|
51
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Phase 1 — the seller's public key and the debit card's 3DS session, each
|
|
57
|
+
* null when it could not be minted (the session absent on a host that does
|
|
58
|
+
* not mint one yet: the debit scenarios then stay pending, saying why).
|
|
59
|
+
*/
|
|
60
|
+
onPrepare: () => Promise<{
|
|
61
|
+
runId: string;
|
|
62
|
+
publicKey: string | null;
|
|
63
|
+
threeDSSession?: ThreeDSecureSession | null;
|
|
64
|
+
/** What the debit card is authenticated with — the server's, so the amount matches what it charges. */
|
|
65
|
+
debitTest?: DebitTestData;
|
|
66
|
+
}>;
|
|
52
67
|
/** Phase 2 — raise the orders with the encrypted cards. */
|
|
53
68
|
onRun: (runId: string, cards: BrowserCardResult[]) => Promise<void>;
|
|
54
69
|
/** Build and deliver the file; resolves to the counts it holds. */
|
|
@@ -58,29 +73,69 @@ export interface HomologacaoEvidenceCardProps {
|
|
|
58
73
|
type Phase = 'idle' | 'preparing' | 'encrypting' | 'running' | 'downloading';
|
|
59
74
|
type ActivePhase = Exclude<Phase, 'idle'>;
|
|
60
75
|
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
type TestCard = typeof PAGBANK_3DS_TEST_CARD;
|
|
77
|
+
|
|
78
|
+
/** One official test card, encrypted — or the code saying why it is not. */
|
|
79
|
+
async function encryptTestCard(card: TestCard, publicKey: string | null): Promise<BrowserCardResult> {
|
|
80
|
+
const base = { scenario: card.scenario, brand: card.brand, masked: maskTestCard(card.number) };
|
|
81
|
+
if (!publicKey) return { ...base, encrypted: null, failure: 'NO_PUBLIC_KEY' };
|
|
82
|
+
const encrypted = await encryptPagBankCard({
|
|
83
|
+
publicKey,
|
|
84
|
+
holder: card.holder,
|
|
85
|
+
number: card.number,
|
|
86
|
+
expMonth: card.expMonth,
|
|
87
|
+
expYear: card.expYear,
|
|
88
|
+
securityCode: card.securityCode,
|
|
89
|
+
});
|
|
90
|
+
return 'encrypted' in encrypted
|
|
91
|
+
? { ...base, encrypted: encrypted.encrypted }
|
|
92
|
+
: { ...base, encrypted: null, failure: encrypted.failure };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The SDK's 3DS failure, as the code the evidence copy words. */
|
|
96
|
+
const THREEDS_FAILURES: Readonly<Record<ThreeDSFailure, BrowserCardFailure>> = {
|
|
97
|
+
SDK_UNAVAILABLE: 'SDK_UNAVAILABLE',
|
|
98
|
+
NOT_SUPPORTED: 'THREEDS_NOT_SUPPORTED',
|
|
99
|
+
REFUSED: 'THREEDS_REFUSED',
|
|
100
|
+
INVALID_DATA: 'THREEDS_FAILED',
|
|
101
|
+
ERROR: 'THREEDS_FAILED',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The 3DS test card, encrypted and authenticated with PagBank's SDK for the
|
|
106
|
+
* amount the server will charge — or the code saying where it stopped. The
|
|
107
|
+
* server charges it as DEBIT only with the authentication id.
|
|
108
|
+
*/
|
|
109
|
+
async function authenticateDebitTestCard(
|
|
110
|
+
publicKey: string | null,
|
|
111
|
+
session: ThreeDSecureSession | null,
|
|
112
|
+
test: DebitTestData | undefined,
|
|
113
|
+
): Promise<BrowserCardResult> {
|
|
114
|
+
const encrypted = await encryptTestCard(PAGBANK_3DS_TEST_CARD, publicKey);
|
|
115
|
+
if (!encrypted.encrypted) return encrypted;
|
|
116
|
+
if (!session || !test) return { ...encrypted, failure: 'NO_THREEDS_SESSION' };
|
|
117
|
+
const outcome = await authenticatePagBankDebit({
|
|
118
|
+
session: session.session,
|
|
119
|
+
environment: session.environment,
|
|
120
|
+
encryptedCard: encrypted.encrypted,
|
|
121
|
+
amountCents: test.amountCents,
|
|
122
|
+
customer: test.customer,
|
|
123
|
+
billingAddress: test.billingAddress,
|
|
124
|
+
// The test card never challenges; a hung SDK must not hold the Pix and
|
|
125
|
+
// credit scenarios back with it.
|
|
126
|
+
timeoutMs: 60_000,
|
|
127
|
+
});
|
|
128
|
+
return outcome.ok
|
|
129
|
+
? { ...encrypted, authenticationId: outcome.authenticationId, authenticated: outcome.authenticated }
|
|
130
|
+
: { ...encrypted, failure: THREEDS_FAILURES[outcome.failure] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Each official test card, encrypted, then the debit one authenticated (FUT-3755). */
|
|
134
|
+
async function prepareTestCards(prepared: Awaited<ReturnType<HomologacaoEvidenceCardProps['onPrepare']>>): Promise<BrowserCardResult[]> {
|
|
135
|
+
const { publicKey } = prepared;
|
|
63
136
|
const results: BrowserCardResult[] = [];
|
|
64
|
-
for (const card of PAGBANK_TEST_CARDS)
|
|
65
|
-
|
|
66
|
-
if (!publicKey) {
|
|
67
|
-
results.push({ ...base, encrypted: null, failure: 'NO_PUBLIC_KEY' });
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const encrypted = await encryptPagBankCard({
|
|
71
|
-
publicKey,
|
|
72
|
-
holder: card.holder,
|
|
73
|
-
number: card.number,
|
|
74
|
-
expMonth: card.expMonth,
|
|
75
|
-
expYear: card.expYear,
|
|
76
|
-
securityCode: card.securityCode,
|
|
77
|
-
});
|
|
78
|
-
results.push(
|
|
79
|
-
'encrypted' in encrypted
|
|
80
|
-
? { ...base, encrypted: encrypted.encrypted }
|
|
81
|
-
: { ...base, encrypted: null, failure: encrypted.failure },
|
|
82
|
-
);
|
|
83
|
-
}
|
|
137
|
+
for (const card of PAGBANK_TEST_CARDS) results.push(await encryptTestCard(card, publicKey));
|
|
138
|
+
results.push(await authenticateDebitTestCard(publicKey, prepared.threeDSSession ?? null, prepared.debitTest));
|
|
84
139
|
return results;
|
|
85
140
|
}
|
|
86
141
|
|
|
@@ -169,7 +224,7 @@ function useEvidenceRun({ onPrepare, onRun, onDownload }: Pick<HomologacaoEviden
|
|
|
169
224
|
enter('preparing');
|
|
170
225
|
const prepared = await onPrepare();
|
|
171
226
|
enter('encrypting');
|
|
172
|
-
const cards = await
|
|
227
|
+
const cards = await prepareTestCards(prepared);
|
|
173
228
|
enter('running');
|
|
174
229
|
await onRun(prepared.runId, cards);
|
|
175
230
|
}
|
|
@@ -123,12 +123,12 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
|
|
|
123
123
|
connectFailed: 'Could not start the test seller connection.',
|
|
124
124
|
returnedConnected: 'Test seller connected. The callback and the code exchange were recorded in the evidence.',
|
|
125
125
|
returnedFailed: 'The test seller connection did not complete. Try connecting again.',
|
|
126
|
-
generate: 'Generate evidence file\u00a0(TXT)',
|
|
126
|
+
generate: 'Generate evidence file\u00a0(TXT and JSON)',
|
|
127
127
|
downloadLatest: 'Download the latest recorded',
|
|
128
128
|
phases: {
|
|
129
|
-
preparing: 'Consulting the application, renewing the token and minting the public key…',
|
|
130
|
-
encrypting: 'Encrypting the test cards in the browser…',
|
|
131
|
-
running: 'Creating the Pix and
|
|
129
|
+
preparing: 'Consulting the application, renewing the token and minting the public key and the 3DS session…',
|
|
130
|
+
encrypting: 'Encrypting the test cards and authenticating the debit card (3DS) in the browser…',
|
|
131
|
+
running: 'Creating the Pix, credit and debit orders in the sandbox — this can take a few minutes…',
|
|
132
132
|
downloading: 'Building the file…',
|
|
133
133
|
},
|
|
134
134
|
summary: ({ executed, failed, pending }) =>
|
|
@@ -139,7 +139,7 @@ export const EN_US_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
|
|
|
139
139
|
`${
|
|
140
140
|
{
|
|
141
141
|
preparing: 'Consulting the application and preparing the test seller failed.',
|
|
142
|
-
encrypting: 'Encrypting the test cards in the browser failed.',
|
|
142
|
+
encrypting: 'Encrypting the test cards or authenticating the debit card in the browser failed.',
|
|
143
143
|
running: 'Creating the orders in the sandbox failed.',
|
|
144
144
|
downloading: 'Building the file failed.',
|
|
145
145
|
}[phase]
|
|
@@ -104,12 +104,12 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
|
|
|
104
104
|
'Lojista de teste conectado. O callback e a troca do código foram registrados nas evidências.',
|
|
105
105
|
returnedFailed:
|
|
106
106
|
'A conexão do lojista de teste não foi concluída. Tente conectar de novo.',
|
|
107
|
-
generate: 'Gerar arquivo de evidências\u00a0(TXT)',
|
|
107
|
+
generate: 'Gerar arquivo de evidências\u00a0(TXT e JSON)',
|
|
108
108
|
downloadLatest: 'Baixar o último registrado',
|
|
109
109
|
phases: {
|
|
110
|
-
preparing: 'Consultando a aplicação, renovando o token e gerando a chave pública…',
|
|
111
|
-
encrypting: 'Criptografando os cartões de teste no navegador…',
|
|
112
|
-
running: 'Criando os pedidos Pix e
|
|
110
|
+
preparing: 'Consultando a aplicação, renovando o token e gerando a chave pública e a sessão 3DS…',
|
|
111
|
+
encrypting: 'Criptografando os cartões de teste e autenticando o cartão de débito (3DS) no navegador…',
|
|
112
|
+
running: 'Criando os pedidos Pix, crédito e débito no sandbox — pode levar alguns minutos…',
|
|
113
113
|
downloading: 'Montando o arquivo…',
|
|
114
114
|
},
|
|
115
115
|
summary: ({ executed, failed, pending }) =>
|
|
@@ -123,7 +123,7 @@ export const PT_BR_PLATFORM_HOMOLOGACAO_COPY: PlatformHomologacaoCopy = {
|
|
|
123
123
|
`${
|
|
124
124
|
{
|
|
125
125
|
preparing: 'Falhou ao consultar a aplicação e preparar o lojista de teste.',
|
|
126
|
-
encrypting: 'Falhou ao criptografar os cartões de teste no navegador.',
|
|
126
|
+
encrypting: 'Falhou ao criptografar os cartões de teste ou autenticar o cartão de débito no navegador.',
|
|
127
127
|
running: 'Falhou ao criar os pedidos no sandbox.',
|
|
128
128
|
downloading: 'Falhou ao montar o arquivo.',
|
|
129
129
|
}[phase]
|
package/src/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ export { PT_BR_CHECKOUT_PAYMENT_COPY } from './components/checkout-payment-pt-BR
|
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
57
|
export { CheckoutFlow, type CheckoutCartView, type CheckoutFlowProps } from './components/checkout/checkout-flow';
|
|
58
58
|
// The Pagamento step as ONE list (FUT-3591/3592) — opt-in via `CheckoutFlowProps.paymentList`.
|
|
59
|
-
export type { CheckoutDoorOption, CheckoutDoorRowControls, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, CheckoutPixBank, PaymentListCopy, PixBankCopy, PixBankId, PixBankName } from './components/checkout/list/types';
|
|
59
|
+
export type { CheckoutBillingAddress, CheckoutDoorOption, CheckoutDoorRowControls, CheckoutListChoice, CheckoutListHold, CheckoutOnlineChoice, CheckoutPaymentList, CheckoutPixBank, PaymentListCopy, PaymentListDebitCopy, PixBankCopy, PixBankId, PixBankName } from './components/checkout/list/types';
|
|
60
60
|
export { PT_BR_PAYMENT_LIST_COPY } from './components/checkout/list/pt-BR';
|
|
61
61
|
export type { BackActionEmphasis } from './components/checkout/payment-status-parts';
|
|
62
62
|
export type { CheckoutDeliverySummary, CheckoutSummaryLine } from './components/checkout/payment-summary';
|