@12-apps/payments-frontend 3.22.0 → 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.
Files changed (42) hide show
  1. package/package.json +2 -2
  2. package/src/components/checkout/checkout-actions.ts +55 -9
  3. package/src/components/checkout/checkout-flow.tsx +15 -7
  4. package/src/components/checkout/confirmation-wait.ts +97 -0
  5. package/src/components/checkout/en-US.ts +8 -0
  6. package/src/components/checkout/failure-codes.ts +82 -0
  7. package/src/components/checkout/hosted-store.ts +23 -1
  8. package/src/components/checkout/payment-error-panel.tsx +9 -3
  9. package/src/components/checkout/pix-view.tsx +97 -8
  10. package/src/components/checkout/poll-loop.ts +5 -3
  11. package/src/components/checkout/pt-BR.ts +9 -0
  12. package/src/components/checkout/types.ts +11 -0
  13. package/src/components/checkout/use-card-checkout.ts +3 -2
  14. package/src/components/checkout/use-checkout-controller.ts +19 -5
  15. package/src/components/checkout/use-payment-polling.ts +58 -7
  16. package/src/components/checkout/use-wallet-charge.ts +24 -1
  17. package/src/components/checkout/view-copy.ts +38 -0
  18. package/src/components/checkout/wallet-pane.tsx +6 -1
  19. package/src/flows/catalog-exit.ts +33 -0
  20. package/src/flows/create-payment-flows.tsx +12 -1
  21. package/src/flows/pipeline/actions.tsx +104 -0
  22. package/src/flows/pipeline/admission.ts +55 -0
  23. package/src/flows/pipeline/context.ts +140 -0
  24. package/src/flows/pipeline/derive-step.ts +234 -0
  25. package/src/flows/pipeline/engine-actions.ts +257 -0
  26. package/src/flows/pipeline/engine-chrome.tsx +123 -0
  27. package/src/flows/pipeline/engine-state.ts +107 -0
  28. package/src/flows/pipeline/engine.tsx +377 -0
  29. package/src/flows/pipeline/methods.ts +71 -0
  30. package/src/flows/pipeline/refusal-routing.ts +106 -0
  31. package/src/flows/pipeline/slices.ts +110 -0
  32. package/src/flows/pipeline/stable-plugins.ts +72 -0
  33. package/src/flows/pipeline/steps/buyer-steps.tsx +297 -0
  34. package/src/flows/pipeline/steps/index.ts +54 -0
  35. package/src/flows/pipeline/steps/pay-steps.tsx +182 -0
  36. package/src/flows/pipeline/steps/status-step.tsx +41 -0
  37. package/src/flows/pipeline/types.ts +232 -0
  38. package/src/flows/public.ts +78 -0
  39. package/src/flows/screens-hosted.tsx +37 -4
  40. package/src/flows/screens-pay.tsx +6 -1
  41. package/src/flows/types.ts +25 -2
  42. package/src/index.ts +7 -19
