@12-apps/payments-frontend 3.21.4 → 3.22.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.
Files changed (31) hide show
  1. package/package.json +2 -2
  2. package/src/components/checkout/basket.ts +85 -0
  3. package/src/components/checkout/card-outcome.ts +81 -0
  4. package/src/components/checkout/card-view.tsx +62 -22
  5. package/src/components/checkout/checkout-actions.ts +341 -0
  6. package/src/components/checkout/checkout-flow.tsx +112 -18
  7. package/src/components/checkout/checkout-steps.tsx +149 -174
  8. package/src/components/checkout/checkout-totals.tsx +51 -0
  9. package/src/components/checkout/client-context.tsx +3 -0
  10. package/src/components/checkout/dados-step.tsx +141 -0
  11. package/src/components/checkout/decline.ts +48 -0
  12. package/src/components/checkout/en-US.ts +33 -0
  13. package/src/components/checkout/hosted-return.ts +190 -206
  14. package/src/components/checkout/hosted-store.ts +269 -0
  15. package/src/components/checkout/payment-status-parts.tsx +311 -0
  16. package/src/components/checkout/payment-status.tsx +69 -264
  17. package/src/components/checkout/providers/types.ts +20 -3
  18. package/src/components/checkout/pt-BR.ts +33 -0
  19. package/src/components/checkout/screens-copy.ts +14 -0
  20. package/src/components/checkout/screens-en-US.ts +1 -0
  21. package/src/components/checkout/screens-pt-BR.ts +3 -0
  22. package/src/components/checkout/transport.ts +21 -1
  23. package/src/components/checkout/types.ts +24 -0
  24. package/src/components/checkout/use-card-checkout.ts +31 -31
  25. package/src/components/checkout/use-checkout-controller.ts +51 -271
  26. package/src/components/checkout/use-hosted-resume.ts +326 -0
  27. package/src/components/checkout/view-copy.ts +32 -0
  28. package/src/components/checkout/wallet-pane.tsx +3 -0
  29. package/src/flows/create-payment-flows.tsx +7 -0
  30. package/src/flows/screens-hosted.tsx +19 -2
  31. package/src/index.ts +22 -0
