@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
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import { useCallback,
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import type { CheckoutBasketIdentity } from "./basket";
|
|
4
|
+
import {
|
|
5
|
+
resumeSurface,
|
|
6
|
+
useCheckoutNav,
|
|
7
|
+
useCreateFailure,
|
|
8
|
+
useGoToPayment,
|
|
9
|
+
useResumedCheckout,
|
|
10
|
+
useRetryAction,
|
|
11
|
+
useSettledPort,
|
|
12
|
+
useStartPayment,
|
|
13
|
+
type Step,
|
|
14
|
+
} from "./checkout-actions";
|
|
15
|
+
import { useConfirmationWait } from "./confirmation-wait";
|
|
4
16
|
import { useCheckoutCopy } from "./copy-context";
|
|
5
17
|
|
|
6
|
-
import {
|
|
7
|
-
import { useCheckoutNavigate
|
|
18
|
+
import { forgetHostedOrder } from "./hosted-return";
|
|
19
|
+
import { useCheckoutNavigate } from "./navigate-context";
|
|
20
|
+
import type { CheckoutDecline } from "./decline";
|
|
8
21
|
import type {
|
|
9
22
|
BuyerContact,
|
|
10
|
-
BuyerField,
|
|
11
23
|
BuyerInfo,
|
|
12
24
|
CheckoutOrder,
|
|
13
25
|
CreateOrderRequest,
|
|
@@ -16,9 +28,7 @@ import type {
|
|
|
16
28
|
OrderStatus,
|
|
17
29
|
PaymentMethod,
|
|
18
30
|
} from "./types";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
type Step = "dados" | "payment" | "status";
|
|
31
|
+
import { useHostedResume } from "./use-hosted-resume";
|
|
22
32
|
|
|
23
33
|
const STEP_ORDER: Step[] = ["dados", "payment", "status"];
|
|
24
34
|
|
|
@@ -57,233 +67,6 @@ export interface CheckoutHostPorts {
|
|
|
57
67
|
onPaid?: () => void;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
/**
|
|
61
|
-
* The flow's navigation actions, split out of {@link useCheckoutController} for
|
|
62
|
-
* the 80-line per-function gate.
|
|
63
|
-
*
|
|
64
|
-
* `back` is where "the Dados step was skipped" has to be honoured: going back
|
|
65
|
-
* off Pagamento normally lands on Dados, but for a buyer with a CPF on file
|
|
66
|
-
* that step is not part of their flow, so the menu is the only honest
|
|
67
|
-
* destination — until they open it themselves via `editBuyer` ("alterar" on the
|
|
68
|
-
* payer block), after which it IS part of their flow and back returns to it.
|
|
69
|
-
*/
|
|
70
|
-
function useCheckoutNav(
|
|
71
|
-
taxIdOnFile: boolean,
|
|
72
|
-
goToMenu: () => void,
|
|
73
|
-
setStep: Dispatch<SetStateAction<Step>>,
|
|
74
|
-
): { back: () => void; editBuyer: (() => void) | undefined } {
|
|
75
|
-
const [dadosOpened, setDadosOpened] = useState(false);
|
|
76
|
-
const openDados = useCallback(() => {
|
|
77
|
-
setDadosOpened(true);
|
|
78
|
-
setStep("dados");
|
|
79
|
-
}, [setStep]);
|
|
80
|
-
const back = useCallback(() => {
|
|
81
|
-
setStep((current) => {
|
|
82
|
-
if (current === "payment" && (!taxIdOnFile || dadosOpened)) return "dados";
|
|
83
|
-
goToMenu();
|
|
84
|
-
return current;
|
|
85
|
-
});
|
|
86
|
-
}, [dadosOpened, goToMenu, setStep, taxIdOnFile]);
|
|
87
|
-
// Undefined unless Dados was skipped — the payer block keys off its presence,
|
|
88
|
-
// so the decision lives here rather than being re-derived by every caller.
|
|
89
|
-
return { back, editBuyer: taxIdOnFile ? openDados : undefined };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Where checkout opens: on the outcome for a buyer returning from a hosted
|
|
94
|
-
* provider, on Pagamento when their CPF is already on file, else on Dados.
|
|
95
|
-
*/
|
|
96
|
-
function initialStep(resuming: boolean, taxIdOnFile: boolean): Step {
|
|
97
|
-
if (resuming) return "status";
|
|
98
|
-
return taxIdOnFile ? "payment" : "dados";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Hand the buyer to a redirect provider's own page, if that is where this
|
|
103
|
-
* charge settles (FUT-556).
|
|
104
|
-
*
|
|
105
|
-
* Called BEFORE the order is stored: storing it first would render the PIX or
|
|
106
|
-
* card step for a provider that returned neither, which is the dead end this
|
|
107
|
-
* fixes.
|
|
108
|
-
*
|
|
109
|
-
* A full navigation rather than the host's router, because the destination is
|
|
110
|
-
* another origin. The return trip comes back to this same checkout route
|
|
111
|
-
* carrying `transaction_nsu` + `slug`, which the status poll already reads.
|
|
112
|
-
*
|
|
113
|
-
* @returns true when the buyer is on their way and the caller must stop.
|
|
114
|
-
*/
|
|
115
|
-
function handOverToProvider(
|
|
116
|
-
order: CheckoutOrder,
|
|
117
|
-
navigate: CheckoutNavigate,
|
|
118
|
-
tenantSlug?: string,
|
|
119
|
-
): boolean {
|
|
120
|
-
if (!order.hostedCheckoutUrl) return false;
|
|
121
|
-
// PARK FIRST, navigate second. The order is the only thing the return trip
|
|
122
|
-
// has to rehydrate from, and the navigation may tear this SPA down before
|
|
123
|
-
// any later write lands.
|
|
124
|
-
//
|
|
125
|
-
// The STORE goes with it: one tab holds one slot, and on a multi-tenant
|
|
126
|
-
// storefront every store shares an origin. Without the slug, abandoning this
|
|
127
|
-
// hand-off and opening another store's checkout resumed THIS order there.
|
|
128
|
-
rememberHostedOrder(order, tenantSlug);
|
|
129
|
-
navigate(order.hostedCheckoutUrl);
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* How long the resumed screen keeps asking, and how often.
|
|
135
|
-
*
|
|
136
|
-
* TWO RATES, because one rate cannot serve this wait. The interval decides two
|
|
137
|
-
* things that pull opposite ways: how fast a buyer WHO PAID is told so, and
|
|
138
|
-
* what an abandoned checkout costs for the rest of the window. Every poll is a
|
|
139
|
-
* provider round trip, so a slow rate is cheap and leaves a paying buyer
|
|
140
|
-
* watching a spinner seconds longer than they need to — and the person on this
|
|
141
|
-
* screen has almost always paid. A single number picks one of them to lose;
|
|
142
|
-
* this shipped at a flat 5 s and picked the wrong one.
|
|
143
|
-
*
|
|
144
|
-
* So: 2.5 s for the first two minutes, which is where essentially every real
|
|
145
|
-
* webhook lands (it fires within seconds of the payment, and this rate matches
|
|
146
|
-
* what card and PIX already use), then 10 s for the remaining thirteen. A
|
|
147
|
-
* confirmation is at most 2.5 s late, and an abandoned checkout costs ~126
|
|
148
|
-
* polls instead of the 360 a flat 2.5 s would have.
|
|
149
|
-
*
|
|
150
|
-
* Fifteen minutes because by then a webhook that was ever coming has come. Past
|
|
151
|
-
* that the answer will not change while the buyer watches: the scheduled
|
|
152
|
-
* reconciliation is what rescues a genuinely late one, and it does that whether
|
|
153
|
-
* the tab is open or not.
|
|
154
|
-
*
|
|
155
|
-
* The BOUND is that wall-clock window, not a poll count (FUT-1144). The two are
|
|
156
|
-
* the same number for a healthy wait — 48 × 2.5 s + 78 × 10 s = 15 min — and
|
|
157
|
-
* they part company for the wait that needed bounding: a poll that FAILS
|
|
158
|
-
* incremented nothing, so a connection that never came back left this screen
|
|
159
|
-
* asking, and spinning, with no end at all. A clock cannot be stopped by the
|
|
160
|
-
* failure it is measuring.
|
|
161
|
-
*
|
|
162
|
-
* The card wait is bounded at 90 s (`CARD_AWAITING_WAIT_MS`) because a card
|
|
163
|
-
* authorises inline and a buyer is holding their phone. This leg is the other
|
|
164
|
-
* shape: the buyer has already been off to another site and back, and may
|
|
165
|
-
* legitimately still be finishing there.
|
|
166
|
-
*/
|
|
167
|
-
const HOSTED_RESUME_FAST_MS = 2_500;
|
|
168
|
-
const HOSTED_RESUME_SLOW_MS = 10_000;
|
|
169
|
-
/** Two minutes at the fast rate, before the wait is worth economising on. */
|
|
170
|
-
const HOSTED_RESUME_FAST_POLLS = (2 * 60_000) / HOSTED_RESUME_FAST_MS;
|
|
171
|
-
/** Thirteen more at the slow one — 15 minutes all told. */
|
|
172
|
-
const HOSTED_RESUME_WINDOW_MS = 15 * 60_000;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* The leg of checkout that resumes after a hosted provider sent the buyer back
|
|
176
|
-
* (FUT-556).
|
|
177
|
-
*
|
|
178
|
-
* The SPA was torn down by the redirect, so the order is rehydrated from the
|
|
179
|
-
* parked copy — once, on first render — and polled here rather than in a PIX or
|
|
180
|
-
* card view, because a redirect provider produced neither. The webhook is still
|
|
181
|
-
* what settles the order; this only tells the buyer that it did.
|
|
182
|
-
*
|
|
183
|
-
* And it stops telling them eventually. This poll was the one unbounded wait
|
|
184
|
-
* left in checkout — card and wallet both cap theirs — so a buyer who came back
|
|
185
|
-
* from a payment they never completed got "Confirmando seu pagamento… isso
|
|
186
|
-
* costuma levar alguns segundos" and a spinner, truthfully forever. The screen
|
|
187
|
-
* had no way to reach a terminal state, because the ORDER has none: expiry is
|
|
188
|
-
* PIX-only, and a redirect charge carries no QR window to lapse. `timedOut` is
|
|
189
|
-
* what the screen says instead of spinning.
|
|
190
|
-
*
|
|
191
|
-
* `error` is the half that was dropped on the floor (FUT-1144), and dropping it
|
|
192
|
-
* is what made this leg the SILENT one. The poll below has always been able to
|
|
193
|
-
* fail; this hook returned `status` and `timedOut` and nothing else, so a leg
|
|
194
|
-
* whose every request was failing looked identical to one still waiting — the
|
|
195
|
-
* spinner, forever, with the reason a `console`-less browser away. It is
|
|
196
|
-
* surfaced now, together with the wait's own `checkAgain`, because a screen that
|
|
197
|
-
* says "we cannot reach the payment" and offers nothing to press is only half
|
|
198
|
-
* of an answer.
|
|
199
|
-
*/
|
|
200
|
-
function useHostedResume(tenantSlug?: string): {
|
|
201
|
-
order: CheckoutOrder | null;
|
|
202
|
-
status: OrderStatus | null;
|
|
203
|
-
timedOut: boolean;
|
|
204
|
-
error: string | null;
|
|
205
|
-
checkAgain: () => void;
|
|
206
|
-
} {
|
|
207
|
-
const [order] = useState(() => takeHostedOrder(tenantSlug));
|
|
208
|
-
const { status, timedOut, error, checkAgain } = usePaymentPolling(order?.orderId ?? null, {
|
|
209
|
-
enabled: Boolean(order),
|
|
210
|
-
intervalMs: HOSTED_RESUME_FAST_MS,
|
|
211
|
-
slowAfterPolls: HOSTED_RESUME_FAST_POLLS,
|
|
212
|
-
slowIntervalMs: HOSTED_RESUME_SLOW_MS,
|
|
213
|
-
maxWaitMs: HOSTED_RESUME_WINDOW_MS,
|
|
214
|
-
});
|
|
215
|
-
return { order, status, timedOut, error, checkAgain };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* What the resumed leg contributes to the controller's surface.
|
|
220
|
-
*
|
|
221
|
-
* `resumeTimedOut` is only ever true on that leg — `useHostedResume` is the
|
|
222
|
-
* sole caller that bounds its wait, and a buyer who never left has a card or
|
|
223
|
-
* PIX view reporting its own. `resumeError` and `resumeCheckAgain` are the
|
|
224
|
-
* transient failure and the buyer's way out of it (FUT-1144).
|
|
225
|
-
*
|
|
226
|
-
* All three are inert for a checkout that never left this tab: with nothing
|
|
227
|
-
* parked the poll is disabled, so the error stays null, the bound never
|
|
228
|
-
* elapses, and the action has no wait to restart.
|
|
229
|
-
*/
|
|
230
|
-
function resumeSurface(resume: ReturnType<typeof useHostedResume>): {
|
|
231
|
-
resumeTimedOut: boolean;
|
|
232
|
-
resumeError: string | null;
|
|
233
|
-
resumeCheckAgain: () => void;
|
|
234
|
-
} {
|
|
235
|
-
return {
|
|
236
|
-
resumeTimedOut: resume.timedOut,
|
|
237
|
-
resumeError: resume.error,
|
|
238
|
-
resumeCheckAgain: resume.checkAgain,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Fire the host's `onPaid` port once the order settles PAID.
|
|
244
|
-
*
|
|
245
|
-
* FUT-601 made the SERVER empty the cart inside the confirmation transaction —
|
|
246
|
-
* but nothing told the SPA, whose cart provider survives every checkout route
|
|
247
|
-
* change and kept counting the items the buyer had just bought. The server was
|
|
248
|
-
* right and the screen was stale; this is where the host is told to catch up.
|
|
249
|
-
*
|
|
250
|
-
* PAID only. A FAILED or EXPIRED order fires nothing — that shopper still has
|
|
251
|
-
* a basket to retry with, and the host must not be told otherwise.
|
|
252
|
-
*/
|
|
253
|
-
function usePaidPort(settled: OrderStatus | null, onPaid: (() => void) | undefined): void {
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
if (settled !== "PAID") return;
|
|
256
|
-
onPaid?.();
|
|
257
|
-
}, [settled, onPaid]);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* The create-order refusal the steps render: what to say, which field to
|
|
262
|
-
* highlight, and the machine CODE that decides how it is presented — an
|
|
263
|
-
* unresolved charge is not a failed one, and the Pagamento step must not offer
|
|
264
|
-
* it a "Tentar novamente" (FUT-563). One hook so the three always move
|
|
265
|
-
* together; they were three `useState`s that could be cleared apart.
|
|
266
|
-
*/
|
|
267
|
-
function useCreateFailure() {
|
|
268
|
-
const [message, setMessage] = useState<string | null>(null);
|
|
269
|
-
const [field, setField] = useState<BuyerField | null>(null);
|
|
270
|
-
const [code, setCode] = useState<string | null>(null);
|
|
271
|
-
const clear = useCallback(() => {
|
|
272
|
-
setMessage(null);
|
|
273
|
-
setField(null);
|
|
274
|
-
setCode(null);
|
|
275
|
-
}, []);
|
|
276
|
-
const fail = useCallback(
|
|
277
|
-
(next: { message: string; field?: BuyerField | null; code?: string }) => {
|
|
278
|
-
setMessage(next.message);
|
|
279
|
-
setField(next.field ?? null);
|
|
280
|
-
setCode(next.code ?? null);
|
|
281
|
-
},
|
|
282
|
-
[],
|
|
283
|
-
);
|
|
284
|
-
return { message, field, code, clear, fail };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
70
|
/**
|
|
288
71
|
* All checkout state + handlers, so the checkout flow stays presentational.
|
|
289
72
|
* - `setMethod` drops any order raised for the previous method (no stale QR/form).
|
|
@@ -295,6 +78,8 @@ function useCreateFailure() {
|
|
|
295
78
|
* being saved.
|
|
296
79
|
* - `startPayment` raises the order (PIX → QR, CARD → chargeable); `payWithEmail`
|
|
297
80
|
* re-raises with a corrected e-mail for the merchant-email rejection.
|
|
81
|
+
* - `retry` re-charges the SAME order when the refusal says another instrument
|
|
82
|
+
* could work (FUT-1145), and raises a fresh one when it does not.
|
|
298
83
|
*
|
|
299
84
|
* @param taxIdOnFile The buyer already has a CPF saved (FUT-465) ⇒ the Dados
|
|
300
85
|
* step has nothing left to ask, so checkout opens on Pagamento and `back`
|
|
@@ -304,6 +89,9 @@ function useCreateFailure() {
|
|
|
304
89
|
* @param buyerFields What the store's chain declares it needs from the buyer
|
|
305
90
|
* (FUT-595). Absent ⇒ CPF-required, which is what this gate demanded before
|
|
306
91
|
* there was a declaration to read — never "ask nothing".
|
|
92
|
+
* @param basket WHICH basket this checkout is for (FUT-1213). Absent ⇒ the
|
|
93
|
+
* pre-1213 behaviour: a parked payment resumes on whatever checkout mounts
|
|
94
|
+
* next. See `./basket.ts` for why it is a signature of the lines.
|
|
307
95
|
*/
|
|
308
96
|
export function useCheckoutController(
|
|
309
97
|
ports: CheckoutHostPorts,
|
|
@@ -311,72 +99,78 @@ export function useCheckoutController(
|
|
|
311
99
|
taxIdOnFile = false,
|
|
312
100
|
buyerFields: readonly CheckoutCustomerField[] = CPF_ONLY,
|
|
313
101
|
tenantSlug?: string,
|
|
102
|
+
basket?: CheckoutBasketIdentity,
|
|
314
103
|
) {
|
|
315
104
|
const { createOrder, saveBuyerContact, onExitToMenu, onPaid } = ports;
|
|
316
105
|
const validation = useCheckoutCopy().screens.validation;
|
|
317
106
|
const navigate = useCheckoutNavigate();
|
|
318
|
-
const resume = useHostedResume(tenantSlug);
|
|
319
|
-
const [step, setStep] = useState<Step>(
|
|
107
|
+
const resume = useHostedResume(tenantSlug, basket);
|
|
108
|
+
const [step, setStep] = useState<Step>(taxIdOnFile ? "payment" : "dados");
|
|
320
109
|
// No method pre-selected: the Pagamento step shows just the picker until the
|
|
321
110
|
// buyer chooses PIX or card, then that method's order is raised and its UI
|
|
322
111
|
// revealed. Avoids raising a throwaway PIX charge for a buyer who wants card.
|
|
323
112
|
const [method, setMethodState] = useState<PaymentMethod | null>(null);
|
|
324
113
|
const [buyer, setBuyerState] = useState<BuyerInfo>(defaultBuyer ?? {});
|
|
325
114
|
const [saveProfile, setSaveProfile] = useState(true);
|
|
326
|
-
const [order, setOrder] = useState<CheckoutOrder | null>(
|
|
115
|
+
const [order, setOrder] = useState<CheckoutOrder | null>(null);
|
|
327
116
|
const [finalStatus, setFinalStatus] = useState<OrderStatus | null>(null);
|
|
117
|
+
const [decline, setDecline] = useState<CheckoutDecline | null>(null);
|
|
118
|
+
const [freshInstrument, setFreshInstrument] = useState(false);
|
|
328
119
|
const [creating, setCreating] = useState(false);
|
|
329
120
|
const failure = useCreateFailure();
|
|
121
|
+
useResumedCheckout(resume, setOrder, setStep, setFinalStatus, setMethodState);
|
|
330
122
|
|
|
331
123
|
const clearError = failure.clear;
|
|
332
124
|
const setBuyer = useCallback((next: BuyerInfo) => { setBuyerState(next); clearError(); }, [clearError]);
|
|
333
125
|
const { back, editBuyer } = useCheckoutNav(taxIdOnFile, onExitToMenu, setStep);
|
|
334
126
|
const setMethod = useCallback((next: PaymentMethod) => {
|
|
335
|
-
setMethodState((prev) => {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
setStep("payment");
|
|
352
|
-
}, [buyer, buyerFields, saveProfile, taxIdOnFile, clearError, saveBuyerContact, validation]);
|
|
353
|
-
const startPayment = useCallback(async (chosen: PaymentMethod, override?: BuyerInfo) => {
|
|
354
|
-
clearError();
|
|
355
|
-
setCreating(true);
|
|
356
|
-
const result = await createOrder({ method: chosen, buyer: override ?? buyer, saveProfile });
|
|
357
|
-
setCreating(false);
|
|
358
|
-
if (!result.ok) { failure.fail(result.error); return; }
|
|
359
|
-
if (handOverToProvider(result.data, navigate, tenantSlug)) return;
|
|
360
|
-
setOrder(result.data);
|
|
361
|
-
setFinalStatus(null);
|
|
362
|
-
}, [buyer, saveProfile, createOrder, clearError, navigate, tenantSlug]);
|
|
127
|
+
setMethodState((prev) => {
|
|
128
|
+
// SCOPED (FUT-1240): the order dropped here is THIS store's, and so is
|
|
129
|
+
// the parked entry that goes with it. Unscoped, changing method at store
|
|
130
|
+
// B threw away store A's parked hand-off — the same cross-store slot the
|
|
131
|
+
// slug closed on the read side, still open on the write side.
|
|
132
|
+
if (prev !== next) { setOrder(null); forgetHostedOrder(tenantSlug); clearError(); }
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
}, [clearError, tenantSlug]);
|
|
136
|
+
const goToPayment = useGoToPayment({
|
|
137
|
+
buyer, buyerFields, taxIdOnFile, saveProfile, saveBuyerContact, validation, failure, setStep,
|
|
138
|
+
});
|
|
139
|
+
const startPayment = useStartPayment({
|
|
140
|
+
buyer, saveProfile, createOrder, navigate, tenantSlug, basket, failure,
|
|
141
|
+
setCreating, setDecline, setOrder, setFinalStatus,
|
|
142
|
+
});
|
|
363
143
|
const payWithEmail = useCallback((email: string) => {
|
|
364
144
|
if (!method) return;
|
|
365
145
|
const next = { ...buyer, email };
|
|
366
146
|
setBuyerState(next);
|
|
367
147
|
void startPayment(method, next);
|
|
368
148
|
}, [buyer, method, startPayment]);
|
|
369
|
-
const handleResolved = useCallback((s: OrderStatus
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
149
|
+
const handleResolved = useCallback((s: OrderStatus, refusal?: CheckoutDecline | null) => {
|
|
150
|
+
setDecline(refusal ?? null);
|
|
151
|
+
setFinalStatus(s);
|
|
152
|
+
setStep("status");
|
|
153
|
+
}, []);
|
|
154
|
+
const retry = useRetryAction({
|
|
155
|
+
decline, order, clearError, setOrder, setDecline, setFinalStatus, setStep, setFreshInstrument,
|
|
156
|
+
});
|
|
373
157
|
const completed = useMemo(() => new Set(STEP_ORDER.slice(0, STEP_ORDER.indexOf(step))), [step]);
|
|
374
|
-
|
|
158
|
+
const settled = finalStatus ?? resume.status;
|
|
159
|
+
useSettledPort(settled, onPaid);
|
|
160
|
+
// The confirmation screen's own wait (FUT-1170). Live only where nothing else
|
|
161
|
+
// is polling and nothing has answered: a resumed leg brings its own wait, and
|
|
162
|
+
// every method's screen polls for itself on the payment step.
|
|
163
|
+
const confirming = useConfirmationWait({
|
|
164
|
+
orderId: order?.orderId ?? null,
|
|
165
|
+
active: step === "status" && settled === null && resume.order === null,
|
|
166
|
+
onSettled: setFinalStatus,
|
|
167
|
+
});
|
|
375
168
|
|
|
376
169
|
return {
|
|
377
170
|
step, setStep, method, setMethod, buyer, setBuyer, saveProfile, setSaveProfile,
|
|
378
|
-
order, finalStatus:
|
|
379
|
-
|
|
171
|
+
order, finalStatus: settled, creating,
|
|
172
|
+
decline, freshInstrument,
|
|
173
|
+
...resumeSurface(resume, confirming),
|
|
380
174
|
createError: failure.message, errorField: failure.field, errorCode: failure.code,
|
|
381
175
|
goToMenu: onExitToMenu, back, editBuyer,
|
|
382
176
|
goToPayment, startPayment, payWithEmail, handleResolved, retry, completed,
|