@@ -0,0 +1,182 @@
1
+ /**
2
+ * THE PAY PHASE (FUT-1240): the Pix code, the card form, the hand-off, and the
3
+ * return from one.
4
+ *
5
+ * Every step here is `phase: "pay"` except the resume, and that matters: the
6
+ * engine drops the whole phase for a settlement method whose `raisesCharge` is
7
+ * `false`. So a shopper paying the courier never reaches any of these, and no
8
+ * lane has to remember not to render them.
9
+ *
10
+ * The resume is `before-pay` rather than `pay` deliberately. A shopper coming
11
+ * back from a provider's own page has to be resolved BEFORE they are offered a
12
+ * way to pay again — and the same screen has to work for an in-person method,
13
+ * which has no pay phase at all.
14
+ */
15
+ import { Box } from "@mui/material";
16
+ import { useEffect, type JSX } from "react";
17
+
18
+ import { useCheckoutComponents } from "../../../components/checkout/ui";
19
+ import { useHostedResume, type HostedResume } from "../../../components/checkout/use-hosted-resume";
20
+ import type { FlowsRuntime } from "../../runtime";
21
+ import { usePipelineActions } from "../actions";
22
+ import { CARD_PANE_STEP, PIX_PANE_STEP } from "../methods";
23
+ import type { CheckoutStep, CheckoutStepRender } from "../types";
24
+
25
+ export const HANDOFF_STEP_ID = "handoff";
26
+ export const RESUME_STEP_ID = "resume";
27
+
28
+ /** A raised order whose provider takes the money on its own page. */
29
+ function handedOver(url: string | undefined): url is string {
30
+ return typeof url === "string" && url.length > 0;
31
+ }
32
+
33
+ function PixView({ ctx }: CheckoutStepRender<void>): JSX.Element | null {
34
+ const actions = usePipelineActions();
35
+ const PixPayment = actions.screens.PixPayment;
36
+ if (!ctx.order) return null;
37
+ return <PixPayment payable={ctx.order} onResolved={actions.resolve} />;
38
+ }
39
+
40
+ function CardView({ ctx }: CheckoutStepRender<void>): JSX.Element | null {
41
+ const actions = usePipelineActions();
42
+ const CardEntry = actions.screens.CardEntry;
43
+ if (!ctx.order) return null;
44
+ return <CardEntry payable={ctx.order} onResolved={actions.resolve} />;
45
+ }
46
+
47
+ /**
48
+ * The Pix code, once one exists.
49
+ *
50
+ * WHICH method this pane belongs to is not asked here: the engine reads it off
51
+ * the descriptor that named this step as its `pane`, so a method the package
52
+ * has never heard of can reuse the pane by naming it, and no pane states its
53
+ * own method twice.
54
+ */
55
+ export const pixStep: CheckoutStep = {
56
+ id: PIX_PANE_STEP,
57
+ phase: "pay",
58
+ order: 0,
59
+ applies(ctx) {
60
+ return ctx.order !== null && !handedOver(ctx.order.hostedCheckoutUrl);
61
+ },
62
+ complete(ctx) {
63
+ return ctx.outcome !== null;
64
+ },
65
+ render(props) {
66
+ return <PixView {...props} />;
67
+ },
68
+ };
69
+
70
+ /** The card form, once an order exists to charge. Its method is its `pane` owner. */
71
+ export const cardStep: CheckoutStep = {
72
+ id: CARD_PANE_STEP,
73
+ phase: "pay",
74
+ order: 1,
75
+ applies(ctx) {
76
+ return ctx.order !== null && !handedOver(ctx.order.hostedCheckoutUrl);
77
+ },
78
+ complete(ctx) {
79
+ return ctx.outcome !== null;
80
+ },
81
+ render(props) {
82
+ return <CardView {...props} />;
83
+ },
84
+ };
85
+
86
+ /**
87
+ * The hand-off interstitial: the order is parked and the tab is leaving.
88
+ *
89
+ * `complete` is `false` forever on purpose — there is no next step on THIS
90
+ * page. What comes back is a fresh mount that meets the resume step.
91
+ */
92
+ function HandoffView({ ctx, back }: CheckoutStepRender<void>): JSX.Element | null {
93
+ const { Text } = useCheckoutComponents();
94
+ const actions = usePipelineActions();
95
+ const HostedHandoff = actions.screens.HostedHandoff;
96
+ const url = ctx.order?.hostedCheckoutUrl;
97
+ if (!ctx.order || !handedOver(url)) return null;
98
+ const pipeline = actions.copy.pipeline;
99
+ const waiting =
100
+ (ctx.method === null ? undefined : pipeline.awaitingHandover[ctx.method]) ?? pipeline.loading;
101
+ return (
102
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
103
+ <Text variant="body" size="sm" color="secondary" as="p" data-testid="checkout-awaiting-handover">
104
+ {waiting}
105
+ </Text>
106
+ <HostedHandoff url={url} payable={ctx.order} onCancel={back} />
107
+ </Box>
108
+ );
109
+ }
110
+
111
+ export const handoffStep: CheckoutStep = {
112
+ id: HANDOFF_STEP_ID,
113
+ phase: "pay",
114
+ order: 2,
115
+ applies(ctx) {
116
+ return ctx.order !== null && handedOver(ctx.order.hostedCheckoutUrl);
117
+ },
118
+ complete() {
119
+ return false;
120
+ },
121
+ render(props) {
122
+ return <HandoffView {...props} />;
123
+ },
124
+ };
125
+
126
+ /**
127
+ * A shopper coming back from a provider's own page.
128
+ *
129
+ * It runs the package's OWN resume machinery — `useHostedResume`, with this
130
+ * checkout's slug and basket — rather than a second reading of the parked
131
+ * entry. That matters more than the extra hook: `useHostedResume` carries the
132
+ * whole FUT-1213 rule, including the deferral until the cart has answered, the
133
+ * bounded wait when it never does, and the one server ASK for a basket that
134
+ * has changed. A step that re-implemented "is something parked?" would be a
135
+ * second answer to a question with one correct one, on the money path.
136
+ *
137
+ * WHERE a resume lands is `hosted-return.ts`'s decision, honoured here:
138
+ *
139
+ * - a HAND-OFF has nothing left on our page, so it waits on the confirmation
140
+ * until the poll reaches a terminal state;
141
+ * - anything else — a Pix code, a card charge raised on our own page — is
142
+ * ADOPTED as this visit's order, and the walk puts the shopper back in
143
+ * front of the pane that still has what they need.
144
+ */
145
+ export function buildResumeStep(runtime: FlowsRuntime): CheckoutStep<void, HostedResume> {
146
+ return {
147
+ id: RESUME_STEP_ID,
148
+ phase: "before-pay",
149
+ order: -10,
150
+ useFacts() {
151
+ const tenantSlug = runtime.useTenantSlug();
152
+ const cart = runtime.config.useCart();
153
+ return useHostedResume(tenantSlug, cart.identity);
154
+ },
155
+ applies(ctx, resume) {
156
+ return ctx.outcome === null && ctx.order === null && resume.order !== null;
157
+ },
158
+ complete(ctx) {
159
+ return ctx.outcome !== null || ctx.order !== null;
160
+ },
161
+ render({ facts }) {
162
+ return <ResumeView resume={facts} />;
163
+ },
164
+ };
165
+ }
166
+
167
+ /** The resumed order, taken up by whichever half of the walk owns it. */
168
+ function ResumeView({ resume }: { resume: HostedResume }): JSX.Element | null {
169
+ const actions = usePipelineActions();
170
+ const { adoptOrder, resolve } = actions;
171
+ const { order, step, status } = resume;
172
+ useEffect(() => {
173
+ if (!order) return;
174
+ // An on-page charge belongs to the WALK, not to this screen: adopting it
175
+ // makes the pane apply on the very next render.
176
+ if (step === "payment") adoptOrder(order);
177
+ else if (status !== null && status !== "AWAITING_PAYMENT") resolve(status);
178
+ }, [order, step, status, adoptOrder, resolve]);
179
+ if (!order || step === "payment") return null;
180
+ const PaymentStatus = actions.screens.PaymentStatus;
181
+ return <PaymentStatus status={status} payable={order} />;
182
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * THE CONFIRMATION (FUT-1240) — the one step every settlement reaches.
3
+ *
4
+ * `complete` is `false` forever: a walk whose last step is finished has
5
+ * nowhere to put the shopper, and this is where a checkout ends. The
6
+ * registered method may replace the screen entirely (`Confirmation` on its
7
+ * descriptor), which is how a settlement that raises no charge says
8
+ * "recebemos seu pedido" instead of "pagamento aprovado" without the engine
9
+ * knowing either sentence.
10
+ */
11
+ import type { JSX } from "react";
12
+
13
+ import { descriptorFor, usePipelineActions } from "../actions";
14
+ import type { CheckoutStep, CheckoutStepRender } from "../types";
15
+
16
+ export const STATUS_STEP_ID = "status";
17
+
18
+ function StatusView({ ctx }: CheckoutStepRender<void>): JSX.Element {
19
+ const actions = usePipelineActions();
20
+ const descriptor = descriptorFor(actions.methods, ctx);
21
+ const Confirmation = descriptor?.Confirmation;
22
+ if (Confirmation && ctx.order) return <Confirmation ctx={ctx} order={ctx.order} />;
23
+ const PaymentStatus = actions.screens.PaymentStatus;
24
+ return <PaymentStatus status={ctx.outcome} payable={ctx.order} />;
25
+ }
26
+
27
+ export const statusStep: CheckoutStep = {
28
+ id: STATUS_STEP_ID,
29
+ phase: "after-pay",
30
+ order: 0,
31
+ label: "status",
32
+ applies(ctx) {
33
+ return ctx.outcome !== null;
34
+ },
35
+ complete() {
36
+ return false;
37
+ },
38
+ render(props) {
39
+ return <StatusView {...props} />;
40
+ },
41
+ };
@@ -0,0 +1,232 @@
1
+ /**
2
+ * THE CHECKOUT PIPELINE'S VOCABULARY (FUT-1240, step 4 of FUT-1216).
3
+ *
4
+ * A checkout is a list of STEPS, a list of GATES and a list of SETTLEMENT
5
+ * METHODS. Each is a plain object in an array; registration is array
6
+ * membership and nothing else. The engine derives which step the shopper is
7
+ * on rather than remembering it, so a reload, a torn-down tab and a hand-off
8
+ * return all land where the server's own facts say they should.
9
+ *
10
+ * ## Two properties this file exists to keep
11
+ *
12
+ * 1. **Method syntax, never arrow properties.** Under `strictFunctionTypes` a
13
+ * method's parameters are BIVARIANT while a property holding a function is
14
+ * contravariant — so `CheckoutStep<S, F>` is assignable to
15
+ * {@link AnyCheckoutStep} with no `any` and no cast. Written as
16
+ * `applies: (ctx, facts) => boolean` instead, every registration would need
17
+ * a cast, and this repo forbids `any`.
18
+ * 2. **The package names no host.** Nothing here imports a host, a sibling
19
+ * workspace package or a domain word. A mode, a mesa, a comanda and a
20
+ * delivery address are all host concepts and reach the engine as a
21
+ * registered step or a registered method, never as a field.
22
+ */
23
+ import type { ComponentType, ReactNode } from "react";
24
+
25
+ import type { CheckoutCartView } from "../../components/checkout/checkout-flow";
26
+ import type {
27
+ BuyerInfo,
28
+ CheckoutError,
29
+ CheckoutOrder,
30
+ CheckoutProviderConfig,
31
+ CreateOrderRequest,
32
+ OrderStatus,
33
+ SettlementCheckout,
34
+ } from "../../components/checkout/types";
35
+ import type {
36
+ CheckoutStepperCopy,
37
+ CheckoutViewCopy,
38
+ } from "../../components/checkout/view-copy";
39
+
40
+ /**
41
+ * Where a step sits in the walk. The engine sorts by this first and by
42
+ * {@link CheckoutStep.order} second, so a host appends a step without knowing
43
+ * what else is registered.
44
+ *
45
+ * `pay` is the phase the no-charge rule is stated over: a settlement method
46
+ * that raises no charge mounts NO `pay` step, ever — see
47
+ * {@link SettlementMethodDescriptor.raisesCharge}.
48
+ */
49
+ export type CheckoutStepPhase = "details" | "before-pay" | "pay" | "after-pay";
50
+
51
+ /** Everything a step, a gate or a method may read. All of it server-owned or parked. */
52
+ export interface CheckoutContext {
53
+ tenantSlug?: string;
54
+ /** `GET /config`: chain, methods, tokenization. */
55
+ config: CheckoutProviderConfig | null;
56
+ configPending: boolean;
57
+ cart: CheckoutCartView;
58
+ /** Host-resolved balance, opaque scope. */
59
+ settlement: SettlementCheckout | null;
60
+ buyer: BuyerInfo;
61
+ taxIdOnFile: boolean;
62
+ /** A {@link SettlementMethodDescriptor.id}, or `null` before a choice. */
63
+ method: string | null;
64
+ /** Raised now, resumed from the server, or parked by a hand-off. */
65
+ order: CheckoutOrder | null;
66
+ outcome: OrderStatus | null;
67
+ intent: { oneClick: boolean; resuming: boolean; presetMethod: string | null };
68
+ /** Every step's declared slice, by step id. */
69
+ slices: Readonly<Record<string, unknown>>;
70
+ }
71
+
72
+ /**
73
+ * A step's own scrap of state, DECLARED rather than held.
74
+ *
75
+ * The engine owns the storage, which is what makes a reload survivable: a
76
+ * `session` slice is parked under `payments.checkout.<slug>.<stepId>` and
77
+ * rehydrated on mount. Nothing money-shaped belongs here — that is the order.
78
+ */
79
+ export interface StepSlice<S> {
80
+ initial(ctx: CheckoutContext): S;
81
+ /** `"session"` ⇒ `payments.checkout.<slug>.<stepId>`, rehydrated on mount. */
82
+ persist: "none" | "session";
83
+ /**
84
+ * Trust nothing that came back out of storage — the `hosted-return.ts` rule.
85
+ * A `session` slice with NO parser is never rehydrated: the engine cannot
86
+ * check a shape it has not been told, and a half-written value reaching a
87
+ * step as its state is exactly what that rule exists to stop.
88
+ */
89
+ parse?(raw: unknown): S | null;
90
+ }
91
+
92
+ /** What a step's `render` is handed. Its own type so the method syntax stays readable. */
93
+ export interface CheckoutStepRender<S> {
94
+ ctx: CheckoutContext;
95
+ slice: S;
96
+ setSlice(s: S): void;
97
+ /** Done here — clear any back-navigation and let the walk move on. */
98
+ advance(): void;
99
+ /** The previous applying step, or the host's catalog when there is none. */
100
+ back(): void;
101
+ /** A server refusal THIS step claimed through {@link CheckoutStep.answersCodes}. */
102
+ error: CheckoutError | null;
103
+ }
104
+
105
+ /** One step of the walk. */
106
+ export interface CheckoutStep<S = void, F = void> {
107
+ id: string;
108
+ phase: CheckoutStepPhase;
109
+ order?: number;
110
+ /** Host hook; runs EVERY render, in array order. */
111
+ useFacts?(): F;
112
+ /** Pure. */
113
+ applies(ctx: CheckoutContext, facts: F): boolean;
114
+ /** Pure — the current step is the first applying step whose answer is `false`. */
115
+ complete(ctx: CheckoutContext, facts: F, slice: S): boolean;
116
+ slice?: StepSlice<S>;
117
+ render(p: CheckoutStepRender<S> & { facts: F }): ReactNode;
118
+ /** Server refusal codes re-rendered HERE, never as a generic retry. */
119
+ answersCodes?: readonly string[];
120
+ contribute?(ctx: CheckoutContext, facts: F, slice: S): Partial<CreateOrderRequest>;
121
+ /** Absent ⇒ an interstitial, not a stepper node. */
122
+ label?: keyof CheckoutStepperCopy | null;
123
+ }
124
+
125
+ /** Any registered step. Reachable with no cast because every member above is a method. */
126
+ export type AnyCheckoutStep = CheckoutStep<unknown, unknown>;
127
+
128
+ /** What a gate decided about this shopper. */
129
+ export type GateVerdict =
130
+ | { kind: "pass" }
131
+ | { kind: "pending" }
132
+ | {
133
+ kind: "refuse";
134
+ Screen: ComponentType<{
135
+ ctx: CheckoutContext;
136
+ error: CheckoutError | null;
137
+ retry(): void;
138
+ }>;
139
+ };
140
+
141
+ /** Something that must be true before ANY step renders. */
142
+ export interface CheckoutGate<F = void> {
143
+ id: string;
144
+ useFacts?(): F;
145
+ /** Pure. */
146
+ decide(ctx: CheckoutContext, facts: F): GateVerdict;
147
+ answersCodes?: readonly string[];
148
+ /** Bypassed when a hand-off from this tab is still waiting to be resolved. */
149
+ standsAsideForResume?: boolean;
150
+ }
151
+
152
+ /** Any registered gate. */
153
+ export type AnyCheckoutGate = CheckoutGate<unknown>;
154
+
155
+ /**
156
+ * A way of settling — the seam a no-charge settlement never had.
157
+ *
158
+ * `raisesCharge: false` is the whole reason this type exists: placing the
159
+ * order IS the settlement, so there is no `/charge`, no poll, and no payment
160
+ * surface after the method is chosen. The engine enforces that ONCE, over the
161
+ * `pay` phase, so no lane can forget it.
162
+ */
163
+ export interface SettlementMethodDescriptor<F = void> {
164
+ /** `"PIX"`, `"CARD"`, `"ON_DELIVERY"`, `"WAITER"`, `"BOLETO"`… */
165
+ id: string;
166
+ /** `false` ⇒ no `/charge`, no poll; placing the order IS the settlement. */
167
+ raisesCharge: boolean;
168
+ /** The `pay`-phase step id shown once the order exists; `null` ⇒ Confirmation. */
169
+ pane: string | null;
170
+ useFacts?(): F;
171
+ offered(ctx: CheckoutContext, facts: F): boolean;
172
+ Review?: ComponentType<{
173
+ ctx: CheckoutContext;
174
+ place(): void;
175
+ placing: boolean;
176
+ error: CheckoutError | null;
177
+ }>;
178
+ /** Default: the package's own `PaymentStatus`. */
179
+ Confirmation?: ComponentType<{ ctx: CheckoutContext; order: CheckoutOrder }>;
180
+ tile(copy: CheckoutViewCopy): { label: string; hint?: string };
181
+ }
182
+
183
+ /** Any registered settlement method. */
184
+ export type AnySettlementMethod = SettlementMethodDescriptor<unknown>;
185
+
186
+ /**
187
+ * Where the shopper leaves for, and how.
188
+ *
189
+ * A hook for the label because it is the host's router that knows which
190
+ * catalog this checkout came from, and a value would freeze the first one.
191
+ */
192
+ export interface CheckoutExit {
193
+ useCatalog(): { to: string; label: string };
194
+ navigate(to: string): void;
195
+ }
196
+
197
+ /**
198
+ * The engine's half of `PaymentFlowsConfig` — every key OPTIONAL.
199
+ *
200
+ * Setting ANY of them switches `Checkout` onto the pipeline. Setting NONE
201
+ * leaves it rendering exactly today's `CheckoutFlow`, which is the whole
202
+ * meaning of this step being additive.
203
+ */
204
+ export interface CheckoutPipelineConfig {
205
+ /** Merged with the package's own, by phase then `order`. */
206
+ steps?: readonly AnyCheckoutStep[];
207
+ /** Evaluated in order before any step renders. */
208
+ gates?: readonly AnyCheckoutGate[];
209
+ /** Merged with the package's PIX + CARD. */
210
+ settlementMethods?: readonly AnySettlementMethod[];
211
+ /** The HOST reads its own URL; the package reads nothing. */
212
+ useIntent?(): CheckoutContext["intent"];
213
+ /** Resume from the SERVER, not only from a parked hand-off. */
214
+ useOpenPayable?(): { order: CheckoutOrder | null; pending: boolean };
215
+ /** Absent ⇒ `ports.exitToCatalog()`, which is what every host wires today. */
216
+ exit?: CheckoutExit;
217
+ /** The host invalidates ITS keys. */
218
+ onSettled?(outcome: OrderStatus, order: CheckoutOrder): void;
219
+ }
220
+
221
+ /** Whether a config asked for the pipeline at all. */
222
+ export function pipelineRequested(config: CheckoutPipelineConfig): boolean {
223
+ return (
224
+ config.steps !== undefined ||
225
+ config.gates !== undefined ||
226
+ config.settlementMethods !== undefined ||
227
+ config.useIntent !== undefined ||
228
+ config.useOpenPayable !== undefined ||
229
+ config.exit !== undefined ||
230
+ config.onSettled !== undefined
231
+ );
232
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * THE FACTORY'S PUBLIC SURFACE — `createPaymentFlows` (FUT-741) and the
3
+ * checkout pipeline it can now run (FUT-1240).
4
+ *
5
+ * Listed here rather than inline in `src/index.ts` for the reason
6
+ * `./activation/public.ts` already gives: the root barrel is at its size gate,
7
+ * and a surface that grows with every plugin type is one that will keep
8
+ * growing. Everything below is re-exported verbatim by the root.
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // The MOUNTED buyer checkout (FUT-741) — `createPaymentFlows` returns every
13
+ // screen pre-bound to one transport, one scope, one slot table and one set of
14
+ // host ports. Additive: the hand-composing path is unchanged.
15
+ // ---------------------------------------------------------------------------
16
+ export { createPaymentFlows } from './create-payment-flows';
17
+ // A type and nothing else now: `DEFAULT_CHECKOUT_COPY_FE` used to sit beside it
18
+ // and was the only value this module ever published (FUT-760).
19
+ export type { CheckoutCopyFE } from './copy';
20
+ export {
21
+ type BoundCheckoutClient,
22
+ type BuyerDetailsProps,
23
+ type CheckoutAvailability,
24
+ type CheckoutConfigState,
25
+ type CheckoutController,
26
+ type CheckoutPorts,
27
+ type CheckoutScreens,
28
+ type PaymentFlows,
29
+ type PaymentFlowsConfig,
30
+ } from './types';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // The checkout PIPELINE (FUT-1240) — steps, gates and settlement methods as
34
+ // registered plugins, with the current step DERIVED from the server's own
35
+ // facts rather than held in a `useState` a reload forgets.
36
+ //
37
+ // ADDITIVE: a host that registers none of these gets exactly the flat
38
+ // three-step `CheckoutFlow`, unchanged, down to its test ids. What the types
39
+ // below buy is the ability to say "this store has a step of its own", "this
40
+ // shopper may not check out yet", and "this settlement raises no charge" — the
41
+ // last of which the package had no seam for at all, so every in-person payment
42
+ // path was written beside the checkout instead of inside it.
43
+ // ---------------------------------------------------------------------------
44
+ export type {
45
+ AnyCheckoutGate,
46
+ AnyCheckoutStep,
47
+ AnySettlementMethod,
48
+ CheckoutContext,
49
+ CheckoutExit,
50
+ CheckoutGate,
51
+ CheckoutPipelineConfig,
52
+ CheckoutStep,
53
+ CheckoutStepPhase,
54
+ CheckoutStepRender,
55
+ GateVerdict,
56
+ SettlementMethodDescriptor,
57
+ StepSlice,
58
+ } from './pipeline/types';
59
+ /**
60
+ * The step ids the PACKAGE registers, so a host ordering its own steps around
61
+ * them names a constant instead of retyping a string that can change.
62
+ */
63
+ export {
64
+ DADOS_STEP_ID,
65
+ HANDOFF_STEP_ID,
66
+ METHOD_STEP_ID,
67
+ RESUME_STEP_ID,
68
+ STATUS_STEP_ID,
69
+ } from './pipeline/steps';
70
+ export { CARD_PANE_STEP, PIX_PANE_STEP } from './pipeline/methods';
71
+ /**
72
+ * Where a step's parked slice lives — `payments.checkout.<slug>.<stepId>`.
73
+ * Public for the same reason `HOSTED_ORDER_STORAGE_KEY` is: a host clearing
74
+ * storage on sign-out, or a spec asserting a resume, otherwise retypes it.
75
+ */
76
+ export { sliceKey } from './pipeline/slices';
77
+ /** Whose plugin answers a refusal code — the routing, without the renderer. */
78
+ export { refusalOwner, type RefusalOwner } from './pipeline/refusal-routing';
@@ -23,6 +23,7 @@
23
23
  import { Box } from "@mui/material";
24
24
  import { useEffect, useState, type JSX } from "react";
25
25
 
26
+ import { parkedBasket, type CheckoutBasketIdentity } from "../components/checkout/basket";
26
27
  import { rememberHostedOrder, takeHostedOrder } from "../components/checkout/hosted-return";
27
28
  import type { CheckoutOrder, OrderStatus } from "../components/checkout/types";
28
29
  import { useCheckoutComponents } from "../components/checkout/ui";
@@ -31,18 +32,45 @@ import { usePaymentPolling } from "../components/checkout/use-payment-polling";
31
32
  import { FlowsShell, type FlowsRuntime } from "./runtime";
32
33
  import type { CheckoutScreens } from "./types";
33
34
 
35
+ /** Whose store and which basket this hand-off belongs to. */
36
+ interface HandoverScope {
37
+ tenantSlug?: string;
38
+ basket?: CheckoutBasketIdentity;
39
+ }
40
+
34
41
  /** Park the order, then navigate — exactly once, even under StrictMode. */
35
- function useHandover(payable: CheckoutOrder, url: string, navigate: (url: string) => void): void {
42
+ function useHandover(
43
+ payable: CheckoutOrder,
44
+ url: string,
45
+ navigate: (url: string) => void,
46
+ /**
47
+ * PARKED WITH THE STORE AND THE BASKET (FUT-1240).
48
+ *
49
+ * This call site named neither, and both absences are read as "no opinion"
50
+ * by the resume: `belongsHere` passes a slug-less entry at ANY store and the
51
+ * basket rule passes a basket-less entry against ANY basket. So the one
52
+ * hand-off screen this package ships was exempt from the multi-tenant
53
+ * scoping (FUT-556) and from the basket binding (FUT-1213) — it resumed over
54
+ * whatever checkout mounted next. The other three parks were fixed one at a
55
+ * time; this is the last of them.
56
+ */
57
+ scope: HandoverScope,
58
+ ): void {
36
59
  const [done, setDone] = useState(false);
37
60
  useEffect(() => {
38
61
  if (done) return;
39
62
  setDone(true);
63
+ const basket = parkedBasket(scope.basket);
40
64
  // `handoff: true` is what tells the return leg this order was finished on
41
65
  // ANOTHER SITE (FUT-1140): only such an order resumes onto the confirmation
42
66
  // screen, because only such an order has nothing left on our page to show.
43
- rememberHostedOrder(payable, { handoff: true });
67
+ rememberHostedOrder(payable, {
68
+ ...(scope.tenantSlug === undefined ? {} : { tenantSlug: scope.tenantSlug }),
69
+ ...(basket === undefined ? {} : { basket }),
70
+ handoff: true,
71
+ });
44
72
  navigate(url);
45
- }, [done, payable, url, navigate]);
73
+ }, [done, payable, url, navigate, scope.tenantSlug, scope.basket]);
46
74
  }
47
75
 
48
76
  function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHandoff"] {
@@ -57,7 +85,12 @@ function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHando
57
85
  }): JSX.Element {
58
86
  const { Button, LoadingState, Text } = useCheckoutComponents();
59
87
  const copy = runtime.copy;
60
- useHandover(payable, url, runtime.navigate);
88
+ const tenantSlug = runtime.useTenantSlug();
89
+ const cart = runtime.config.useCart();
90
+ useHandover(payable, url, runtime.navigate, {
91
+ ...(tenantSlug === undefined ? {} : { tenantSlug }),
92
+ ...(cart.identity === undefined ? {} : { basket: cart.identity }),
93
+ });
61
94
  return (
62
95
  <Box
63
96
  data-testid="checkout-hosted-handoff"
@@ -15,6 +15,7 @@ import { PixView } from "../components/checkout/pix-view";
15
15
  import type { CheckoutOrder, OrderStatus } from "../components/checkout/types";
16
16
  import { useCheckoutComponents } from "../components/checkout/ui";
17
17
 
18
+ import { useCatalogExit } from "./catalog-exit";
18
19
  import { FlowsShell, useResolvedConfig, type FlowsRuntime } from "./runtime";
19
20
  import type { CheckoutScreens } from "./types";
20
21
 
@@ -79,6 +80,10 @@ function buildPaymentStatus(runtime: FlowsRuntime): CheckoutScreens["PaymentStat
79
80
  status: OrderStatus | null;
80
81
  payable?: CheckoutOrder | null;
81
82
  }) {
83
+ // The SAME door the chrome's back link takes (FUT-1240). Bound straight to
84
+ // the port, this control ignored a registered `exit` — so one host got its
85
+ // router from one way out of the checkout and a page load from the other.
86
+ const backToMenu = useCatalogExit(runtime);
82
87
  return (
83
88
  <FlowsShell runtime={runtime}>
84
89
  <PaymentStatusView
@@ -86,7 +91,7 @@ function buildPaymentStatus(runtime: FlowsRuntime): CheckoutScreens["PaymentStat
86
91
  status={status}
87
92
  totalLabel={payable?.totalLabel ?? ""}
88
93
  orderId={payable?.orderId}
89
- onBackToMenu={runtime.config.ports.exitToCatalog}
94
+ onBackToMenu={backToMenu}
90
95
  paidExtra={runtime.config.confirmation?.extra}
91
96
  />
92
97
  </FlowsShell>
@@ -45,6 +45,7 @@ import type { useCheckoutController } from "../components/checkout/use-checkout-
45
45
  import type { Result } from "../result";
46
46
 
47
47
  import type { CheckoutCopyFE } from "./copy";
48
+ import type { CheckoutPipelineConfig, GateVerdict } from "./pipeline/types";
48
49
 
49
50
  /** The host's remedy on the unavailable screen, and its veto over a live chain. */
50
51
  export interface CheckoutAvailability {
@@ -88,8 +89,17 @@ export interface CheckoutPorts {
88
89
  useAvailability?(): CheckoutAvailability;
89
90
  }
90
91
 
91
- /** What `createPaymentFlows` is configured with. */
92
- export interface PaymentFlowsConfig {
92
+ /**
93
+ * What `createPaymentFlows` is configured with.
94
+ *
95
+ * It extends {@link CheckoutPipelineConfig}, whose every key is OPTIONAL: set
96
+ * one and `Checkout` renders through the checkout PIPELINE (FUT-1240); set
97
+ * none and it renders exactly the `CheckoutFlow` it always did, with the same
98
+ * pixels and the same test ids. That is the whole meaning of the pipeline
99
+ * landing additively — a host adopts it when it has a step, a gate or a
100
+ * settlement method of its own to register, and not a release earlier.
101
+ */
102
+ export interface PaymentFlowsConfig extends CheckoutPipelineConfig {
93
103
  /** Where the `createPaymentFlowsBE` mount lives. Default `/api/checkout`. */
94
104
  transport?: CheckoutTransportBinding;
95
105
 
@@ -209,6 +219,19 @@ export interface PaymentFlows {
209
219
  screens: CheckoutScreens;
210
220
  /** The flow controller, pre-bound to the ports — the easy path is not the only path. */
211
221
  useCheckout(): CheckoutController;
222
+ /**
223
+ * MAY THIS SHOPPER CHECK OUT — the registered gate list, headless (FUT-1240).
224
+ *
225
+ * The same list, in the same order, that `Checkout` runs before it renders
226
+ * anything. Exported so the surfaces that OFFER a checkout — a cart
227
+ * drawer's CTA, a buy-now button — consume the answer the checkout will
228
+ * give rather than a second one of their own. Two surfaces deciding this
229
+ * separately is how a storefront ends up offering a checkout the checkout
230
+ * itself refuses.
231
+ *
232
+ * A host that registered no gates always passes.
233
+ */
234
+ useAdmission(): GateVerdict;
212
235
  useCheckoutConfig(): CheckoutConfigState;
213
236
  client: BoundCheckoutClient;
214
237
  }