@@ -0,0 +1,326 @@
1
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
2
+
3
+ import type { CheckoutBasketIdentity } from "./basket";
4
+ import { useCheckoutClientApi } from "./client-context";
5
+ import {
6
+ forgetHostedOrder,
7
+ takeHostedOrder,
8
+ type HostedResumeStep,
9
+ } from "./hosted-return";
10
+ import type { CheckoutOrder, OrderStatus } from "./types";
11
+ import { usePaymentPolling } from "./use-payment-polling";
12
+
13
+ /**
14
+ * A LAYOUT effect where there is a DOM, an ordinary one where there is not.
15
+ *
16
+ * The resume decision cannot be made during render — it waits for the host's
17
+ * cart to load — so it lands from an effect, and an ordinary effect runs AFTER
18
+ * paint: a buyer coming back from a payment would see one frame of the Dados
19
+ * or Pagamento step before their confirmation replaced it. A layout effect
20
+ * commits before the browser paints, so the flow simply opens where it belongs.
21
+ *
22
+ * The branch is by ENVIRONMENT rather than by render pass, which is what makes
23
+ * it a constant and not a conditional hook. It costs nothing on a server render
24
+ * either: everything this hook decides from is `sessionStorage`, which does not
25
+ * exist there — and `useLayoutEffect` on the server is a warning React prints
26
+ * for exactly the case where the effect would matter and cannot run.
27
+ */
28
+ export const useResumeEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
29
+
30
+ /**
31
+ * The leg of checkout that RESUMES a payment this tab already raised (FUT-556,
32
+ * FUT-1140, FUT-1213, FUT-1146).
33
+ *
34
+ * Three tickets share this hook because they are three halves of one mechanism,
35
+ * and stacking them would have produced three: FUT-1213 decides WHETHER a
36
+ * parked checkout may be resumed, FUT-1140 makes the parked entry cover every
37
+ * raised order rather than only a hand-off, and FUT-1146 gives the buyer the
38
+ * one way out that a hosted charge's own protocol cannot offer.
39
+ *
40
+ * ## How long it keeps asking, and how often
41
+ *
42
+ * TWO RATES, because one rate cannot serve this wait. The interval decides two
43
+ * things that pull opposite ways: how fast a buyer WHO PAID is told so, and
44
+ * what an abandoned checkout costs for the rest of the window. Every poll is a
45
+ * provider round trip, so a slow rate is cheap and leaves a paying buyer
46
+ * watching a spinner seconds longer than they need to — and the person on this
47
+ * screen has almost always paid. A single number picks one of them to lose;
48
+ * this shipped at a flat 5 s and picked the wrong one.
49
+ *
50
+ * So: 2.5 s for the first two minutes, which is where essentially every real
51
+ * webhook lands, then 10 s for the remaining thirteen.
52
+ *
53
+ * Fifteen minutes because by then a webhook that was ever coming has come. The
54
+ * BOUND is that wall-clock window, not a poll count (FUT-1144): a poll that
55
+ * FAILS increments nothing, so a connection that never came back left this
56
+ * screen asking with no end at all. A clock cannot be stopped by the failure it
57
+ * is measuring.
58
+ */
59
+ const HOSTED_RESUME_FAST_MS = 2_500;
60
+ const HOSTED_RESUME_SLOW_MS = 10_000;
61
+ /** Two minutes at the fast rate, before the wait is worth economising on. */
62
+ const HOSTED_RESUME_FAST_POLLS = (2 * 60_000) / HOSTED_RESUME_FAST_MS;
63
+ /** Thirteen more at the slow one — 15 minutes all told. */
64
+ const HOSTED_RESUME_WINDOW_MS = 15 * 60_000;
65
+
66
+ /**
67
+ * How long the buyer waits before being offered a way out (FUT-1146).
68
+ *
69
+ * A cancelled or refused hosted payment has NO terminal state to arrive at.
70
+ * The provider's `payment_check` publishes `success` and `paid` and nothing
71
+ * else — no status, no cancel, no decline — the webhook verifier refuses an
72
+ * unpaid delivery outright, and every server-side writer of FAILED is
73
+ * unreachable from a hosted cancel. So the screen waits fifteen minutes and
74
+ * then tells someone who never paid not to pay again. The only signal that
75
+ * exists is the BUYER's, and this is how long we wait before asking for it.
76
+ *
77
+ * Thirty seconds: a webhook that was ever coming lands within seconds of the
78
+ * payment (the fast rate above is sized on exactly that), so a wait still going
79
+ * at thirty is already unusual — while a button offered instantly would sit
80
+ * under a spinner during the two seconds in which most confirmations arrive,
81
+ * inviting a shopper to abandon a payment that is landing. The release itself
82
+ * is guarded server-side regardless: a payable the provider reports PAID is
83
+ * answered PAID and released by nothing.
84
+ */
85
+ const RELEASE_OFFER_AFTER_MS = 30_000;
86
+
87
+ /** What the resumed leg contributes to the controller's surface. */
88
+ export interface HostedResume {
89
+ /** The rehydrated order, once the decision has been made. */
90
+ order: CheckoutOrder | null;
91
+ /** Where the flow should open for it — see `hosted-return.ts`. */
92
+ step: HostedResumeStep | null;
93
+ status: OrderStatus | null;
94
+ timedOut: boolean;
95
+ error: string | null;
96
+ checkAgain: () => void;
97
+ /**
98
+ * The buyer's own "I did not pay" (FUT-1146), or `undefined` while it must
99
+ * not be offered — nothing is being resumed, the wait has settled, or the
100
+ * grace period above has not elapsed.
101
+ */
102
+ release: (() => void) | undefined;
103
+ /** A release is in flight; the action must not be pressable twice. */
104
+ releasing: boolean;
105
+ /** The order was released — the caller opens a fresh checkout. */
106
+ released: boolean;
107
+ }
108
+
109
+ /**
110
+ * How long the decision waits for the host's cart before deciding without it.
111
+ *
112
+ * The deferral (FUT-1213) assumes the cart eventually answers. A cart FETCH can
113
+ * fail — on exactly the flaky connection a buyer has coming back from their
114
+ * bank app — and an unbounded wait there is the same shape of bug this ticket
115
+ * is about, pointed the other way: the flow renders Dados forever and a paid
116
+ * buyer's confirmation never lands.
117
+ *
118
+ * So the wait is bounded, and past the bound the decision is made WITHOUT a
119
+ * basket, which is the pre-1213 answer: resume. That is the permissive
120
+ * direction, deliberately, and it is the same one `hostedCheckoutReturnPending`
121
+ * takes for an unloaded cart — a shopper whose cart never loaded cannot check
122
+ * out with it either way, so the only outcome still worth protecting is the
123
+ * confirmation of a payment that already happened.
124
+ *
125
+ * Eight seconds: long enough that no ordinary cart fetch reaches it, short
126
+ * enough that a buyer coming back from a payment is not left looking at a form.
127
+ */
128
+ const BASKET_WAIT_MS = 8_000;
129
+
130
+ /** The decision, made ONCE, as soon as the basket is loaded enough to make it. */
131
+ function useResumeDecision(
132
+ tenantSlug: string | undefined,
133
+ basket: CheckoutBasketIdentity | undefined,
134
+ ): {
135
+ resumed: { order: CheckoutOrder; step: HostedResumeStep } | null;
136
+ setResumed: (next: { order: CheckoutOrder; step: HostedResumeStep } | null) => void;
137
+ asking: CheckoutOrder | null;
138
+ setAsking: (next: CheckoutOrder | null) => void;
139
+ } {
140
+ const [resumed, setResumed] = useState<{ order: CheckoutOrder; step: HostedResumeStep } | null>(
141
+ null,
142
+ );
143
+ const [asking, setAsking] = useState<CheckoutOrder | null>(null);
144
+ // Once, whatever React does with this effect. The decision CONSUMES the
145
+ // parked entry, so a second run would find nothing and un-resume a buyer
146
+ // mid-confirmation — which is what StrictMode's double-invoke does for free.
147
+ const decided = useRef(false);
148
+ const waitedLongEnough = useBasketDeadline();
149
+
150
+ useResumeEffect(() => {
151
+ if (decided.current) return;
152
+ // Past the deadline the basket is treated as unnamed rather than as
153
+ // pending — see BASKET_WAIT_MS. A cart that never answered cannot be
154
+ // compared with, and waiting forever is the worse of the two failures.
155
+ const decision = takeHostedOrder(tenantSlug, waitedLongEnough ? undefined : basket);
156
+ // WAIT is the host's cart still loading. Nothing was read and nothing was
157
+ // consumed; the next render with a loaded basket decides for real.
158
+ if (decision.verdict === "WAIT") return;
159
+ decided.current = true;
160
+ if (decision.verdict === "RESUME") setResumed({ order: decision.order, step: decision.step });
161
+ if (decision.verdict === "ASK") setAsking(decision.order);
162
+ }, [tenantSlug, basket, waitedLongEnough]);
163
+
164
+ return { resumed, setResumed, asking, setAsking };
165
+ }
166
+
167
+ /**
168
+ * Rule 3: a DIFFERENT basket stands, so ask the server once what the parked
169
+ * order is worth.
170
+ *
171
+ * PAID resumes on the confirmation — that order is settled in the host's own
172
+ * row by the webhook, and it is the one thing a shopper must never lose.
173
+ * Anything else drops the entry and leaves the checkout to the basket in front
174
+ * of them.
175
+ *
176
+ * A FAILED ASK leaves the entry parked, deliberately. "We could not reach the
177
+ * server" is not "the order is not paid", and the shopper still gets their
178
+ * normal checkout either way — so the cheap outcome is that a later mount asks
179
+ * again and can still find the confirmation, rather than a dropped entry that
180
+ * can never be recovered.
181
+ */
182
+ function useAskBeforeResuming(
183
+ asking: CheckoutOrder | null,
184
+ onResume: (next: { order: CheckoutOrder; step: HostedResumeStep }) => void,
185
+ onDrop: () => void,
186
+ ): void {
187
+ const client = useCheckoutClientApi();
188
+ useEffect(() => {
189
+ if (!asking) return undefined;
190
+ let live = true;
191
+ void client.getStatus(asking.orderId).then((answer) => {
192
+ if (!live) return;
193
+ if (!answer.ok) {
194
+ onDrop();
195
+ return;
196
+ }
197
+ forgetHostedOrder();
198
+ if (answer.data === "PAID") onResume({ order: asking, step: "status" });
199
+ else onDrop();
200
+ });
201
+ return () => {
202
+ live = false;
203
+ };
204
+ }, [asking, client, onDrop, onResume]);
205
+ }
206
+
207
+ /** Whether the wait for the host's cart has run out — see {@link BASKET_WAIT_MS}. */
208
+ function useBasketDeadline(): boolean {
209
+ const [elapsed, setElapsed] = useState(false);
210
+ useEffect(() => {
211
+ const timer = setTimeout(() => setElapsed(true), BASKET_WAIT_MS);
212
+ return () => clearTimeout(timer);
213
+ }, []);
214
+ return elapsed;
215
+ }
216
+
217
+ /** Whether the "I did not pay" way out may be offered yet — see the constant. */
218
+ function useReleaseOffered(waiting: boolean): boolean {
219
+ const [elapsed, setElapsed] = useState(false);
220
+ useEffect(() => {
221
+ if (!waiting) return undefined;
222
+ const timer = setTimeout(() => setElapsed(true), RELEASE_OFFER_AFTER_MS);
223
+ return () => clearTimeout(timer);
224
+ }, [waiting]);
225
+ return waiting && elapsed;
226
+ }
227
+
228
+ /** The release itself: ask the server to let the order go, unless it is paid. */
229
+ function useRelease(
230
+ order: CheckoutOrder | null,
231
+ onSettled: (status: OrderStatus) => void,
232
+ onReleased: () => void,
233
+ ): { run: () => void; releasing: boolean } {
234
+ const client = useCheckoutClientApi();
235
+ const [releasing, setReleasing] = useState(false);
236
+ const run = useCallback(() => {
237
+ if (!order || releasing) return;
238
+ setReleasing(true);
239
+ void client.releaseCheckout({ orderId: order.orderId }).then((answer) => {
240
+ setReleasing(false);
241
+ // The one answer that overrules the buyer: they say they did not pay and
242
+ // the provider says they did. They keep their confirmation.
243
+ if (answer.ok && answer.data === "PAID") {
244
+ onSettled("PAID");
245
+ return;
246
+ }
247
+ // Everything else — released, or a request that never got out — returns
248
+ // them to a usable checkout. A server that could not be reached has not
249
+ // taken their money either, and leaving them on a dead wait to be sure
250
+ // is the failure this ticket exists to remove.
251
+ forgetHostedOrder();
252
+ onReleased();
253
+ });
254
+ }, [client, order, releasing, onSettled, onReleased]);
255
+ return { run, releasing };
256
+ }
257
+
258
+ /** Still waiting on an answer for the order we resumed — nothing more. */
259
+ function stillWaiting(polling: boolean, released: boolean, settled: OrderStatus | null): boolean {
260
+ if (!polling || released) return false;
261
+ return settled === null || settled === "AWAITING_PAYMENT";
262
+ }
263
+
264
+ /** The wait's own poll, run for the CONFIRMATION leg and nothing else. */
265
+ function useResumePoll(
266
+ order: CheckoutOrder | null,
267
+ polling: boolean,
268
+ ): { status: OrderStatus | null; timedOut: boolean; error: string | null; checkAgain: () => void } {
269
+ // A resume that lands back on the payment step is handed to the PIX or card
270
+ // pane, which runs its own wait — two polls for one order would race each
271
+ // other to the same answer.
272
+ return usePaymentPolling(order === null ? null : order.orderId, {
273
+ enabled: polling,
274
+ intervalMs: HOSTED_RESUME_FAST_MS,
275
+ slowAfterPolls: HOSTED_RESUME_FAST_POLLS,
276
+ slowIntervalMs: HOSTED_RESUME_SLOW_MS,
277
+ maxWaitMs: HOSTED_RESUME_WINDOW_MS,
278
+ });
279
+ }
280
+
281
+ export function useHostedResume(
282
+ tenantSlug?: string,
283
+ basket?: CheckoutBasketIdentity,
284
+ ): HostedResume {
285
+ const { resumed, setResumed, asking, setAsking } = useResumeDecision(tenantSlug, basket);
286
+ const [override, setOverride] = useState<OrderStatus | null>(null);
287
+ const [released, setReleased] = useState(false);
288
+
289
+ const drop = useCallback(() => setAsking(null), [setAsking]);
290
+ const resume = useCallback(
291
+ (next: { order: CheckoutOrder; step: HostedResumeStep }) => {
292
+ setAsking(null);
293
+ setResumed(next);
294
+ },
295
+ [setAsking, setResumed],
296
+ );
297
+ useAskBeforeResuming(asking, resume, drop);
298
+
299
+ const order = resumed === null ? null : resumed.order;
300
+ const step = resumed === null ? null : resumed.step;
301
+ const polling = step === "status";
302
+ const wait = useResumePoll(order, polling);
303
+
304
+ const settled = override === null ? wait.status : override;
305
+ const offered = useReleaseOffered(stillWaiting(polling, released, settled));
306
+ const onReleased = useCallback(() => {
307
+ setResumed(null);
308
+ setReleased(true);
309
+ }, [setResumed]);
310
+ const { run, releasing } = useRelease(order, setOverride, onReleased);
311
+
312
+ return {
313
+ order,
314
+ step,
315
+ // A RELEASED order reports nothing. The poll's last answer is still sitting
316
+ // in its own state, and a controller that read it would carry "we are
317
+ // confirming your payment" into the fresh checkout the buyer just asked for.
318
+ status: released ? null : settled,
319
+ timedOut: wait.timedOut,
320
+ error: wait.error,
321
+ checkAgain: wait.checkAgain,
322
+ release: offered ? run : undefined,
323
+ releasing,
324
+ released,
325
+ };
326
+ }
@@ -1,4 +1,5 @@
1
1
  import type { CheckoutCopy } from "./copy-context";
