@12-apps/payments-frontend 3.23.1 → 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 +1 -1
- package/src/components/checkout/checkout-actions.ts +1 -108
- package/src/components/checkout/checkout-flow.tsx +13 -2
- package/src/components/checkout/payment-status-parts.tsx +38 -2
- package/src/components/checkout/payment-status.tsx +27 -1
- package/src/components/checkout/start-payment.ts +151 -0
- package/src/components/checkout/use-checkout-controller.ts +2 -2
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "3.
|
|
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
|
-
}
|
|
@@ -7,6 +7,7 @@ import { EmptyCart, PaymentStep } from "./checkout-steps";
|
|
|
7
7
|
import { DadosStep } from "./dados-step";
|
|
8
8
|
import { ArrowBackIcon } from "./icons";
|
|
9
9
|
import { PaymentStatus } from "./payment-status";
|
|
10
|
+
import type { BackActionEmphasis } from "./payment-status-parts";
|
|
10
11
|
import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
11
12
|
import { CheckoutCopyProvider } from "./copy-context";
|
|
12
13
|
import { OneClickProvider, useOneClick } from "./one-click";
|
|
@@ -94,6 +95,10 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
94
95
|
validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
95
96
|
/** Host content shown on the paid confirmation (the storefront's install invite). */
|
|
96
97
|
confirmationExtra?: ReactNode;
|
|
98
|
+
/** Host content shown AFTER the confirmation's actions — `PaymentStatusProps.paidFooter`. */
|
|
99
|
+
confirmationFooter?: ReactNode;
|
|
100
|
+
/** Whether the way out leads the paid screen — see {@link BackActionEmphasis}. */
|
|
101
|
+
backActionEmphasis?: BackActionEmphasis;
|
|
97
102
|
/** Design-system slots; unfilled slots render the raw-MUI defaults. */
|
|
98
103
|
components?: Partial<CheckoutComponents>;
|
|
99
104
|
}
|
|
@@ -180,12 +185,16 @@ function StatusStep({
|
|
|
180
185
|
settlement,
|
|
181
186
|
cart,
|
|
182
187
|
confirmationExtra,
|
|
188
|
+
confirmationFooter,
|
|
189
|
+
backActionEmphasis,
|
|
183
190
|
}: {
|
|
184
191
|
copy: CheckoutViewCopy;
|
|
185
192
|
c: ReturnType<typeof useCheckoutController>;
|
|
186
193
|
settlement: SettlementCheckout | null | undefined;
|
|
187
194
|
cart: CheckoutCartView;
|
|
188
195
|
confirmationExtra: ReactNode;
|
|
196
|
+
confirmationFooter: ReactNode;
|
|
197
|
+
backActionEmphasis: BackActionEmphasis | undefined;
|
|
189
198
|
}): JSX.Element {
|
|
190
199
|
return (
|
|
191
200
|
<PaymentStatus
|
|
@@ -197,6 +206,8 @@ function StatusStep({
|
|
|
197
206
|
onRegenerate={() => { c.setStep("payment"); void c.startPayment("PIX"); }}
|
|
198
207
|
onBackToMenu={c.goToMenu}
|
|
199
208
|
paidExtra={confirmationExtra}
|
|
209
|
+
paidFooter={confirmationFooter}
|
|
210
|
+
backActionEmphasis={backActionEmphasis}
|
|
200
211
|
awaitingTimedOut={c.awaitingTimedOut}
|
|
201
212
|
// How the wait behind this screen is going, and the way out of it
|
|
202
213
|
// (FUT-1144). It is the resumed leg's wait for a checkout that came back
|
|
@@ -297,7 +308,7 @@ function PagamentoStep({
|
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Element {
|
|
300
|
-
const { copy, cart, defaultBuyer, settlement, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, oneClick = false, ...ports } = props;
|
|
311
|
+
const { copy, cart, defaultBuyer, settlement, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, confirmationFooter, backActionEmphasis, validateApplePayMerchant, oneClick = false, ...ports } = props;
|
|
301
312
|
// Resolved for NO method on purpose (FUT-595): the Dados step opens before
|
|
302
313
|
// the picker, and the form is filled once — so it asks for the union of what
|
|
303
314
|
// any chain member may need rather than re-opening after the choice. A chain
|
|
@@ -354,7 +365,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
354
365
|
) : null}
|
|
355
366
|
|
|
356
367
|
{c.step === "status" ? (
|
|
357
|
-
<StatusStep copy={copy} c={c} settlement={settlement} cart={cart} confirmationExtra={confirmationExtra} />
|
|
368
|
+
<StatusStep copy={copy} c={c} settlement={settlement} cart={cart} confirmationExtra={confirmationExtra} confirmationFooter={confirmationFooter} backActionEmphasis={backActionEmphasis} />
|
|
358
369
|
) : null}
|
|
359
370
|
</Box>
|
|
360
371
|
);
|
|
@@ -234,6 +234,38 @@ function retryable(decline: CheckoutDecline | null): boolean {
|
|
|
234
234
|
return decline?.retriable !== false;
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
/**
|
|
238
|
+
* How much of the eye the way out asks for on a PAID confirmation.
|
|
239
|
+
*
|
|
240
|
+
* `"primary"` is the default and the historical look: back-to-menu is the only
|
|
241
|
+
* control a settled screen carries, so it wears the solid fill. A host that
|
|
242
|
+
* renders an action of its OWN in {@link PaymentStatusProps.paidExtra} — a
|
|
243
|
+
* route to the order it just raised, say — has two controls on one screen, and
|
|
244
|
+
* only one of them may lead. `"secondary"` is how such a host says which.
|
|
245
|
+
*
|
|
246
|
+
* A prop rather than a guess, because the package cannot make it: whether the
|
|
247
|
+
* buyer's next move is "look at the pedido" or "buy something else" is a fact
|
|
248
|
+
* about the way this shop serves people, and nothing in a payment knows it.
|
|
249
|
+
* Every other status is unaffected — an unsettled screen already paints this
|
|
250
|
+
* button as the quiet way out, and the retry beside it is the lead.
|
|
251
|
+
*/
|
|
252
|
+
export type BackActionEmphasis = "primary" | "secondary";
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The back-to-menu button's look, from the outcome and the host's emphasis.
|
|
256
|
+
*
|
|
257
|
+
* Its own function so the two attributes cannot disagree: they moved together
|
|
258
|
+
* as a pair of inline ternaries, and a third condition would have made that
|
|
259
|
+
* four places to keep in step.
|
|
260
|
+
*/
|
|
261
|
+
function backLook(
|
|
262
|
+
status: OrderStatus,
|
|
263
|
+
emphasis: BackActionEmphasis,
|
|
264
|
+
): { variant: "solid" | "outline"; color: "primary" | "neutral" } {
|
|
265
|
+
const leads = status === "PAID" && emphasis === "primary";
|
|
266
|
+
return leads ? { variant: "solid", color: "primary" } : { variant: "outline", color: "neutral" };
|
|
267
|
+
}
|
|
268
|
+
|
|
237
269
|
/** The next-action row: retry / regenerate / check-again, always back-to-menu. */
|
|
238
270
|
export function StatusActions({
|
|
239
271
|
copy,
|
|
@@ -244,6 +276,7 @@ export function StatusActions({
|
|
|
244
276
|
onCheckAgain,
|
|
245
277
|
onNotPaid,
|
|
246
278
|
onBackToMenu,
|
|
279
|
+
backActionEmphasis = "primary",
|
|
247
280
|
}: {
|
|
248
281
|
copy: PaymentStatusCopy;
|
|
249
282
|
status: OrderStatus;
|
|
@@ -259,8 +292,11 @@ export function StatusActions({
|
|
|
259
292
|
/** The buyer's "I did not pay" (FUT-1146) — present only while it applies. */
|
|
260
293
|
onNotPaid?: () => void;
|
|
261
294
|
onBackToMenu: () => void;
|
|
295
|
+
/** Whether back-to-menu leads a PAID screen. See {@link BackActionEmphasis}. */
|
|
296
|
+
backActionEmphasis?: BackActionEmphasis;
|
|
262
297
|
}): JSX.Element {
|
|
263
298
|
const { Button } = useCheckoutComponents();
|
|
299
|
+
const back = backLook(status, backActionEmphasis);
|
|
264
300
|
return (
|
|
265
301
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
|
266
302
|
{onCheckAgain ? (
|
|
@@ -298,8 +334,8 @@ export function StatusActions({
|
|
|
298
334
|
<Button
|
|
299
335
|
// Full width and last, so the thumb lands on the same place in every
|
|
300
336
|
// outcome instead of hunting a button that moves with the state.
|
|
301
|
-
variant={
|
|
302
|
-
color={
|
|
337
|
+
variant={back.variant}
|
|
338
|
+
color={back.color}
|
|
303
339
|
size="lg"
|
|
304
340
|
onClick={onBackToMenu}
|
|
305
341
|
dataTestId="payment-back-to-menu"
|
|
@@ -2,7 +2,13 @@ import { Box } from "@mui/material";
|
|
|
2
2
|
import type { JSX, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import type { CheckoutDecline } from "./decline";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
OutcomeHero,
|
|
7
|
+
PaidFacts,
|
|
8
|
+
StatusActions,
|
|
9
|
+
type BackActionEmphasis,
|
|
10
|
+
type WaitState,
|
|
11
|
+
} from "./payment-status-parts";
|
|
6
12
|
import type { OrderStatus } from "./types";
|
|
7
13
|
import { useCheckoutComponents } from "./ui";
|
|
8
14
|
import type { PaymentStatusCopy } from "./view-copy";
|
|
@@ -50,6 +56,23 @@ interface PaymentStatusProps {
|
|
|
50
56
|
* is the difference between an offer and an interruption.
|
|
51
57
|
*/
|
|
52
58
|
paidExtra?: ReactNode;
|
|
59
|
+
/**
|
|
60
|
+
* Host content rendered AFTER the action row, and ONLY on PAID.
|
|
61
|
+
*
|
|
62
|
+
* The other half of {@link paidExtra}, and the difference between them is the
|
|
63
|
+
* actions. What a buyer has to DO next belongs beside the way out, in one
|
|
64
|
+
* block: a panel dropped between two controls splits the only decision on the
|
|
65
|
+
* screen in half. What is merely OFFERED — an install invite, an ask to turn
|
|
66
|
+
* on alerts — belongs after that block, because it is about everything that
|
|
67
|
+
* happens once this screen is closed and must not come between a shopper and
|
|
68
|
+
* the order they just paid for.
|
|
69
|
+
*/
|
|
70
|
+
paidFooter?: ReactNode;
|
|
71
|
+
/**
|
|
72
|
+
* Whether back-to-menu leads a PAID screen (default) or stands down for an
|
|
73
|
+
* action the host renders in {@link paidExtra}. See {@link BackActionEmphasis}.
|
|
74
|
+
*/
|
|
75
|
+
backActionEmphasis?: BackActionEmphasis;
|
|
53
76
|
/**
|
|
54
77
|
* The wait has been given up on. Only meaningful while AWAITING_PAYMENT;
|
|
55
78
|
* every other status has already resolved, so a stale flag cannot change what
|
|
@@ -180,7 +203,10 @@ export function PaymentStatus(props: PaymentStatusProps): JSX.Element {
|
|
|
180
203
|
onCheckAgain={view.checkAgain}
|
|
181
204
|
onNotPaid={view.notPaid}
|
|
182
205
|
onBackToMenu={onBackToMenu}
|
|
206
|
+
backActionEmphasis={props.backActionEmphasis}
|
|
183
207
|
/>
|
|
208
|
+
|
|
209
|
+
{view.paid ? props.paidFooter : null}
|
|
184
210
|
</Box>
|
|
185
211
|
);
|
|
186
212
|
}
|
|
@@ -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,
|
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ export {
|
|
|
59
59
|
type CheckoutCartView,
|
|
60
60
|
type CheckoutFlowProps,
|
|
61
61
|
} from './components/checkout/checkout-flow';
|
|
62
|
+
export type { BackActionEmphasis } from './components/checkout/payment-status-parts';
|
|
62
63
|
|
|
63
64
|
// ---------------------------------------------------------------------------
|
|
64
65
|
// The MOUNTED buyer checkout (FUT-741) and the checkout PIPELINE (FUT-1240).
|