@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.
- package/package.json +2 -2
- package/src/components/checkout/checkout-actions.ts +55 -9
- package/src/components/checkout/checkout-flow.tsx +15 -7
- package/src/components/checkout/confirmation-wait.ts +97 -0
- package/src/components/checkout/en-US.ts +8 -0
- package/src/components/checkout/failure-codes.ts +82 -0
- package/src/components/checkout/hosted-store.ts +23 -1
- package/src/components/checkout/payment-error-panel.tsx +9 -3
- package/src/components/checkout/pix-view.tsx +97 -8
- package/src/components/checkout/poll-loop.ts +5 -3
- package/src/components/checkout/pt-BR.ts +9 -0
- package/src/components/checkout/types.ts +11 -0
- package/src/components/checkout/use-card-checkout.ts +3 -2
- package/src/components/checkout/use-checkout-controller.ts +19 -5
- 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 +38 -0
- package/src/components/checkout/wallet-pane.tsx +6 -1
- package/src/flows/catalog-exit.ts +33 -0
- package/src/flows/create-payment-flows.tsx +12 -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 +37 -4
- package/src/flows/screens-pay.tsx +6 -1
- package/src/flows/types.ts +25 -2
- 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(
|
|
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, {
|
|
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
|
-
|
|
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={
|
|
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
|
}
|