@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/payments-frontend",
3
- "version": "1.21.0",
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": "^2.1.0",
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. O Future Pay cria as cobranças em seu nome — nenhuma chave precisa ser copiada.'
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, ComandaCheckout } from "./types";
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 comanda settlement ignores it). */
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, comanda and order CREATION stay in the host and
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 a comanda, not the cart (FUT-comandas). */
42
- comanda?: ComandaCheckout | null;
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 comanda (else the cart's own totals). */
63
- function comandaTotalOverride(
64
- comanda: ComandaCheckout | null | undefined,
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 comanda ? { label: comanda.totalLabel, items: comanda.totalItems } : undefined;
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 comanda scope's, else the cart's. */
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
- comanda: ComandaCheckout | null | undefined,
84
+ settlement: SettlementCheckout | null | undefined,
85
85
  cart: { totalLabel: string },
86
86
  ): string {
87
- return order?.totalLabel ?? comanda?.totalLabel ?? cart.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, comanda, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, ...ports } = props;
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 comanda settlement pays already-sent kitchen items — the cart is
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 (!comanda && cart.empty && !c.order && c.step !== "status") {
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={comandaTotalOverride(comanda)}
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, comanda, cart)}
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 comanda scope's when settling a comanda
117
- * (FUT-comandas), otherwise the cart's own — both supplied by the host, which
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
- /** Comanda settlement (FUT-comandas): totals come from the comanda, not the cart. */
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 comanda: those totals come from the
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
- const KEY = "futurepay.checkout.hostedOrder";
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(KEY, JSON.stringify(order));
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
- export function takeHostedOrder(): CheckoutOrder | null {
66
- if (!isReturnTrip()) return null;
67
- let raw: string | null = null;
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 = window.sessionStorage?.getItem(KEY) ?? null;
70
- window.sessionStorage?.removeItem(KEY);
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 comanda path): 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
- * Comanda settlement context for the checkout (FUT-comandas): the scope plus
121
- * the host-resolved totals shown in place of the cart total when settling a
122
- * comanda. HOW a comanda is resolved is the host's business; the flow only
123
- * renders the answer.
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 ComandaCheckout {
126
- scope: "MINE" | "TABLE";
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 comanda scope, WHICH tenant — is the host's own
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 comanda scope.
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
- * Every default below is today's pt-BR, verbatim a host that passes no `copy`
13
- * reads exactly what a buyer reads now.
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`, `useComanda` and
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, ComandaCheckout } from "../components/checkout/types";
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: { ...DEFAULT_CHECKOUT_COPY_FE, ...config.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({ comanda }: { comanda?: ComandaCheckout | null }): JSX.Element {
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 hostComanda = runtime.config.useComanda?.() ?? null;
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
- comanda={comanda ?? hostComanda}
95
+ settlement={settlement ?? hostSettlement}
97
96
  providerConfig={config}
98
97
  tenantSlug={tenantSlug}
99
98
  confirmationExtra={runtime.config.confirmation?.extra}
@@ -31,7 +31,7 @@ import type {
31
31
  ChargeOutcome,
32
32
  CheckoutOrder,
33
33
  CheckoutProviderConfig,
34
- ComandaCheckout,
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 comanda rather than the cart. */
104
- useComanda?(): ComandaCheckout | null;
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
- copy?: Partial<CheckoutCopyFE>;
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<{ comanda?: ComandaCheckout | null }>;
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 ComandaCheckout,
145
+ type SettlementCheckout,
140
146
  type CreateOrderRequest,
141
147
  type CreateOrderResult,
142
148
  type OrderStatus,