@12-apps/payments-frontend 3.21.4 → 3.23.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/basket.ts +85 -0
- package/src/components/checkout/card-outcome.ts +81 -0
- package/src/components/checkout/card-view.tsx +62 -22
- package/src/components/checkout/checkout-actions.ts +387 -0
- package/src/components/checkout/checkout-flow.tsx +126 -24
- package/src/components/checkout/checkout-steps.tsx +149 -174
- package/src/components/checkout/checkout-totals.tsx +51 -0
- package/src/components/checkout/client-context.tsx +3 -0
- package/src/components/checkout/confirmation-wait.ts +97 -0
- package/src/components/checkout/dados-step.tsx +141 -0
- package/src/components/checkout/decline.ts +48 -0
- package/src/components/checkout/en-US.ts +41 -0
- package/src/components/checkout/failure-codes.ts +82 -0
- package/src/components/checkout/hosted-return.ts +190 -206
- package/src/components/checkout/hosted-store.ts +291 -0
- package/src/components/checkout/payment-error-panel.tsx +9 -3
- package/src/components/checkout/payment-status-parts.tsx +311 -0
- package/src/components/checkout/payment-status.tsx +69 -264
- package/src/components/checkout/pix-view.tsx +97 -8
- package/src/components/checkout/poll-loop.ts +5 -3
- package/src/components/checkout/providers/types.ts +20 -3
- package/src/components/checkout/pt-BR.ts +42 -0
- package/src/components/checkout/screens-copy.ts +14 -0
- package/src/components/checkout/screens-en-US.ts +1 -0
- package/src/components/checkout/screens-pt-BR.ts +3 -0
- package/src/components/checkout/transport.ts +21 -1
- package/src/components/checkout/types.ts +35 -0
- package/src/components/checkout/use-card-checkout.ts +34 -33
- package/src/components/checkout/use-checkout-controller.ts +68 -274
- package/src/components/checkout/use-hosted-resume.ts +326 -0
- package/src/components/checkout/use-payment-polling.ts +58 -7
- package/src/components/checkout/use-wallet-charge.ts +24 -1
- package/src/components/checkout/view-copy.ts +70 -0
- package/src/components/checkout/wallet-pane.tsx +9 -1
- package/src/flows/catalog-exit.ts +33 -0
- package/src/flows/create-payment-flows.tsx +19 -1
- package/src/flows/pipeline/actions.tsx +104 -0
- package/src/flows/pipeline/admission.ts +55 -0
- package/src/flows/pipeline/context.ts +140 -0
- package/src/flows/pipeline/derive-step.ts +234 -0
- package/src/flows/pipeline/engine-actions.ts +257 -0
- package/src/flows/pipeline/engine-chrome.tsx +123 -0
- package/src/flows/pipeline/engine-state.ts +107 -0
- package/src/flows/pipeline/engine.tsx +377 -0
- package/src/flows/pipeline/methods.ts +71 -0
- package/src/flows/pipeline/refusal-routing.ts +106 -0
- package/src/flows/pipeline/slices.ts +110 -0
- package/src/flows/pipeline/stable-plugins.ts +72 -0
- package/src/flows/pipeline/steps/buyer-steps.tsx +297 -0
- package/src/flows/pipeline/steps/index.ts +54 -0
- package/src/flows/pipeline/steps/pay-steps.tsx +182 -0
- package/src/flows/pipeline/steps/status-step.tsx +41 -0
- package/src/flows/pipeline/types.ts +232 -0
- package/src/flows/public.ts +78 -0
- package/src/flows/screens-hosted.tsx +55 -5
- package/src/flows/screens-pay.tsx +6 -1
- package/src/flows/types.ts +25 -2
- package/src/index.ts +29 -19
|
@@ -217,4 +217,18 @@ export interface CheckoutScreensCopy {
|
|
|
217
217
|
validation: CheckoutValidationCopy;
|
|
218
218
|
/** The step wrapper's own wait, while the charge is being raised. */
|
|
219
219
|
generatingPayment: string;
|
|
220
|
+
/**
|
|
221
|
+
* The pay bar's money caption — "Total · 2 itens".
|
|
222
|
+
*
|
|
223
|
+
* A FUNCTION because it inflects on the count, which is a property of the
|
|
224
|
+
* language rather than of the checkout: one item and two items are not the
|
|
225
|
+
* same word, and they are not the same word in different ways per language.
|
|
226
|
+
*
|
|
227
|
+
* It was a hard-coded Portuguese template inside the component until
|
|
228
|
+
* FUT-1179, which is the ticket about a checkout that never showed the
|
|
229
|
+
* amount — so the fix put that caption on a SECOND step and made a
|
|
230
|
+
* single-locale string render twice. Everything else on both bars had been a
|
|
231
|
+
* host's word since FUT-760; this one had simply been missed.
|
|
232
|
+
*/
|
|
233
|
+
totalCaption(items: number): string;
|
|
220
234
|
}
|
|
@@ -100,4 +100,7 @@ export const PT_BR_CHECKOUT_SCREENS_COPY: CheckoutScreensCopy = {
|
|
|
100
100
|
required: 'Campo obrigatório.',
|
|
101
101
|
},
|
|
102
102
|
generatingPayment: 'Gerando pagamento…',
|
|
103
|
+
// Verbatim what the component rendered before the key existed, so nothing
|
|
104
|
+
// changes for a Brazilian shopper.
|
|
105
|
+
totalCaption: (items) => `Total · ${items} ${items === 1 ? 'item' : 'itens'}`,
|
|
103
106
|
};
|
|
@@ -96,10 +96,20 @@ export interface CheckoutTransport {
|
|
|
96
96
|
export type CheckoutTransportBinding = Omit<CheckoutTransport, "copy"> &
|
|
97
97
|
Partial<Pick<CheckoutTransport, "copy">>;
|
|
98
98
|
|
|
99
|
-
/** The
|
|
99
|
+
/** The nine calls the buyer checkout makes, pre-bound to a {@link CheckoutTransport}. */
|
|
100
100
|
export interface CheckoutClient {
|
|
101
101
|
getConfig(tenantSlug: string): Promise<Result<CheckoutProviderConfig>>;
|
|
102
102
|
getStatus(ref: string): Promise<Result<OrderStatus>>;
|
|
103
|
+
/**
|
|
104
|
+
* `POST /release` (FUT-1146): the buyer says they did not pay, and the
|
|
105
|
+
* charge they were sent away for has no terminal state of its own.
|
|
106
|
+
*
|
|
107
|
+
* Answers the payable's status AFTER the server has re-asked the provider,
|
|
108
|
+
* so a payment that actually succeeded comes back `PAID` and nothing is
|
|
109
|
+
* released — the race against a late webhook resolves in the shopper's
|
|
110
|
+
* favour rather than against it.
|
|
111
|
+
*/
|
|
112
|
+
releaseCheckout(input: { orderId: string }): Promise<Result<OrderStatus>>;
|
|
103
113
|
charge(input: ChargeCardInput): Promise<Result<ChargeOutcome>>;
|
|
104
114
|
/** A wallet instrument against the same `/charge` route (FUT-471/472). */
|
|
105
115
|
chargeWallet(input: ChargeWalletInput): Promise<Result<ChargeOutcome>>;
|
|
@@ -266,6 +276,16 @@ export function createCheckoutClient(transport: CheckoutTransport): CheckoutClie
|
|
|
266
276
|
{ method: "GET" },
|
|
267
277
|
),
|
|
268
278
|
|
|
279
|
+
releaseCheckout: (input) =>
|
|
280
|
+
// The settlement hints ride in the QUERY, exactly as the poll's do: they
|
|
281
|
+
// are the only way a hosted provider can be asked anything at all, so a
|
|
282
|
+
// release must carry them or the server decides "not paid" from a
|
|
283
|
+
// question it was never able to put.
|
|
284
|
+
call<OrderStatus>(`/release?${new URLSearchParams(returnedSettlement()).toString()}`, {
|
|
285
|
+
method: "POST",
|
|
286
|
+
body: JSON.stringify({ orderId: input.orderId }),
|
|
287
|
+
}),
|
|
288
|
+
|
|
269
289
|
charge: (input) =>
|
|
270
290
|
call<ChargeOutcome>("/charge", { method: "POST", body: flatChargeBody(input) }),
|
|
271
291
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* the ports declared on `CheckoutFlowProps`.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { CheckoutDecline, CheckoutDeclineReason } from "./decline";
|
|
13
|
+
|
|
12
14
|
/** Payment methods offered at checkout. Mirrors `Payment.method` (FUT-42). */
|
|
13
15
|
export type PaymentMethod = "PIX" | "CARD";
|
|
14
16
|
|
|
@@ -34,6 +36,16 @@ export type OrderStatus = "AWAITING_PAYMENT" | "PAID" | "FAILED" | "EXPIRED";
|
|
|
34
36
|
/** Terminal states — polling stops once the order reaches one of these. */
|
|
35
37
|
export const TERMINAL_STATUSES: readonly OrderStatus[] = ["PAID", "FAILED", "EXPIRED"];
|
|
36
38
|
|
|
39
|
+
/**
|
|
40
|
+
* What a payment pane calls once the charge has an answer.
|
|
41
|
+
*
|
|
42
|
+
* The refusal rides along OPTIONALLY (FUT-1145): a pane that has one hands it
|
|
43
|
+
* over so the confirmation screen can say which refusal this was and whether
|
|
44
|
+
* another instrument could work, and every caller that has nothing to add keeps
|
|
45
|
+
* calling this with one argument exactly as before.
|
|
46
|
+
*/
|
|
47
|
+
export type OnCheckoutResolved = (status: OrderStatus, decline?: CheckoutDecline | null) => void;
|
|
48
|
+
|
|
37
49
|
/** Buyer contact captured at checkout. */
|
|
38
50
|
export interface BuyerInfo {
|
|
39
51
|
name?: string;
|
|
@@ -173,6 +185,17 @@ export interface CreateOrderRequest {
|
|
|
173
185
|
buyer: BuyerInfo;
|
|
174
186
|
/** Opt-in: save buyer name/phone (not CPF — never persisted here) for next-checkout pre-fill. */
|
|
175
187
|
saveProfile: boolean;
|
|
188
|
+
/**
|
|
189
|
+
* WHICH settlement the buyer chose, when it is not one of the two above
|
|
190
|
+
* (FUT-1240).
|
|
191
|
+
*
|
|
192
|
+
* `method` is what the CHAIN can be asked to charge, so a pipeline host's own
|
|
193
|
+
* registered settlement — "pay the courier", "pay the waiter" — cannot be
|
|
194
|
+
* said there, and without this every one of them arrives as the same request.
|
|
195
|
+
* ABSENT for `PIX` and `CARD`, which is what keeps it additive; otherwise the
|
|
196
|
+
* registered `SettlementMethodDescriptor.id` verbatim (`flows/pipeline`).
|
|
197
|
+
*/
|
|
198
|
+
settlementMethod?: string;
|
|
176
199
|
}
|
|
177
200
|
|
|
178
201
|
/** What the flow hands the host's `saveBuyerContact` port on "Continuar". */
|
|
@@ -358,8 +381,20 @@ export interface CheckoutProviderConfig {
|
|
|
358
381
|
* present when the provider demands the buyer finish the charge on ITS page —
|
|
359
382
|
* Stripe's redirect-based 3-D Secure — and the client then hands the buyer
|
|
360
383
|
* over exactly as it does for a redirect provider's link (FUT-556).
|
|
384
|
+
*
|
|
385
|
+
* `declineReason` / `retriable` ride along on a refusal (FUT-1145). The server
|
|
386
|
+
* has classified every acquirer decline since FUT-340 — 33 PagBank codes with
|
|
387
|
+
* issuer sub-reasons, each carrying the vendor's own retry verdict — and then
|
|
388
|
+
* answered `{ status }` and threw the classification away, so an expired card,
|
|
389
|
+
* a stolen card and "attempts exhausted, do not retry" all reached the buyer as
|
|
390
|
+
* "Pagamento não concluído. Você pode tentar novamente."
|
|
391
|
+
*
|
|
392
|
+
* BOTH OPTIONAL, and the degrade direction is today's behaviour: a server that
|
|
393
|
+
* sends neither renders exactly the generic refusal it always did.
|
|
361
394
|
*/
|
|
362
395
|
export interface ChargeOutcome {
|
|
363
396
|
status: OrderStatus;
|
|
364
397
|
hostedCheckoutUrl?: string;
|
|
398
|
+
declineReason?: CheckoutDeclineReason;
|
|
399
|
+
retriable?: boolean;
|
|
365
400
|
}
|
|
@@ -22,12 +22,12 @@ import {
|
|
|
22
22
|
type CardInstruments,
|
|
23
23
|
type RefreshBrowserKey,
|
|
24
24
|
} from "./card-instruments";
|
|
25
|
+
import { handOverToChallenge, reportResolved, type ChallengeScope } from "./card-outcome";
|
|
25
26
|
import { useCheckoutClientApi } from "./client-context";
|
|
26
|
-
import {
|
|
27
|
-
import { useCheckoutNavigate, type CheckoutNavigate } from "./navigate-context";
|
|
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, OrderStatus } from "./types";
|
|
30
|
+
import type { BuyerInfo, CheckoutOrder, OnCheckoutResolved, OrderStatus } from "./types";
|
|
31
31
|
import { usePaymentPolling } from "./use-payment-polling";
|
|
32
32
|
import type { CardCopy } from "../../card/copy";
|
|
33
33
|
import { useCheckoutCopy } from "./copy-context";
|
|
@@ -40,7 +40,17 @@ const EMPTY_CARD: CardDetails = { number: "", holder: "", expiry: "", cvv: "" };
|
|
|
40
40
|
* actually charge are offered. The slug arrives as an argument (the host's
|
|
41
41
|
* routing owns it); absent, the list is unscoped, exactly as before FUT-697.
|
|
42
42
|
*/
|
|
43
|
-
function useSavedCards(
|
|
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
|
+
): {
|
|
44
54
|
savedCards: SavedCard[];
|
|
45
55
|
selection: string;
|
|
46
56
|
setSelection: (id: string) => void;
|
|
@@ -55,12 +65,12 @@ function useSavedCards(tenantSlug: string | undefined): {
|
|
|
55
65
|
if (!active) return;
|
|
56
66
|
setSavedCards(cards);
|
|
57
67
|
const first = cards[0];
|
|
58
|
-
if (first) setSelection(first.id);
|
|
68
|
+
if (first && !freshInstrument) setSelection(first.id);
|
|
59
69
|
});
|
|
60
70
|
return () => {
|
|
61
71
|
active = false;
|
|
62
72
|
};
|
|
63
|
-
}, [tenantSlug, client]);
|
|
73
|
+
}, [tenantSlug, client, freshInstrument]);
|
|
64
74
|
|
|
65
75
|
return { savedCards, selection, setSelection };
|
|
66
76
|
}
|
|
@@ -159,29 +169,12 @@ type CardSubmit = Pick<
|
|
|
159
169
|
* It was 36 healthy polls, which is the same 90 s at the default 2500 ms
|
|
160
170
|
* interval and an unbounded wait at any other — including the one that
|
|
161
171
|
* mattered, where every poll is FAILING and the healthy count never moves.
|
|
162
|
-
* PIX
|
|
163
|
-
*
|
|
172
|
+
* PIX is bounded too since FUT-1170, but by the CHARGE's own expiry rather
|
|
173
|
+
* than by a constant: a code the buyer can still scan is still worth watching,
|
|
174
|
+
* and one that has died is not.
|
|
164
175
|
*/
|
|
165
176
|
const CARD_AWAITING_WAIT_MS = 90_000;
|
|
166
177
|
|
|
167
|
-
/**
|
|
168
|
-
* Hand the buyer to the provider's authentication page (FUT-698) — Stripe's
|
|
169
|
-
* redirect-based 3-D Secure. Park the order and navigate, the same trip a
|
|
170
|
-
* redirect provider's link takes (FUT-556): the return lands back on this
|
|
171
|
-
* checkout route, where the hosted-resume machinery polls the parked order.
|
|
172
|
-
*/
|
|
173
|
-
function handOverToChallenge(
|
|
174
|
-
order: CheckoutOrder,
|
|
175
|
-
url: string,
|
|
176
|
-
navigate: CheckoutNavigate,
|
|
177
|
-
): void {
|
|
178
|
-
// PARK FIRST. The navigation may not come back to a live SPA at all, and a
|
|
179
|
-
// return trip that finds nothing parked lands the buyer on a blank
|
|
180
|
-
// confirmation after they have paid.
|
|
181
|
-
rememberHostedOrder(order);
|
|
182
|
-
navigate(url);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
178
|
/** The buyer's card is invalid — block the submit and show which field. */
|
|
186
179
|
function blockedByForm(form: CardFormState): boolean {
|
|
187
180
|
if (!form.usingNewCard) return false;
|
|
@@ -257,10 +250,13 @@ function useCardSubmit(
|
|
|
257
250
|
order: CheckoutOrder,
|
|
258
251
|
buyer: BuyerInfo,
|
|
259
252
|
providerConfig: CardTokenizationConfig,
|
|
260
|
-
onResolved:
|
|
253
|
+
onResolved: OnCheckoutResolved,
|
|
261
254
|
pollIntervalMs: number,
|
|
262
255
|
form: CardFormState,
|
|
263
256
|
providerChain: readonly CardChainLink[],
|
|
257
|
+
/** WHOSE store and WHICH basket this charge is for — parked with a 3DS
|
|
258
|
+
* hand-off, which is otherwise resumable over any basket anywhere. */
|
|
259
|
+
scope: ChallengeScope,
|
|
264
260
|
): CardSubmit {
|
|
265
261
|
const [submitting, setSubmitting] = useState(false);
|
|
266
262
|
const [submitted, setSubmitted] = useState(false);
|
|
@@ -319,11 +315,11 @@ function useCardSubmit(
|
|
|
319
315
|
// 3-D Secure (FUT-698): the buyer must finish on the provider's page.
|
|
320
316
|
// `submitting` stays true on purpose — the tab is navigating away.
|
|
321
317
|
if (charged.data.hostedCheckoutUrl) {
|
|
322
|
-
return handOverToChallenge(order, charged.data.hostedCheckoutUrl, navigate);
|
|
318
|
+
return handOverToChallenge(order, charged.data.hostedCheckoutUrl, navigate, scope);
|
|
323
319
|
}
|
|
324
|
-
// A business outcome
|
|
325
|
-
// an accepted charge begins polling
|
|
326
|
-
if (charged.data.status !== "AWAITING_PAYMENT")
|
|
320
|
+
// A business outcome shows the status screen, carrying the refusal the
|
|
321
|
+
// server classified (FUT-1145); an accepted charge begins polling.
|
|
322
|
+
if (charged.data.status !== "AWAITING_PAYMENT") reportResolved(charged.data, onResolved);
|
|
327
323
|
else setSubmitted(true);
|
|
328
324
|
};
|
|
329
325
|
|
|
@@ -338,7 +334,7 @@ export function useCardCheckout(
|
|
|
338
334
|
order: CheckoutOrder,
|
|
339
335
|
buyer: BuyerInfo,
|
|
340
336
|
providerConfig: CardTokenizationConfig,
|
|
341
|
-
onResolved:
|
|
337
|
+
onResolved: OnCheckoutResolved,
|
|
342
338
|
pollIntervalMs: number,
|
|
343
339
|
/** The store whose saved cards may be offered (host routing owns the slug). */
|
|
344
340
|
tenantSlug?: string,
|
|
@@ -347,8 +343,12 @@ export function useCardCheckout(
|
|
|
347
343
|
* per entry so a card charge can fail over. Omitted ⇒ the head alone.
|
|
348
344
|
*/
|
|
349
345
|
providerChain: readonly CardChainLink[] = [],
|
|
346
|
+
/** Do not preselect a saved card — the last one was refused (FUT-1145). */
|
|
347
|
+
freshInstrument = false,
|
|
348
|
+
/** WHOSE store and WHICH basket, for the 3-D Secure hand-off's parked order. */
|
|
349
|
+
scope: ChallengeScope = {},
|
|
350
350
|
): CardCheckout {
|
|
351
|
-
const { savedCards, selection, setSelection } = useSavedCards(tenantSlug);
|
|
351
|
+
const { savedCards, selection, setSelection } = useSavedCards(tenantSlug, freshInstrument);
|
|
352
352
|
const [card, setCard] = useState<CardDetails>(EMPTY_CARD);
|
|
353
353
|
const [fieldErrors, setFieldErrors] = useState<CardFieldErrors>({});
|
|
354
354
|
const [saveCard, setSaveCard] = useState(false);
|
|
@@ -373,6 +373,7 @@ export function useCardCheckout(
|
|
|
373
373
|
pollIntervalMs,
|
|
374
374
|
{ card, usingNewCard, selection, saveCard, validate, setFieldErrors },
|
|
375
375
|
providerChain,
|
|
376
|
+
scope,
|
|
376
377
|
);
|
|
377
378
|
// The tap a one-click buyer already made (`./one-click.tsx`). Nothing about
|
|
378
379
|
// the charge differs — this only presses the button, and only while a SAVED
|