2
+ import type { CheckoutDeclineReason } from "./decline";
2
3
 
3
4
  /**
4
5
  * Every string the legacy checkout views render — required props, with NO
@@ -71,8 +72,26 @@ export interface StatusOutcomeCopy {
71
72
  export interface PaymentStatusCopy {
72
73
  paid: StatusOutcomeCopy;
73
74
  awaiting: StatusOutcomeCopy;
75
+ /**
76
+ * A refusal with nothing more specific to say. Still the whole of what a
77
+ * buyer reads when the server sent no `declineReason`, or sent one this
78
+ * bundle has never heard of — see {@link PaymentStatusCopy.declined}.
79
+ */
74
80
  failed: StatusOutcomeCopy;
75
81
  expired: StatusOutcomeCopy;
82
+ /**
83
+ * What a REFUSED CARD says, per normalized reason (FUT-1145).
84
+ *
85
+ * The server has classified declines since FUT-340 and then discarded the
86
+ * classification on the wire, so an expired card, a card reported stolen, no
87
+ * funds, and "attempts exhausted — do not retry" all reached the buyer as one
88
+ * sentence offering a retry that could not work. Each of those asks something
89
+ * different of the person holding the phone, and only they can act on it.
90
+ *
91
+ * A reason with no entry — a newer server, a host mid-migration — falls back
92
+ * to {@link PaymentStatusCopy.failed}, which is exactly today's screen.
93
+ */
94
+ declined: Partial<Record<CheckoutDeclineReason, StatusOutcomeCopy>>;
76
95
  awaitingTimedOut: StatusOutcomeCopy;
