@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.0",
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, rememberHostedOrder } from "./hosted-return";
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,