@12-apps/payments-frontend 3.24.0 → 3.24.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.24.
|
|
3
|
+
"version": "3.24.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
|
|
@@ -3,9 +3,7 @@ import { useCallback, useEffect, useState, type Dispatch, type SetStateAction }
|
|
|
3
3
|
import { buyerGateError } from "./buyer-gate";
|
|
4
4
|
import type { ConfirmationWait } from "./confirmation-wait";
|
|
5
5
|
import type { CheckoutDecline } from "./decline";
|
|
6
|
-
import { forgetHostedOrder
|
|
7
|
-
import { parkedBasket, type CheckoutBasketIdentity } from "./basket";
|
|
8
|
-
import type { CheckoutNavigate } from "./navigate-context";
|
|
6
|
+
import { forgetHostedOrder } from "./hosted-return";
|
|
9
7
|
import type { CheckoutScreensCopy } from "./screens-copy";
|
|
10
8
|
import type {
|
|
11
9
|
BuyerContact,
|
|
@@ -13,8 +11,6 @@ import type {
|
|
|
13
11
|
BuyerInfo,
|
|
14
12
|
CheckoutCustomerField,
|
|
15
13
|
CheckoutOrder,
|
|
16
|
-
CreateOrderRequest,
|
|
17
|
-
CreateOrderResult,
|
|
18
14
|
OrderStatus,
|
|
19
15
|
PaymentMethod,
|
|
20
16
|
} from "./types";
|
|
@@ -282,106 +278,3 @@ export function useRetryAction(input: {
|
|
|
282
278
|
clearError, decline, order, setDecline, setFinalStatus, setFreshInstrument, setOrder, setStep,
|
|
283
279
|
]);
|
|
284
280
|
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Hand the buyer to a redirect provider's own page, if that is where this
|
|
288
|
-
* charge settles (FUT-556).
|
|
289
|
-
*
|
|
290
|
-
* Called BEFORE the order is stored: storing it first would render the PIX or
|
|
291
|
-
* card step for a provider that returned neither, which is the dead end this
|
|
292
|
-
* fixes.
|
|
293
|
-
*
|
|
294
|
-
* A full navigation rather than the host's router, because the destination is
|
|
295
|
-
* another origin. The return trip comes back to this same checkout route
|
|
296
|
-
* carrying `transaction_nsu` + `slug`, which the status poll already reads.
|
|
297
|
-
*
|
|
298
|
-
* @returns true when the buyer is on their way and the caller must stop.
|
|
299
|
-
*/
|
|
300
|
-
function handOverToProvider(
|
|
301
|
-
order: CheckoutOrder,
|
|
302
|
-
navigate: CheckoutNavigate,
|
|
303
|
-
tenantSlug?: string,
|
|
304
|
-
basket?: CheckoutBasketIdentity,
|
|
305
|
-
): boolean {
|
|
306
|
-
if (!order.hostedCheckoutUrl) return false;
|
|
307
|
-
// PARK FIRST, navigate second. The order is the only thing the return trip
|
|
308
|
-
// has to rehydrate from, and the navigation may tear this SPA down before
|
|
309
|
-
// any later write lands.
|
|
310
|
-
//
|
|
311
|
-
// The STORE goes with it: one tab holds one slot, and on a multi-tenant
|
|
312
|
-
// storefront every store shares an origin. Without the slug, abandoning this
|
|
313
|
-
// hand-off and opening another store's checkout resumed THIS order there.
|
|
314
|
-
//
|
|
315
|
-
// So does the BASKET (FUT-1213): a hand-off nobody completed must not resume
|
|
316
|
-
// itself over the shopper's next basket, and the only way to tell the two
|
|
317
|
-
// apart later is to record which basket this one was raised from.
|
|
318
|
-
rememberHostedOrder(order, {
|
|
319
|
-
tenantSlug,
|
|
320
|
-
basket: parkedBasket(basket),
|
|
321
|
-
handoff: true,
|
|
322
|
-
});
|
|
323
|
-
navigate(order.hostedCheckoutUrl);
|
|
324
|
-
return true;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Raise the order for a chosen method, and decide what happens to it.
|
|
329
|
-
*
|
|
330
|
-
* Three outcomes, in order: a refusal the step renders, a HAND-OFF that leaves
|
|
331
|
-
* this page for the provider's own, and an order raised here.
|
|
332
|
-
*/
|
|
333
|
-
export function useStartPayment(input: {
|
|
334
|
-
buyer: BuyerInfo;
|
|
335
|
-
saveProfile: boolean;
|
|
336
|
-
createOrder: (request: CreateOrderRequest) => Promise<CreateOrderResult>;
|
|
337
|
-
navigate: CheckoutNavigate;
|
|
338
|
-
tenantSlug: string | undefined;
|
|
339
|
-
basket: CheckoutBasketIdentity | undefined;
|
|
340
|
-
failure: { clear: () => void; fail: (next: { message: string; field?: BuyerField | null; code?: string }) => void };
|
|
341
|
-
setCreating: Dispatch<SetStateAction<boolean>>;
|
|
342
|
-
setDecline: Dispatch<SetStateAction<CheckoutDecline | null>>;
|
|
343
|
-
setOrder: Dispatch<SetStateAction<CheckoutOrder | null>>;
|
|
344
|
-
setFinalStatus: Dispatch<SetStateAction<OrderStatus | null>>;
|
|
345
|
-
}): (chosen: PaymentMethod, override?: BuyerInfo) => Promise<void> {
|
|
346
|
-
const { buyer, saveProfile, createOrder, navigate, tenantSlug, basket, failure } = input;
|
|
347
|
-
const { setCreating, setDecline, setOrder, setFinalStatus } = input;
|
|
348
|
-
return useCallback(
|
|
349
|
-
async (chosen: PaymentMethod, override?: BuyerInfo) => {
|
|
350
|
-
failure.clear();
|
|
351
|
-
setDecline(null);
|
|
352
|
-
// THE CHARGE BEING REPLACED IS DROPPED FIRST (FUT-1170), before the raise
|
|
353
|
-
// rather than after it. Raising a payment means whatever was on screen is
|
|
354
|
-
// no longer the one being paid, and a provider round trip is long enough
|
|
355
|
-
// for the difference to matter: "Gerar novo código" left the expired
|
|
356
|
-
// charge mounted, so its own view polled it, got the terminal EXPIRED it
|
|
357
|
-
// was always going to get, and bounced the flow to the confirmation
|
|
358
|
-
// screen — where the new charge then landed with nothing polling it.
|
|
359
|
-
//
|
|
360
|
-
// A no-op on every other caller (the auto-raise, the alternate e-mail and
|
|
361
|
-
// the retry all run with no order held), which is the point: the clear
|
|
362
|
-
// belongs to what raising a payment MEANS, not to the one path that
|
|
363
|
-
// noticed.
|
|
364
|
-
setOrder(null);
|
|
365
|
-
setFinalStatus(null);
|
|
366
|
-
setCreating(true);
|
|
367
|
-
const result = await createOrder({ method: chosen, buyer: override ?? buyer, saveProfile });
|
|
368
|
-
setCreating(false);
|
|
369
|
-
if (!result.ok) {
|
|
370
|
-
failure.fail(result.error);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
if (handOverToProvider(result.data, navigate, tenantSlug, basket)) return;
|
|
374
|
-
// PARKED EVEN THOUGH NOBODY IS LEAVING (FUT-1140). A low-memory phone
|
|
375
|
-
// discards this tab while the shopper is in their bank app, and the SPA
|
|
376
|
-
// that comes back has never heard of the order it raised — so the buyer
|
|
377
|
-
// meets an empty cart and a retry button instead of the confirmation for
|
|
378
|
-
// the payment they just made.
|
|
379
|
-
rememberHostedOrder(result.data, { tenantSlug, basket: parkedBasket(basket) });
|
|
380
|
-
setOrder(result.data);
|
|
381
|
-
},
|
|
382
|
-
[
|
|
383
|
-
buyer, saveProfile, createOrder, failure, navigate, tenantSlug, basket,
|
|
384
|
-
setCreating, setDecline, setOrder, setFinalStatus,
|
|
385
|
-
],
|
|
386
|
-
);
|
|
387
|
-
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useCallback, type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
|
|
3
|
+
import { parkedBasket, type CheckoutBasketIdentity } from "./basket";
|
|
4
|
+
import type { Step } from "./checkout-actions";
|
|
5
|
+
import type { CheckoutDecline } from "./decline";
|
|
6
|
+
import { rememberHostedOrder } from "./hosted-return";
|
|
7
|
+
import type { CheckoutNavigate } from "./navigate-context";
|
|
8
|
+
import type {
|
|
9
|
+
BuyerField,
|
|
10
|
+
BuyerInfo,
|
|
11
|
+
CheckoutOrder,
|
|
12
|
+
CreateOrderRequest,
|
|
13
|
+
CreateOrderResult,
|
|
14
|
+
OrderStatus,
|
|
15
|
+
PaymentMethod,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RAISING the order, and what becomes of it.
|
|
20
|
+
*
|
|
21
|
+
* Split out of `./checkout-actions.ts` when that file reached its 400-line gate
|
|
22
|
+
* — the same seam it was itself split along. What is here is the one moment the
|
|
23
|
+
* flow asks the host for an order and then has to decide, from the answer
|
|
24
|
+
* alone, which of four things is true: it was refused, it is already finished,
|
|
25
|
+
* it is payable somewhere else, or it is payable here.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hand the buyer to a redirect provider's own page, if that is where this
|
|
30
|
+
* charge settles (FUT-556).
|
|
31
|
+
*
|
|
32
|
+
* Called BEFORE the order is stored: storing it first would render the PIX or
|
|
33
|
+
* card step for a provider that returned neither, which is the dead end this
|
|
34
|
+
* fixes.
|
|
35
|
+
*
|
|
36
|
+
* A full navigation rather than the host's router, because the destination is
|
|
37
|
+
* another origin. The return trip comes back to this same checkout route
|
|
38
|
+
* carrying `transaction_nsu` + `slug`, which the status poll already reads.
|
|
39
|
+
*
|
|
40
|
+
* @returns true when the buyer is on their way and the caller must stop.
|
|
41
|
+
*/
|
|
42
|
+
function handOverToProvider(
|
|
43
|
+
order: CheckoutOrder,
|
|
44
|
+
navigate: CheckoutNavigate,
|
|
45
|
+
tenantSlug?: string,
|
|
46
|
+
basket?: CheckoutBasketIdentity,
|
|
47
|
+
): boolean {
|
|
48
|
+
if (!order.hostedCheckoutUrl) return false;
|
|
49
|
+
// PARK FIRST, navigate second. The order is the only thing the return trip
|
|
50
|
+
// has to rehydrate from, and the navigation may tear this SPA down before
|
|
51
|
+
// any later write lands.
|
|
52
|
+
//
|
|
53
|
+
// The STORE goes with it: one tab holds one slot, and on a multi-tenant
|
|
54
|
+
// storefront every store shares an origin. Without the slug, abandoning this
|
|
55
|
+
// hand-off and opening another store's checkout resumed THIS order there.
|
|
56
|
+
//
|
|
57
|
+
// So does the BASKET (FUT-1213): a hand-off nobody completed must not resume
|
|
58
|
+
// itself over the shopper's next basket, and the only way to tell the two
|
|
59
|
+
// apart later is to record which basket this one was raised from.
|
|
60
|
+
rememberHostedOrder(order, {
|
|
61
|
+
tenantSlug,
|
|
62
|
+
basket: parkedBasket(basket),
|
|
63
|
+
handoff: true,
|
|
64
|
+
});
|
|
65
|
+
navigate(order.hostedCheckoutUrl);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Raise the order for a chosen method, and decide what happens to it.
|
|
71
|
+
*
|
|
72
|
+
* FOUR outcomes, in order: a refusal the step renders, an order that came back
|
|
73
|
+
* ALREADY RESOLVED, a HAND-OFF to the provider's own page, and an order raised
|
|
74
|
+
* here to be paid on this one.
|
|
75
|
+
*
|
|
76
|
+
* The resolved case is first because a raise does not always leave something to
|
|
77
|
+
* pay: a host whose buyer spends a stored balance settles a fully covered
|
|
78
|
+
* pedido server-side and answers PAID with no payable at all. That used to fall
|
|
79
|
+
* through and merely `setOrder`, leaving the flow on Pagamento holding a paid
|
|
80
|
+
* pedido — invisible on the PIX/card screen, which polls and resolves itself,
|
|
81
|
+
* and a dead end on the hand-off screen, which polls nothing and so offered a
|
|
82
|
+
* provider a charge that no longer existed. Reading `status` rather than the
|
|
83
|
+
* absence of a payable is what tells that apart from a failed raise.
|
|
84
|
+
*/
|
|
85
|
+
export function useStartPayment(input: {
|
|
86
|
+
buyer: BuyerInfo;
|
|
87
|
+
saveProfile: boolean;
|
|
88
|
+
createOrder: (request: CreateOrderRequest) => Promise<CreateOrderResult>;
|
|
89
|
+
navigate: CheckoutNavigate;
|
|
90
|
+
tenantSlug: string | undefined;
|
|
91
|
+
basket: CheckoutBasketIdentity | undefined;
|
|
92
|
+
failure: { clear: () => void; fail: (next: { message: string; field?: BuyerField | null; code?: string }) => void };
|
|
93
|
+
setCreating: Dispatch<SetStateAction<boolean>>;
|
|
94
|
+
setDecline: Dispatch<SetStateAction<CheckoutDecline | null>>;
|
|
95
|
+
setOrder: Dispatch<SetStateAction<CheckoutOrder | null>>;
|
|
96
|
+
setFinalStatus: Dispatch<SetStateAction<OrderStatus | null>>;
|
|
97
|
+
/** Moves the flow to Confirmação for an order that needs no payment. */
|
|
98
|
+
setStep: Dispatch<SetStateAction<Step>>;
|
|
99
|
+
}): (chosen: PaymentMethod, override?: BuyerInfo) => Promise<void> {
|
|
100
|
+
const { buyer, saveProfile, createOrder, navigate, tenantSlug, basket, failure } = input;
|
|
101
|
+
const { setCreating, setDecline, setOrder, setFinalStatus, setStep } = input;
|
|
102
|
+
return useCallback(
|
|
103
|
+
async (chosen: PaymentMethod, override?: BuyerInfo) => {
|
|
104
|
+
failure.clear();
|
|
105
|
+
setDecline(null);
|
|
106
|
+
// THE CHARGE BEING REPLACED IS DROPPED FIRST (FUT-1170), before the raise
|
|
107
|
+
// rather than after it. Raising a payment means whatever was on screen is
|
|
108
|
+
// no longer the one being paid, and a provider round trip is long enough
|
|
109
|
+
// for the difference to matter: "Gerar novo código" left the expired
|
|
110
|
+
// charge mounted, so its own view polled it, got the terminal EXPIRED it
|
|
111
|
+
// was always going to get, and bounced the flow to the confirmation
|
|
112
|
+
// screen — where the new charge then landed with nothing polling it.
|
|
113
|
+
//
|
|
114
|
+
// A no-op on every other caller (the auto-raise, the alternate e-mail and
|
|
115
|
+
// the retry all run with no order held), which is the point: the clear
|
|
116
|
+
// belongs to what raising a payment MEANS, not to the one path that
|
|
117
|
+
// noticed.
|
|
118
|
+
setOrder(null);
|
|
119
|
+
setFinalStatus(null);
|
|
120
|
+
setCreating(true);
|
|
121
|
+
const result = await createOrder({ method: chosen, buyer: override ?? buyer, saveProfile });
|
|
122
|
+
setCreating(false);
|
|
123
|
+
if (!result.ok) {
|
|
124
|
+
failure.fail(result.error);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// NOTHING LEFT TO PAY — see the docblock. Parked first for the same
|
|
128
|
+
// reason every other raised order is: the confirmation this is about to
|
|
129
|
+
// show has to survive a tab the phone discards on the way to it.
|
|
130
|
+
if (result.data.status !== "AWAITING_PAYMENT") {
|
|
131
|
+
rememberHostedOrder(result.data, { tenantSlug, basket: parkedBasket(basket) });
|
|
132
|
+
setOrder(result.data);
|
|
133
|
+
setFinalStatus(result.data.status);
|
|
134
|
+
setStep("status");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (handOverToProvider(result.data, navigate, tenantSlug, basket)) return;
|
|
138
|
+
// PARKED EVEN THOUGH NOBODY IS LEAVING (FUT-1140). A low-memory phone
|
|
139
|
+
// discards this tab while the shopper is in their bank app, and the SPA
|
|
140
|
+
// that comes back has never heard of the order it raised — so the buyer
|
|
141
|
+
// meets an empty cart and a retry button instead of the confirmation for
|
|
142
|
+
// the payment they just made.
|
|
143
|
+
rememberHostedOrder(result.data, { tenantSlug, basket: parkedBasket(basket) });
|
|
144
|
+
setOrder(result.data);
|
|
145
|
+
},
|
|
146
|
+
[
|
|
147
|
+
buyer, saveProfile, createOrder, failure, navigate, tenantSlug, basket,
|
|
148
|
+
setCreating, setDecline, setOrder, setFinalStatus, setStep,
|
|
149
|
+
],
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
useResumedCheckout,
|
|
10
10
|
useRetryAction,
|
|
11
11
|
useSettledPort,
|
|
12
|
-
useStartPayment,
|
|
13
12
|
type Step,
|
|
14
13
|
} from "./checkout-actions";
|
|
14
|
+
import { useStartPayment } from "./start-payment";
|
|
15
15
|
import { useConfirmationWait } from "./confirmation-wait";
|
|
16
16
|
import { useCheckoutCopy } from "./copy-context";
|
|
17
17
|
|
|
@@ -139,7 +139,7 @@ export function useCheckoutController(
|
|
|
139
139
|
});
|
|
140
140
|
const startPayment = useStartPayment({
|
|
141
141
|
buyer, saveProfile, createOrder, navigate, tenantSlug, basket, failure,
|
|
142
|
-
setCreating, setDecline, setOrder, setFinalStatus,
|
|
142
|
+
setCreating, setDecline, setOrder, setFinalStatus, setStep,
|
|
143
143
|
});
|
|
144
144
|
const { payWithEmail, handleResolved } = useResolutionActions({
|
|
145
145
|
buyer, method, startPayment, setBuyerState, setDecline, setFinalStatus, setStep,
|