77
96
  /**
78
97
  * The wait cannot reach the payment right now (FUT-1144) — and is STILL
@@ -86,6 +105,19 @@ export interface PaymentStatusCopy {
86
105
  awaitingUnreachable: StatusOutcomeCopy;
87
106
  retryAction: string;
88
107
  regenerateAction: string;
108
+ /**
109
+ * "I did not pay" — the buyer's own way out of a wait with no terminal state
110
+ * (FUT-1146).
111
+ *
112
+ * A cancelled or refused payment on a provider's own page produces NO signal
113
+ * anywhere: the provider's check publishes `success` and `paid` and nothing
114
+ * else, an unpaid webhook delivery fails verification before it is parsed,
115
+ * and no server-side writer of FAILED is reachable from it. So the screen
116
+ * waited fifteen minutes and then told someone who had never paid not to pay
117
+ * again. The only signal that exists is this one, and it is safe to act on
118
+ * because the server re-asks the provider before letting anything go.
119
+ */
120
+ notPaidAction: string;
89
121
  /**
90
122
  * Ask now, rather than waiting for the next automatic poll — and, once the
91
123
  * wait has run out, the only thing that starts it again.
@@ -149,6 +149,7 @@ export function WalletCardPane(props: WalletPaneProps): JSX.Element {
149
149
  const { Alert } = useCheckoutComponents();
150
150
  const copy = useCheckoutCopy().screens.settling;
151
151
  const { order, buyer, config, tenantSlug, onResolved, pollIntervalMs } = props;
152
+ const { freshInstrument, basket } = props;
152
153
  const wallet = useWalletCharge(order, buyer, onResolved, pollIntervalMs);
153
154
  // A sheet failure the wallet reported before any charge existed (pay.js
154
155
  // refused, merchant validation unavailable, the sheet errored) — shown
@@ -187,6 +188,8 @@ export function WalletCardPane(props: WalletPaneProps): JSX.Element {
187
188
  tenantSlug={tenantSlug}
188
189
  onResolved={onResolved}
189
190
  pollIntervalMs={pollIntervalMs}
191
+ freshInstrument={freshInstrument}
192
+ basket={basket}
190
193
  />
191
194
  </Box>
192
195
  );
@@ -150,6 +150,8 @@ function buildUseCheckout(runtime: FlowsRuntime): () => CheckoutController {
150
150
  return function useCheckout(): CheckoutController {
151
151
  const { config } = useResolvedConfig(runtime);
152
152
  const defaults = runtime.config.useBuyerDefaults?.() ?? {};
153
+ const cart = runtime.config.useCart();
154
+ const scope = { tenantSlug: runtime.useTenantSlug() };
153
155
  return useCheckoutController(
154
156
  {
155
157
  createOrder: ports.createPayable,
@@ -162,6 +164,11 @@ function buildUseCheckout(runtime: FlowsRuntime): () => CheckoutController {
162
164
  // Resolved for NO method: the gate runs on the Dados step, before the
163
165
  // picker, and FUT-595's rule is to collect the union up front.
164
166
  buyerFieldsFor(config?.chain, null),
167
+ scope.tenantSlug,
168
+ // WHICH basket, so a payment raised from another one cannot resume itself
169
+ // over it (FUT-1213). The host answers on its cart view; a host that does
170
+ // not gets the pre-1213 behaviour.
171
+ cart.identity,
165
172
  );
166
173
  };
167
174
  }
@@ -37,7 +37,10 @@ function useHandover(payable: CheckoutOrder, url: string, navigate: (url: string
37
37
  useEffect(() => {
38
38
  if (done) return;
39
39
  setDone(true);
40
- rememberHostedOrder(payable);
40
+ // `handoff: true` is what tells the return leg this order was finished on
41
+ // ANOTHER SITE (FUT-1140): only such an order resumes onto the confirmation
42
+ // screen, because only such an order has nothing left on our page to show.
43
+ rememberHostedOrder(payable, { handoff: true });
41
44
  navigate(url);
42
45
  }, [done, payable, url, navigate]);
43
46
  }
@@ -163,6 +166,20 @@ function ReturnStalled({
163
166
  );
164
167
  }
165
168
 
169
+ /**
170
+ * The parked order this return trip is for, or null.
171
+ *
172
+ * This screen is mounted at the host's RETURN route, so it carries no basket to
173
+ * weigh the entry against and the FUT-1213 rule cannot ask for one: with no
174
+ * basket named the decision is the pre-1213 one — resume what was parked —
175
+ * which is the right answer for a URL that exists only for buyers coming back
176
+ * from a payment. `ASK` is unreachable from here.
177
+ */
178
+ function resumeParked(): CheckoutOrder | null {
179
+ const decision = takeHostedOrder();
180
+ return decision.verdict === "RESUME" ? decision.order : null;
181
+ }
182
+
166
183
  function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn"] {
