@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
|
@@ -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,15 +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(
|
|
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);
|
|
40
|
-
|
|
63
|
+
const basket = parkedBasket(scope.basket);
|
|
64
|
+
// `handoff: true` is what tells the return leg this order was finished on
|
|
65
|
+
// ANOTHER SITE (FUT-1140): only such an order resumes onto the confirmation
|
|
66
|
+
// screen, because only such an order has nothing left on our page to show.
|
|
67
|
+
rememberHostedOrder(payable, {
|
|
68
|
+
...(scope.tenantSlug === undefined ? {} : { tenantSlug: scope.tenantSlug }),
|
|
69
|
+
...(basket === undefined ? {} : { basket }),
|
|
70
|
+
handoff: true,
|
|
71
|
+
});
|
|
41
72
|
navigate(url);
|
|
42
|
-
}, [done, payable, url, navigate]);
|
|
73
|
+
}, [done, payable, url, navigate, scope.tenantSlug, scope.basket]);
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHandoff"] {
|
|
@@ -54,7 +85,12 @@ function buildHostedHandoff(runtime: FlowsRuntime): CheckoutScreens["HostedHando
|
|
|
54
85
|
}): JSX.Element {
|
|
55
86
|
const { Button, LoadingState, Text } = useCheckoutComponents();
|
|
56
87
|
const copy = runtime.copy;
|
|
57
|
-
|
|
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
|
+
});
|
|
58
94
|
return (
|
|
59
95
|
<Box
|
|
60
96
|
data-testid="checkout-hosted-handoff"
|
|
@@ -163,6 +199,20 @@ function ReturnStalled({
|
|
|
163
199
|
);
|
|
164
200
|
}
|
|
165
201
|
|
|
202
|
+
/**
|
|
203
|
+
* The parked order this return trip is for, or null.
|
|
204
|
+
*
|
|
205
|
+
* This screen is mounted at the host's RETURN route, so it carries no basket to
|
|
206
|
+
* weigh the entry against and the FUT-1213 rule cannot ask for one: with no
|
|
207
|
+
* basket named the decision is the pre-1213 one — resume what was parked —
|
|
208
|
+
* which is the right answer for a URL that exists only for buyers coming back
|
|
209
|
+
* from a payment. `ASK` is unreachable from here.
|
|
210
|
+
*/
|
|
211
|
+
function resumeParked(): CheckoutOrder | null {
|
|
212
|
+
const decision = takeHostedOrder();
|
|
213
|
+
return decision.verdict === "RESUME" ? decision.order : null;
|
|
214
|
+
}
|
|
215
|
+
|
|
166
216
|
function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn"] {
|
|
167
217
|
function HostedReturnBody({
|
|
168
218
|
onResolved,
|
|
@@ -172,7 +222,7 @@ function buildHostedReturn(runtime: FlowsRuntime): CheckoutScreens["HostedReturn
|
|
|
172
222
|
const { Alert, LoadingState } = useCheckoutComponents();
|
|
173
223
|
// Read-and-clear, once, on first render: the resumed view belongs to
|
|
174
224
|
// exactly one return trip.
|
|
175
|
-
const [parked] = useState(
|
|
225
|
+
const [parked] = useState(resumeParked);
|
|
176
226
|
// Bounded, for the reason on RETURN_WINDOW_MS: nothing here can ever reach a
|
|
177
227
|
// terminal state on its own, so an unbounded poll is a spinner the buyer
|
|
178
228
|
// watches until they close the tab.
|
|
@@ -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={
|
|
94
|
+
onBackToMenu={backToMenu}
|
|
90
95
|
paidExtra={runtime.config.confirmation?.extra}
|
|
91
96
|
/>
|
|
92
97
|
</FlowsShell>
|
package/src/flows/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
92
|
-
|
|
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
|
}
|
package/src/index.ts
CHANGED
|
@@ -61,26 +61,14 @@ export {
|
|
|
61
61
|
} from './components/checkout/checkout-flow';
|
|
62
62
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
|
-
// The MOUNTED buyer checkout (FUT-741)
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
64
|
+
// The MOUNTED buyer checkout (FUT-741) and the checkout PIPELINE (FUT-1240).
|
|
65
|
+
//
|
|
66
|
+
// Listed in `./flows/public` rather than inline, on the `./activation/public`
|
|
67
|
+
// precedent below: this barrel is at its size gate, and a surface that grows
|
|
68
|
+
// with every plugin type belongs beside the plugins.
|
|
68
69
|
// ---------------------------------------------------------------------------
|
|
69
|
-
export
|
|
70
|
-
|
|
71
|
-
// and was the only value this module ever published (FUT-760).
|
|
72
|
-
export type { CheckoutCopyFE } from './flows/copy';
|
|
73
|
-
export {
|
|
74
|
-
type BoundCheckoutClient,
|
|
75
|
-
type BuyerDetailsProps,
|
|
76
|
-
type CheckoutAvailability,
|
|
77
|
-
type CheckoutConfigState,
|
|
78
|
-
type CheckoutController,
|
|
79
|
-
type CheckoutPorts,
|
|
80
|
-
type CheckoutScreens,
|
|
81
|
-
type PaymentFlows,
|
|
82
|
-
type PaymentFlowsConfig,
|
|
83
|
-
} from './flows/types';
|
|
70
|
+
export * from './flows/public';
|
|
71
|
+
|
|
84
72
|
export {
|
|
85
73
|
buyerFieldsFor,
|
|
86
74
|
fieldSatisfied,
|
|
@@ -198,6 +186,28 @@ export {
|
|
|
198
186
|
type CheckoutStepperStep,
|
|
199
187
|
type CheckoutTextProps,
|
|
200
188
|
} from './components/checkout/ui';
|
|
189
|
+
/**
|
|
190
|
+
* WHICH basket a checkout is for (FUT-1213).
|
|
191
|
+
*
|
|
192
|
+
* A host computes the signature from its own cart lines and hands it to the
|
|
193
|
+
* flow on `cart.identity`, so a payment raised from an ABANDONED basket cannot
|
|
194
|
+
* resume itself over the one the shopper is holding now. Exported because the
|
|
195
|
+
* host owns the cart and therefore has to build it — see `basket.ts` for why
|
|
196
|
+
* the identity is the lines and never the cart's id.
|
|
197
|
+
*/
|
|
198
|
+
export {
|
|
199
|
+
basketSignature,
|
|
200
|
+
type CheckoutBasketIdentity,
|
|
201
|
+
type CheckoutBasketLine,
|
|
202
|
+
} from './components/checkout/basket';
|
|
203
|
+
/**
|
|
204
|
+
* WHY a card was refused, and whether another attempt could work (FUT-1145).
|
|
205
|
+
* A host wiring the confirmation screen's per-reason copy names these.
|
|
206
|
+
*/
|
|
207
|
+
export type {
|
|
208
|
+
CheckoutDecline,
|
|
209
|
+
CheckoutDeclineReason,
|
|
210
|
+
} from './components/checkout/decline';
|
|
201
211
|
export {
|
|
202
212
|
type BuyerContact,
|
|
203
213
|
type BuyerField,
|