@12-apps/payments-frontend 1.21.0 → 2.0.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/ProviderConnection.tsx +14 -1
- package/src/components/checkout/checkout-flow.tsx +18 -18
- package/src/components/checkout/checkout-steps.tsx +8 -8
- package/src/components/checkout/hosted-return.ts +53 -7
- package/src/components/checkout/types.ts +20 -8
- package/src/components/checkout/use-checkout-controller.ts +4 -4
- package/src/flows/copy.ts +7 -32
- package/src/flows/create-payment-flows.tsx +6 -7
- package/src/flows/types.ts +18 -5
- package/src/index.ts +8 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/payments-frontend",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Browser half of the vendor-agnostic payments platform: plug-and-play MUI components for the per-provider settings page (credential form from each provider's schema, masked hints, verify/enable) and the checkout page (PIX QR + polling, card tokenization, hosted-checkout redirect), plus the headless hooks and fetch clients they build on. Talks only to the host's payments HTTP surface — never to a provider directly. Microfrontend-ready: no app coupling, host injects theme and auth.",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"storybook:build": "storybook build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@12-apps/payments-backend": "^
|
|
20
|
+
"@12-apps/payments-backend": "^3.0.0",
|
|
21
21
|
"react-qr-code": "^2.2.0"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
@@ -163,9 +163,22 @@ function ConnectionSummary(props: {
|
|
|
163
163
|
/>
|
|
164
164
|
</Stack>
|
|
165
165
|
) : null}
|
|
166
|
+
{/*
|
|
167
|
+
The connected sentence names NO platform. It used to open with one
|
|
168
|
+
adopter's product name, hard-coded — so every other host installing this
|
|
169
|
+
package told its own merchants that somebody else's product was creating
|
|
170
|
+
their charges, with no prop to change it. The tell that it was an
|
|
171
|
+
oversight rather than a decision is the branch right below, which already
|
|
172
|
+
templates the provider from props.
|
|
173
|
+
|
|
174
|
+
Nothing was lost by removing it. The sentence exists to answer ONE
|
|
175
|
+
question — why no API key had to be copied — and the answer is the OAuth
|
|
176
|
+
grant, not whose logo is on the page. The subject is the connection
|
|
177
|
+
itself, which is true for every host and needs no new configuration.
|
|
178
|
+
*/}
|
|
166
179
|
<Typography variant="body2" color="text.secondary">
|
|
167
180
|
{props.connected
|
|
168
|
-
? 'Sua conta está conectada.
|
|
181
|
+
? 'Sua conta está conectada. As cobranças são criadas em seu nome — nenhuma chave precisa ser copiada.'
|
|
169
182
|
: `Conecte sua conta ${props.displayName} autorizando o acesso no site do provedor. Nenhuma chave precisa ser copiada.`}
|
|
170
183
|
</Typography>
|
|
171
184
|
{props.connected && props.connectedAccount ? (
|
|
@@ -5,7 +5,7 @@ import { buyerFieldsFor } from "./buyer-fields";
|
|
|
5
5
|
import { DadosStep, EmptyCart, PaymentStep } from "./checkout-steps";
|
|
6
6
|
import { ArrowBackIcon } from "./icons";
|
|
7
7
|
import { PaymentStatus } from "./payment-status";
|
|
8
|
-
import type { BuyerInfo, CheckoutProviderConfig,
|
|
8
|
+
import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
|
|
9
9
|
import { CheckoutComponentsProvider, useCheckoutComponents, type CheckoutComponents } from "./ui";
|
|
10
10
|
import { useCheckoutController, type CheckoutHostPorts } from "./use-checkout-controller";
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ const STEPPER_STEPS = [
|
|
|
17
17
|
|
|
18
18
|
/** What the flow reads off the host's cart — display facts, never money math. */
|
|
19
19
|
export interface CheckoutCartView {
|
|
20
|
-
/** Nothing to check out (cart mode only; a
|
|
20
|
+
/** Nothing to check out (cart mode only; a settlement settlement ignores it). */
|
|
21
21
|
empty: boolean;
|
|
22
22
|
totalLabel: string;
|
|
23
23
|
totalItems: number;
|
|
@@ -30,7 +30,7 @@ export interface CheckoutCartView {
|
|
|
30
30
|
* three-step flow — Dados → Pagamento → Confirmação — with the payment step
|
|
31
31
|
* speaking the store's ACTIVE provider protocol (PagBank PIX + card, Stone
|
|
32
32
|
* card, InfinitePay hosted redirect) against the host-mounted `/api/checkout*`
|
|
33
|
-
* surface. Cart, catalog,
|
|
33
|
+
* surface. Cart, catalog, settlement and order CREATION stay in the host and
|
|
34
34
|
* arrive through {@link CheckoutHostPorts} + {@link CheckoutCartView};
|
|
35
35
|
* pixels render through the slot contract (`components`, see `ui.tsx`).
|
|
36
36
|
*/
|
|
@@ -38,8 +38,8 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
38
38
|
/** The host's cart, reduced to what the flow displays. */
|
|
39
39
|
cart: CheckoutCartView;
|
|
40
40
|
defaultBuyer?: BuyerInfo;
|
|
41
|
-
/** Present ⇒ this checkout settles
|
|
42
|
-
|
|
41
|
+
/** Present ⇒ this checkout settles an open balance, not the cart . */
|
|
42
|
+
settlement?: SettlementCheckout | null;
|
|
43
43
|
/** The buyer has a CPF saved ⇒ open on Pagamento, skipping Dados (FUT-465). */
|
|
44
44
|
taxIdOnFile?: boolean;
|
|
45
45
|
/** The store's active payment protocol (FUT-697); absent while loading. */
|
|
@@ -51,7 +51,7 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
51
51
|
* session's `validationURL` for an Apple merchant session, SERVER-SIDE.
|
|
52
52
|
* Optional — without it the Apple Pay sheet cannot start, and the card form
|
|
53
53
|
* remains the way to pay.
|
|
54
|
-
|
|
54
|
+
*/
|
|
55
55
|
validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
|
|
56
56
|
/** Host content shown on the paid confirmation (the storefront's install invite). */
|
|
57
57
|
confirmationExtra?: ReactNode;
|
|
@@ -59,11 +59,11 @@ export interface CheckoutFlowProps extends CheckoutHostPorts {
|
|
|
59
59
|
components?: Partial<CheckoutComponents>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/** The pay-bar total override when settling a
|
|
63
|
-
function
|
|
64
|
-
|
|
62
|
+
/** The pay-bar total override when settling a settlement (else the cart's own totals). */
|
|
63
|
+
function settlementTotalOverride(
|
|
64
|
+
settlement: SettlementCheckout | null | undefined,
|
|
65
65
|
): { label: string; items: number } | undefined {
|
|
66
|
-
return
|
|
66
|
+
return settlement ? { label: settlement.totalLabel, items: settlement.totalItems } : undefined;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -78,13 +78,13 @@ function confirmationFacts(
|
|
|
78
78
|
return { orderId: order?.orderId, buyerEmail: buyer.email };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** The confirmation total: the created order's, else the
|
|
81
|
+
/** The confirmation total: the created order's, else the settlement scope's, else the cart's. */
|
|
82
82
|
function statusTotalLabel(
|
|
83
83
|
order: { totalLabel: string } | null,
|
|
84
|
-
|
|
84
|
+
settlement: SettlementCheckout | null | undefined,
|
|
85
85
|
cart: { totalLabel: string },
|
|
86
86
|
): string {
|
|
87
|
-
return order?.totalLabel ??
|
|
87
|
+
return order?.totalLabel ?? settlement?.totalLabel ?? cart.totalLabel;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
@@ -125,7 +125,7 @@ function ProgressHeader({ step, completed }: { step: string; completed: Set<stri
|
|
|
125
125
|
* card public key are loaded lazily by the card path (order-scoped REST).
|
|
126
126
|
*/
|
|
127
127
|
function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Element {
|
|
128
|
-
const { cart, defaultBuyer,
|
|
128
|
+
const { cart, defaultBuyer, settlement, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, ...ports } = props;
|
|
129
129
|
// Resolved for NO method on purpose (FUT-595): the Dados step opens before
|
|
130
130
|
// the picker, and the form is filled once — so it asks for the union of what
|
|
131
131
|
// any chain member may need rather than re-opening after the choice. A chain
|
|
@@ -133,14 +133,14 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
133
133
|
const buyerFields = useMemo(() => buyerFieldsFor(providerConfig?.chain, null), [providerConfig]);
|
|
134
134
|
const c = useCheckoutController(ports, defaultBuyer, taxIdOnFile, buyerFields);
|
|
135
135
|
|
|
136
|
-
// A
|
|
136
|
+
// A settlement settlement pays already-sent kitchen items — the cart is
|
|
137
137
|
// legitimately empty here, so the empty-cart guard only applies to cart mode.
|
|
138
138
|
//
|
|
139
139
|
// The guard cannot key on the Dados step any more: skipping it (FUT-465) makes
|
|
140
140
|
// Pagamento the first screen, so an empty cart would otherwise reach the
|
|
141
141
|
// method picker. It holds until an order exists — once one does, its lines are
|
|
142
142
|
// snapshotted server-side and the cart no longer speaks for it.
|
|
143
|
-
if (!
|
|
143
|
+
if (!settlement && cart.empty && !c.order && c.step !== "status") {
|
|
144
144
|
return <EmptyCart onBack={c.goToMenu} />;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -162,7 +162,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
162
162
|
cartTotals={cart}
|
|
163
163
|
buyerFields={buyerFields}
|
|
164
164
|
discountLines={cart.discountLines}
|
|
165
|
-
totalOverride={
|
|
165
|
+
totalOverride={settlementTotalOverride(settlement)}
|
|
166
166
|
/>
|
|
167
167
|
) : null}
|
|
168
168
|
|
|
@@ -191,7 +191,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
|
|
|
191
191
|
{c.step === "status" ? (
|
|
192
192
|
<PaymentStatus
|
|
193
193
|
status={c.finalStatus}
|
|
194
|
-
totalLabel={statusTotalLabel(c.order,
|
|
194
|
+
totalLabel={statusTotalLabel(c.order, settlement, cart)}
|
|
195
195
|
{...confirmationFacts(c.order, c.buyer)}
|
|
196
196
|
onRetry={c.retry}
|
|
197
197
|
onRegenerate={() => { c.setStep("payment"); void c.startPayment("PIX"); }}
|
|
@@ -113,8 +113,8 @@ export function EmptyCart({ onBack }: { onBack: () => void }): JSX.Element {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* The totals shown on the pay bar: the
|
|
117
|
-
*
|
|
116
|
+
* The totals shown on the pay bar: the settled balance's when settling a settlement
|
|
117
|
+
* one, otherwise the cart's own — both supplied by the host, which
|
|
118
118
|
* is the only side that knows either.
|
|
119
119
|
*/
|
|
120
120
|
function displayTotals(
|
|
@@ -184,9 +184,9 @@ export function DadosStep({
|
|
|
184
184
|
* (FUT-246) — RENDERED BY THE HOST from its cart (the storefront passes its
|
|
185
185
|
* cart footer's money block), never re-implemented here, so the two surfaces
|
|
186
186
|
* can never word the same discount differently.
|
|
187
|
-
|
|
187
|
+
*/
|
|
188
188
|
discountLines?: ReactNode;
|
|
189
|
-
/**
|
|
189
|
+
/** Settling an open balance: totals come from the settlement, not the cart. */
|
|
190
190
|
totalOverride?: { label: string; items: number };
|
|
191
191
|
}): JSX.Element {
|
|
192
192
|
const { Checkbox } = useCheckoutComponents();
|
|
@@ -215,7 +215,7 @@ export function DadosStep({
|
|
|
215
215
|
createError={createError}
|
|
216
216
|
onContinue={onContinue}
|
|
217
217
|
>
|
|
218
|
-
{/* Suppressed while settling a
|
|
218
|
+
{/* Suppressed while settling a balance: those totals come from the
|
|
219
219
|
frozen ticket, not the cart. */}
|
|
220
220
|
{totalOverride ? null : discountLines}
|
|
221
221
|
</DadosPayBar>
|
|
@@ -275,21 +275,21 @@ interface PaymentStepProps {
|
|
|
275
275
|
/**
|
|
276
276
|
* The refusal's machine code (FUT-563). An UNRESOLVED charge is not a failed
|
|
277
277
|
* one — the panel below must not offer to raise a second.
|
|
278
|
-
|
|
278
|
+
*/
|
|
279
279
|
errorCode?: string | null;
|
|
280
280
|
onGenerate: (method: PaymentMethod) => void;
|
|
281
281
|
onUseEmail: (email: string) => void;
|
|
282
282
|
/**
|
|
283
283
|
* Present ⇒ the buyer reached this step without a Dados step (FUT-465), so
|
|
284
284
|
* the payer block states who is being charged and reopens Dados to change it.
|
|
285
|
-
|
|
285
|
+
*/
|
|
286
286
|
onEditBuyer?: () => void;
|
|
287
287
|
/**
|
|
288
288
|
* The store's active payment protocol (`GET /api/checkout/config`, FUT-697).
|
|
289
289
|
* `null` while loading or on a transient fetch failure — methods then render
|
|
290
290
|
* as before and the card path degrades to the PagBank per-order key refresh,
|
|
291
291
|
* WITHOUT mock permission (fail-open for the UI, fail-closed for the money).
|
|
292
|
-
|
|
292
|
+
*/
|
|
293
293
|
providerConfig?: CheckoutProviderConfig | null;
|
|
294
294
|
/** Scopes the saved-card list to the store being paid (host routing owns it). */
|
|
295
295
|
tenantSlug?: string;
|
|
@@ -18,7 +18,31 @@ import type { CheckoutOrder } from "./types";
|
|
|
18
18
|
* unrelated session.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Where the parked order lives, namespaced to this PACKAGE.
|
|
23
|
+
*
|
|
24
|
+
* It used to carry one adopter's brand as its namespace, written into every
|
|
25
|
+
* adopter's browser. A storage key is not a private detail: it is observable
|
|
26
|
+
* surface, asserted on by `@12-apps/payments-e2e` and visible in devtools to
|
|
27
|
+
* anyone running the host. The sibling handover in this same folder already got
|
|
28
|
+
* this right with a `payments:` prefix; this one did not.
|
|
29
|
+
*
|
|
30
|
+
* Exported so a host or a spec names it rather than retyping it.
|
|
31
|
+
*/
|
|
32
|
+
export const HOSTED_ORDER_STORAGE_KEY = "payments.checkout.hostedOrder";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The key before the rename, READ ONLY.
|
|
36
|
+
*
|
|
37
|
+
* A buyer who left for the provider's page on the old bundle comes back to the
|
|
38
|
+
* new one with their order parked under the old name. Without this they land on
|
|
39
|
+
* the plain return screen — the order still settles, because the webhook does
|
|
40
|
+
* that and never depended on any of this, but the confirmation they were
|
|
41
|
+
* promised is missing for a reason they could not possibly understand.
|
|
42
|
+
*
|
|
43
|
+
* Delete once no session can still be mid-redirect across that deploy.
|
|
44
|
+
*/
|
|
45
|
+
const LEGACY_KEY = "futurepay.checkout.hostedOrder";
|
|
22
46
|
|
|
23
47
|
/**
|
|
24
48
|
* What a hosted provider appends to the return URL. InfinitePay sends the
|
|
@@ -46,7 +70,7 @@ function isReturnTrip(): boolean {
|
|
|
46
70
|
/** Park the raised order before handing the buyer to the provider's page. */
|
|
47
71
|
export function rememberHostedOrder(order: CheckoutOrder): void {
|
|
48
72
|
try {
|
|
49
|
-
window.sessionStorage?.setItem(
|
|
73
|
+
window.sessionStorage?.setItem(HOSTED_ORDER_STORAGE_KEY, JSON.stringify(order));
|
|
50
74
|
} catch {
|
|
51
75
|
// Storage disabled or full. The redirect must still happen: the webhook
|
|
52
76
|
// settles the order either way, and refusing to send the buyer to pay
|
|
@@ -62,15 +86,37 @@ export function rememberHostedOrder(order: CheckoutOrder): void {
|
|
|
62
86
|
* of resuming one they never paid. Read-and-clear for the same reason: the
|
|
63
87
|
* resumed view belongs to exactly one return.
|
|
64
88
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The raw parked payload under either key, cleared as it is read.
|
|
91
|
+
*
|
|
92
|
+
* Split out from {@link takeHostedOrder} so the storage handling and the
|
|
93
|
+
* parsing stay separately readable — reading two keys and clearing both put the
|
|
94
|
+
* combined function over the complexity gate, and the two halves fail for
|
|
95
|
+
* unrelated reasons anyway (storage disabled vs. a value that is not an order).
|
|
96
|
+
*
|
|
97
|
+
* BOTH keys are cleared whichever one answered: this is read-and-clear, and a
|
|
98
|
+
* legacy entry left behind would let a later return trip resume an order that
|
|
99
|
+
* was already consumed.
|
|
100
|
+
*/
|
|
101
|
+
function takeParkedPayload(): string | null {
|
|
68
102
|
try {
|
|
69
|
-
raw =
|
|
70
|
-
|
|
103
|
+
const raw =
|
|
104
|
+
window.sessionStorage?.getItem(HOSTED_ORDER_STORAGE_KEY) ??
|
|
105
|
+
window.sessionStorage?.getItem(LEGACY_KEY) ??
|
|
106
|
+
null;
|
|
107
|
+
window.sessionStorage?.removeItem(HOSTED_ORDER_STORAGE_KEY);
|
|
108
|
+
window.sessionStorage?.removeItem(LEGACY_KEY);
|
|
109
|
+
return raw;
|
|
71
110
|
} catch {
|
|
111
|
+
// Storage disabled or unavailable — the same "no parked order" as an empty
|
|
112
|
+
// slot, and the webhook still settles the order regardless.
|
|
72
113
|
return null;
|
|
73
114
|
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function takeHostedOrder(): CheckoutOrder | null {
|
|
118
|
+
if (!isReturnTrip()) return null;
|
|
119
|
+
const raw = takeParkedPayload();
|
|
74
120
|
if (!raw) return null;
|
|
75
121
|
try {
|
|
76
122
|
const parsed: unknown = JSON.parse(raw);
|
|
@@ -92,7 +92,7 @@ export interface CheckoutOrder {
|
|
|
92
92
|
totalCents: number;
|
|
93
93
|
/**
|
|
94
94
|
* GROSS total before discounts, or null when the discount engine never ran
|
|
95
|
-
* for this order (every order predating FUT-235, and the
|
|
95
|
+
* for this order (every order predating FUT-235, and the settlement path): the
|
|
96
96
|
* subtotal then simply IS {@link totalCents}.
|
|
97
97
|
*/
|
|
98
98
|
subtotalCents: number | null;
|
|
@@ -117,13 +117,25 @@ export interface CheckoutOrder {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
|
-
*
|
|
121
|
-
* the
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* SETTLEMENT context: this checkout pays an already-open balance rather than
|
|
121
|
+
* the cart.
|
|
122
|
+
*
|
|
123
|
+
* Named for what it does, not for the one product it came from. It was
|
|
124
|
+
* `SettlementCheckout` with `scope: "MINE" | "TABLE"` — a restaurant tab and its
|
|
125
|
+
* two ways of splitting one — which is a real concept but that host's, and it
|
|
126
|
+
* arrived in every adopter's type surface. The shape underneath is general: a
|
|
127
|
+
* host has resolved SOME balance, by whatever rule it likes, and the flow shows
|
|
128
|
+
* that total in place of the cart's.
|
|
129
|
+
*
|
|
130
|
+
* `scope` is an opaque string the library only echoes back. It exists so a host
|
|
131
|
+
* can tell its own two settlement modes apart in the events it receives; this
|
|
132
|
+
* package neither reads it nor enumerates it, which is exactly why it must not
|
|
133
|
+
* publish a closed set of somebody else's words.
|
|
134
|
+
*
|
|
135
|
+
* HOW a balance is resolved is the host's business; the flow renders the answer.
|
|
124
136
|
*/
|
|
125
|
-
export interface
|
|
126
|
-
scope:
|
|
137
|
+
export interface SettlementCheckout {
|
|
138
|
+
scope: string;
|
|
127
139
|
totalLabel: string;
|
|
128
140
|
totalItems: number;
|
|
129
141
|
}
|
|
@@ -153,7 +165,7 @@ export type CreateOrderResult =
|
|
|
153
165
|
|
|
154
166
|
/**
|
|
155
167
|
* What the flow hands the host's `createOrder` port. Everything else an order
|
|
156
|
-
* needs — WHICH cart, WHICH
|
|
168
|
+
* needs — WHICH cart, WHICH settlement scope, WHICH tenant — is the host's own
|
|
157
169
|
* context, closed over by its port implementation.
|
|
158
170
|
*/
|
|
159
171
|
export interface CreateOrderRequest {
|
|
@@ -35,15 +35,15 @@ const CPF_ONLY: readonly CheckoutCustomerField[] = [
|
|
|
35
35
|
export interface CheckoutHostPorts {
|
|
36
36
|
/**
|
|
37
37
|
* Raise the order (and its first charge). The host closes over everything
|
|
38
|
-
* the flow must not know: WHICH cart, WHICH tenant, WHICH
|
|
39
|
-
|
|
38
|
+
* the flow must not know: WHICH cart, WHICH tenant, WHICH settlement scope.
|
|
39
|
+
*/
|
|
40
40
|
createOrder: (input: CreateOrderRequest) => Promise<CreateOrderResult>;
|
|
41
41
|
/**
|
|
42
42
|
* Persist the buyer's contact when they press "Continuar" on Dados — the
|
|
43
43
|
* host's account surface owns the write (and the blank-CPF-never-clears
|
|
44
44
|
* rule). Fire-and-forget by contract: the flow advances regardless of the
|
|
45
45
|
* outcome, and only calls this under the "salvar meus dados" consent.
|
|
46
|
-
|
|
46
|
+
*/
|
|
47
47
|
saveBuyerContact?: (contact: BuyerContact) => void;
|
|
48
48
|
/** Leave checkout for the host's menu/catalog. */
|
|
49
49
|
onExitToMenu: () => void;
|
|
@@ -52,7 +52,7 @@ export interface CheckoutHostPorts {
|
|
|
52
52
|
* server emptied it inside the confirmation transaction (FUT-601) and
|
|
53
53
|
* nothing else tells the SPA. Never fired for FAILED/EXPIRED: that shopper
|
|
54
54
|
* still has a basket to retry with.
|
|
55
|
-
|
|
55
|
+
*/
|
|
56
56
|
onPaid?: () => void;
|
|
57
57
|
}
|
|
58
58
|
|
package/src/flows/copy.ts
CHANGED
|
@@ -9,8 +9,13 @@
|
|
|
9
9
|
* keep it; moving all of it here in the same change that introduces the factory
|
|
10
10
|
* would be a copy rewrite disguised as an API.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* THE DEFAULTS ARE GONE (FUT-760). `DEFAULT_CHECKOUT_COPY_FE` held one
|
|
13
|
+
* product's RESTAURANT vocabulary — "Pagamento com o garçom", "Chame o garçom
|
|
14
|
+
* para fechar a conta na mesa", "Ver cardápio" — and was spread into every
|
|
15
|
+
* adopter that passed no `copy`. A host selling insurance got a waiter, and
|
|
16
|
+
* nothing failed, because saying nothing is exactly how a host silently adopts
|
|
17
|
+
* another product's voice. `copy` is required on the config now; this file is
|
|
18
|
+
* the port and nothing else.
|
|
14
19
|
*/
|
|
15
20
|
|
|
16
21
|
/** Every buyer-facing string the factory's own screens render. */
|
|
@@ -49,33 +54,3 @@ export interface CheckoutCopyFE {
|
|
|
49
54
|
manageCardsEmpty: string;
|
|
50
55
|
manageCardsAdd: string;
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
export const DEFAULT_CHECKOUT_COPY_FE: CheckoutCopyFE = {
|
|
54
|
-
unavailableTitle: "Pagamento online indisponível",
|
|
55
|
-
unavailableBody:
|
|
56
|
-
"Esta loja não recebe pagamentos pelo site. Combine o pagamento diretamente com a loja para concluir seu pedido.",
|
|
57
|
-
unavailableWithRemedyTitle: "Pagamento com o garçom",
|
|
58
|
-
unavailableWithRemedyBody:
|
|
59
|
-
"Esta loja não recebe pagamentos pelo site. Chame o garçom para fechar a conta na mesa.",
|
|
60
|
-
handoffTitle: "Você será levado ao pagamento",
|
|
61
|
-
handoffBody:
|
|
62
|
-
"Estamos abrindo a página segura do meio de pagamento. Se ela não abrir sozinha, use o link abaixo.",
|
|
63
|
-
handoffLink: "Abrir a página de pagamento",
|
|
64
|
-
handoffCancel: "Voltar",
|
|
65
|
-
returnPending: "Confirmando seu pagamento…",
|
|
66
|
-
returnUnknown:
|
|
67
|
-
"Não encontramos um pagamento em andamento nesta sessão. Verifique seus pedidos em instantes.",
|
|
68
|
-
emptyCartTitle: "Seu carrinho está vazio.",
|
|
69
|
-
emptyCartAction: "Ver cardápio",
|
|
70
|
-
continueAction: "Continuar",
|
|
71
|
-
addCardTitle: "Adicionar cartão",
|
|
72
|
-
addCardAction: "Salvar cartão",
|
|
73
|
-
addCardPreparing: "Preparando o formulário…",
|
|
74
|
-
addCardSavedTitle: "Cartão salvo",
|
|
75
|
-
addCardSavedBody: "Você poderá usá-lo nas próximas compras.",
|
|
76
|
-
addCardFailedTitle: "Não foi possível salvar o cartão",
|
|
77
|
-
addCardUnavailable: "Esta loja não aceita salvar cartões no momento.",
|
|
78
|
-
manageCardsTitle: "Meus cartões",
|
|
79
|
-
manageCardsEmpty: "Você ainda não tem cartões salvos.",
|
|
80
|
-
manageCardsAdd: "Adicionar cartão",
|
|
81
|
-
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* ## Why the scope arrives as HOOKS
|
|
9
9
|
*
|
|
10
|
-
* `useScope`, `useCart`, `useBuyerDefaults`, `
|
|
10
|
+
* `useScope`, `useCart`, `useBuyerDefaults`, `useSettlement` and
|
|
11
11
|
* `ports.useAvailability` are hooks, not values, and they are invoked in a
|
|
12
12
|
* component BODY — never read at factory time. The factory runs once, at module
|
|
13
13
|
* evaluation, so a value-shaped config would freeze the first store's slug and
|
|
@@ -19,10 +19,9 @@ import { useCallback, type JSX, type ReactNode } from "react";
|
|
|
19
19
|
import { buyerFieldsFor } from "../components/checkout/buyer-fields";
|
|
20
20
|
import { CheckoutFlow } from "../components/checkout/checkout-flow";
|
|
21
21
|
import { createCheckoutClient } from "../components/checkout/transport";
|
|
22
|
-
import type { CheckoutProviderConfig,
|
|
22
|
+
import type { CheckoutProviderConfig, SettlementCheckout } from "../components/checkout/types";
|
|
23
23
|
import { useCheckoutController } from "../components/checkout/use-checkout-controller";
|
|
24
24
|
|
|
25
|
-
import { DEFAULT_CHECKOUT_COPY_FE } from "./copy";
|
|
26
25
|
import { FlowsProvider, useResolvedConfig, type FlowsRuntime } from "./runtime";
|
|
27
26
|
import { buyerScreens } from "./screens-buyer";
|
|
28
27
|
import { hostedScreens } from "./screens-hosted";
|
|
@@ -50,7 +49,7 @@ function buildRuntime(config: PaymentFlowsConfig): FlowsRuntime {
|
|
|
50
49
|
return {
|
|
51
50
|
config,
|
|
52
51
|
client,
|
|
53
|
-
copy:
|
|
52
|
+
copy: config.copy,
|
|
54
53
|
navigate,
|
|
55
54
|
// Both of these are HOOKS. They are called from a component body on every
|
|
56
55
|
// render, so the slug follows the host's router and the availability vote
|
|
@@ -68,10 +67,10 @@ function buildCheckout(
|
|
|
68
67
|
const { ports } = runtime.config;
|
|
69
68
|
const Unavailable = screens.PaymentsUnavailable;
|
|
70
69
|
|
|
71
|
-
function CheckoutBody({
|
|
70
|
+
function CheckoutBody({ settlement }: { settlement?: SettlementCheckout | null }): JSX.Element {
|
|
72
71
|
const cart = runtime.config.useCart();
|
|
73
72
|
const defaults = runtime.config.useBuyerDefaults?.() ?? {};
|
|
74
|
-
const
|
|
73
|
+
const hostSettlement = runtime.config.useSettlement?.() ?? null;
|
|
75
74
|
const { config, pending } = useResolvedConfig(runtime);
|
|
76
75
|
const availability = runtime.useAvailability();
|
|
77
76
|
const tenantSlug = runtime.useTenantSlug();
|
|
@@ -93,7 +92,7 @@ function buildCheckout(
|
|
|
93
92
|
onPaid={ports.onPaid}
|
|
94
93
|
defaultBuyer={defaults.buyer}
|
|
95
94
|
taxIdOnFile={defaults.taxIdOnFile ?? false}
|
|
96
|
-
|
|
95
|
+
settlement={settlement ?? hostSettlement}
|
|
97
96
|
providerConfig={config}
|
|
98
97
|
tenantSlug={tenantSlug}
|
|
99
98
|
confirmationExtra={runtime.config.confirmation?.extra}
|
package/src/flows/types.ts
CHANGED
|
@@ -31,7 +31,7 @@ import type {
|
|
|
31
31
|
ChargeOutcome,
|
|
32
32
|
CheckoutOrder,
|
|
33
33
|
CheckoutProviderConfig,
|
|
34
|
-
|
|
34
|
+
SettlementCheckout,
|
|
35
35
|
CreateOrderRequest,
|
|
36
36
|
CreateOrderResult,
|
|
37
37
|
OrderStatus,
|
|
@@ -100,8 +100,8 @@ export interface PaymentFlowsConfig {
|
|
|
100
100
|
useCart(): CheckoutCartView;
|
|
101
101
|
/** The buyer's saved details, and whether a CPF is already on file. */
|
|
102
102
|
useBuyerDefaults?(): { buyer?: BuyerInfo; taxIdOnFile?: boolean; pending?: boolean };
|
|
103
|
-
/** Present ⇒ this checkout settles a
|
|
104
|
-
|
|
103
|
+
/** Present ⇒ this checkout settles a settlement rather than the cart. */
|
|
104
|
+
useSettlement?(): SettlementCheckout | null;
|
|
105
105
|
|
|
106
106
|
/** Design-system slots, filled ONCE instead of per screen. */
|
|
107
107
|
components?: Partial<CheckoutComponents>;
|
|
@@ -117,7 +117,20 @@ export interface PaymentFlowsConfig {
|
|
|
117
117
|
// "what happens WHEN it fires": a timed-out mint must decide whether the walk
|
|
118
118
|
// advances to the next entry or the whole charge refuses, and that is a money
|
|
119
119
|
// rule (FUT-563), not a wire-up.
|
|
120
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Every buyer-facing sentence the factory's own screens render.
|
|
122
|
+
*
|
|
123
|
+
* REQUIRED, and not partial. It used to be `Partial<…>` over a pt-BR default
|
|
124
|
+
* spread in at `create-payment-flows.tsx`, and that default was one product's
|
|
125
|
+
* RESTAURANT vocabulary — "Pagamento com o garçom", "Chame o garçom para
|
|
126
|
+
* fechar a conta na mesa", "Ver cardápio" — reaching every adopter that said
|
|
127
|
+
* nothing. A host selling insurance got a waiter.
|
|
128
|
+
*
|
|
129
|
+
* That is the failure a default cannot warn about: saying nothing is exactly
|
|
130
|
+
* how a host adopts another product's voice, and nothing fails. Required
|
|
131
|
+
* makes a new host answer once, at the one call site that knows the answer.
|
|
132
|
+
*/
|
|
133
|
+
copy: CheckoutCopyFE;
|
|
121
134
|
/** Host content under the paid receipt (the storefront's PWA install invite). */
|
|
122
135
|
confirmation?: { extra?: ReactNode };
|
|
123
136
|
/**
|
|
@@ -187,7 +200,7 @@ export interface CheckoutConfigState {
|
|
|
187
200
|
/** What `createPaymentFlows` returns. */
|
|
188
201
|
export interface PaymentFlows {
|
|
189
202
|
/** THE mount: a complete buyer checkout in one line. */
|
|
190
|
-
Checkout: ComponentType<{
|
|
203
|
+
Checkout: ComponentType<{ settlement?: SettlementCheckout | null }>;
|
|
191
204
|
/** Slots + transport + scope + the fetched config, for a nesting host. */
|
|
192
205
|
Provider: ComponentType<{ children: ReactNode; config?: CheckoutProviderConfig | null }>;
|
|
193
206
|
screens: CheckoutScreens;
|
package/src/index.ts
CHANGED
|
@@ -57,7 +57,6 @@ export {
|
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
58
|
export { createPaymentFlows } from './flows/create-payment-flows';
|
|
59
59
|
export {
|
|
60
|
-
DEFAULT_CHECKOUT_COPY_FE,
|
|
61
60
|
type CheckoutCopyFE,
|
|
62
61
|
} from './flows/copy';
|
|
63
62
|
export {
|
|
@@ -87,6 +86,13 @@ export {
|
|
|
87
86
|
export { type CheckoutHostPorts } from './components/checkout/use-checkout-controller';
|
|
88
87
|
export { PaymentsUnavailable } from './components/checkout/payments-unavailable';
|
|
89
88
|
export { fetchCheckoutConfig } from './components/checkout/client';
|
|
89
|
+
/**
|
|
90
|
+
* The `sessionStorage` key the hosted-checkout return leg parks the raised
|
|
91
|
+
* order under. Public because it is already observable — a spec asserting the
|
|
92
|
+
* handover, or a host clearing storage on sign-out, otherwise retypes the
|
|
93
|
+
* literal and silently drifts when it changes.
|
|
94
|
+
*/
|
|
95
|
+
export { HOSTED_ORDER_STORAGE_KEY } from './components/checkout/hosted-return';
|
|
90
96
|
// ---------------------------------------------------------------------------
|
|
91
97
|
// Digital wallets (FUT-471/472) — the Google-branded button and the capability
|
|
92
98
|
// read it is gated on. `CheckoutFlow` wires these automatically; they are
|
|
@@ -136,7 +142,7 @@ export {
|
|
|
136
142
|
type CheckoutOrder,
|
|
137
143
|
type CheckoutProviderConfig,
|
|
138
144
|
type CheckoutWalletType,
|
|
139
|
-
type
|
|
145
|
+
type SettlementCheckout,
|
|
140
146
|
type CreateOrderRequest,
|
|
141
147
|
type CreateOrderResult,
|
|
142
148
|
type OrderStatus,
|