167
184
  function HostedReturnBody({
168
185
  onResolved,
@@ -172,7 +189,7 @@ function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn
172
189
  const { Alert, LoadingState } = useCheckoutComponents();
173
190
  // Read-and-clear, once, on first render: the resumed view belongs to
174
191
  // exactly one return trip.
175
- const [parked] = useState(takeHostedOrder);
192
+ const [parked] = useState(resumeParked);
176
193
  // Bounded, for the reason on RETURN_WINDOW_MS: nothing here can ever reach a
177
194
  // terminal state on its own, so an unbounded poll is a spinner the buyer
178
195
  // watches until they close the tab.
package/src/index.ts CHANGED
@@ -198,6 +198,28 @@ export {
198
198
  type CheckoutStepperStep,
199
199
  type CheckoutTextProps,
200
200
  } from './components/checkout/ui';
201
+ /**
202
+ * WHICH basket a checkout is for (FUT-1213).
203
+ *
204
+ * A host computes the signature from its own cart lines and hands it to the
205
+ * flow on `cart.identity`, so a payment raised from an ABANDONED basket cannot
206
+ * resume itself over the one the shopper is holding now. Exported because the
207
+ * host owns the cart and therefore has to build it — see `basket.ts` for why
208
+ * the identity is the lines and never the cart's id.
209
+ */
210
+ export {
211
+ basketSignature,
212
+ type CheckoutBasketIdentity,
213
+ type CheckoutBasketLine,
214
+ } from './components/checkout/basket';
215
+ /**
216
+ * WHY a card was refused, and whether another attempt could work (FUT-1145).
217
+ * A host wiring the confirmation screen's per-reason copy names these.
218
+ */
219
+ export type {
220
+ CheckoutDecline,
221
+ CheckoutDeclineReason,
222
+ } from './components/checkout/decline';
201
223
  export {
202
224
  type BuyerContact,
203
225
  type BuyerField,