@12-apps/payments-frontend 3.5.0 → 3.7.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": "3.5.0",
3
+ "version": "3.7.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": "^4.13.0",
20
+ "@12-apps/payments-backend": "^4.15.0",
21
21
  "react-qr-code": "^2.2.0"
22
22
  },
23
23
  "peerDependencies": {
@@ -28,8 +28,8 @@
28
28
  "react-dom": ">=19.0.0"
29
29
  },
30
30
  "devDependencies": {
31
- "@12-apps/eslint-config": "^1.21.0",
32
- "@12-apps/typescript-config": "^1.20.0",
31
+ "@12-apps/eslint-config": "^1.21.1",
32
+ "@12-apps/typescript-config": "^1.20.1",
33
33
  "@emotion/react": "^11.14.0",
34
34
  "@emotion/styled": "^11.14.0",
35
35
  "@mui/material": "^6.5.0",
@@ -7,13 +7,17 @@ import { ArrowBackIcon } from "./icons";
7
7
  import { PaymentStatus } from "./payment-status";
8
8
  import type { BuyerInfo, CheckoutProviderConfig, SettlementCheckout } from "./types";
9
9
  import { CheckoutComponentsProvider, useCheckoutComponents, type CheckoutComponents } from "./ui";
10
+ import type { CheckoutViewCopy } from "./view-copy";
10
11
  import { useCheckoutController, type CheckoutHostPorts } from "./use-checkout-controller";
11
12
 
12
- const STEPPER_STEPS = [
13
- { id: "dados", label: "Dados" },
14
- { id: "payment", label: "Pagamento" },
15
- { id: "status", label: "Confirmação" },
16
- ];
13
+ /** Step ids are the flow's own contract; the labels beside them are host copy. */
14
+ function stepperSteps(copy: CheckoutViewCopy): { id: string; label: string }[] {
15
+ return [
16
+ { id: "dados", label: copy.steps.dados },
17
+ { id: "payment", label: copy.steps.payment },
18
+ { id: "status", label: copy.steps.status },
19
+ ];
20
+ }
17
21
 
18
22
  /** What the flow reads off the host's cart — display facts, never money math. */
19
23
  export interface CheckoutCartView {
@@ -35,6 +39,14 @@ export interface CheckoutCartView {
35
39
  * pixels render through the slot contract (`components`, see `ui.tsx`).
36
40
  */
37
41
  export interface CheckoutFlowProps extends CheckoutHostPorts {
42
+ /**
43
+ * Every sentence the flow's own chrome renders — stepper labels, the Dados
44
+ * step, the empty cart, the confirmation screen. REQUIRED, with no default:
45
+ * a pt-BR host passes `PT_BR_CHECKOUT_VIEW_COPY` from the package root by
46
+ * hand, so choosing Portuguese is a line in the host's diff, never a
47
+ * silence (FUT-760's doctrine, finally applied to the legacy flow too).
48
+ */
49
+ copy: CheckoutViewCopy;
38
50
  /** The host's cart, reduced to what the flow displays. */
39
51
  cart: CheckoutCartView;
40
52
  defaultBuyer?: BuyerInfo;
@@ -97,23 +109,23 @@ function statusTotalLabel(
97
109
  * per-line remove. Offering it here put a destructive control next to the form
98
110
  * a buyer is filling in, one tap away from the field they are typing into.
99
111
  */
100
- function CheckoutHeader({ step, onBack }: { step: string; onBack: () => void }): JSX.Element {
112
+ function CheckoutHeader({ copy, step, onBack }: { copy: CheckoutViewCopy; step: string; onBack: () => void }): JSX.Element {
101
113
  const { Button } = useCheckoutComponents();
102
114
  return (
103
115
  <Box sx={{ minHeight: 36, display: "flex", alignItems: "center", gap: 1 }}>
104
116
  <Button variant="text" color="neutral" size="sm" icon={<ArrowBackIcon fontSize="small" />} iconPosition="left" onClick={onBack} dataTestId="checkout-back">
105
- {step === "dados" ? "Continuar comprando" : "Voltar"}
117
+ {step === "dados" ? copy.dados.keepShopping : copy.dados.back}
106
118
  </Button>
107
119
  </Box>
108
120
  );
109
121
  }
110
122
 
111
123
  /** Compact 50px progress header so the form stays above the fold on mobile. */
112
- function ProgressHeader({ step, completed }: { step: string; completed: Set<string> }): JSX.Element {
124
+ function ProgressHeader({ copy, step, completed }: { copy: CheckoutViewCopy; step: string; completed: Set<string> }): JSX.Element {
113
125
  const { Stepper } = useCheckoutComponents();
114
126
  return (
115
127
  <Box sx={{ height: 50, display: "flex", flexDirection: "column", justifyContent: "center" }}>
116
- <Stepper steps={STEPPER_STEPS} activeId={step} completed={completed} orientation="horizontal" size="sm" data-testid="checkout-stepper" />
128
+ <Stepper steps={stepperSteps(copy)} activeId={step} completed={completed} orientation="horizontal" size="sm" data-testid="checkout-stepper" />
117
129
  </Box>
118
130
  );
119
131
  }
@@ -124,8 +136,37 @@ function ProgressHeader({ step, completed }: { step: string; completed: Set<stri
124
136
  * chrome (reduced to {@link CheckoutCartView} here), and the provider SDK +
125
137
  * card public key are loaded lazily by the card path (order-scoped REST).
126
138
  */
139
+ /** Step 3, with the controller's facts mapped onto the status screen. */
140
+ function StatusStep({
141
+ copy,
142
+ c,
143
+ settlement,
144
+ cart,
145
+ confirmationExtra,
146
+ }: {
147
+ copy: CheckoutViewCopy;
148
+ c: ReturnType<typeof useCheckoutController>;
149
+ settlement: SettlementCheckout | null | undefined;
150
+ cart: CheckoutCartView;
151
+ confirmationExtra: ReactNode;
152
+ }): JSX.Element {
153
+ return (
154
+ <PaymentStatus
155
+ copy={copy.status}
156
+ status={c.finalStatus}
157
+ totalLabel={statusTotalLabel(c.order, settlement, cart)}
158
+ {...confirmationFacts(c.order, c.buyer)}
159
+ onRetry={c.retry}
160
+ onRegenerate={() => { c.setStep("payment"); void c.startPayment("PIX"); }}
161
+ onBackToMenu={c.goToMenu}
162
+ paidExtra={confirmationExtra}
163
+ awaitingTimedOut={c.resumeTimedOut}
164
+ />
165
+ );
166
+ }
167
+
127
168
  function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Element {
128
- const { cart, defaultBuyer, settlement, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, ...ports } = props;
169
+ const { copy, cart, defaultBuyer, settlement, taxIdOnFile = false, providerConfig, tenantSlug, confirmationExtra, validateApplePayMerchant, ...ports } = props;
129
170
  // Resolved for NO method on purpose (FUT-595): the Dados step opens before
130
171
  // the picker, and the form is filled once — so it asks for the union of what
131
172
  // any chain member may need rather than re-opening after the choice. A chain
@@ -141,17 +182,18 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
141
182
  // method picker. It holds until an order exists — once one does, its lines are
142
183
  // snapshotted server-side and the cart no longer speaks for it.
143
184
  if (!settlement && cart.empty && !c.order && c.step !== "status") {
144
- return <EmptyCart onBack={c.goToMenu} />;
185
+ return <EmptyCart copy={copy.emptyCart} onBack={c.goToMenu} />;
145
186
  }
146
187
 
147
188
  return (
148
189
  <Box sx={{ display: "flex", flexDirection: "column", gap: { xs: 2, sm: 3 } }}>
149
- <CheckoutHeader step={c.step} onBack={c.back} />
190
+ <CheckoutHeader copy={copy} step={c.step} onBack={c.back} />
150
191
 
151
- <ProgressHeader step={c.step} completed={c.completed} />
192
+ <ProgressHeader copy={copy} step={c.step} completed={c.completed} />
152
193
 
153
194
  {c.step === "dados" ? (
154
195
  <DadosStep
196
+ copy={copy.dados}
155
197
  buyer={c.buyer}
156
198
  onBuyerChange={c.setBuyer}
157
199
  saveProfile={c.saveProfile}
@@ -189,16 +231,7 @@ function CheckoutFlowBody(props: Omit<CheckoutFlowProps, "components">): JSX.Ele
189
231
  ) : null}
190
232
 
191
233
  {c.step === "status" ? (
192
- <PaymentStatus
193
- status={c.finalStatus}
194
- totalLabel={statusTotalLabel(c.order, settlement, cart)}
195
- {...confirmationFacts(c.order, c.buyer)}
196
- onRetry={c.retry}
197
- onRegenerate={() => { c.setStep("payment"); void c.startPayment("PIX"); }}
198
- onBackToMenu={c.goToMenu}
199
- paidExtra={confirmationExtra}
200
- awaitingTimedOut={c.resumeTimedOut}
201
- />
234
+ <StatusStep copy={copy} c={c} settlement={settlement} cart={cart} confirmationExtra={confirmationExtra} />
202
235
  ) : null}
203
236
  </Box>
204
237
  );
@@ -3,12 +3,7 @@ import { useEffect, useRef, type JSX, type ReactNode } from "react";
3
3
 
4
4
  import { BuyerInfoForm } from "./buyer-info-form";
5
5
  import { LockOutlinedIcon } from "./icons";
6
- import {
7
- cardPathAvailable,
8
- offeredMethods,
9
- selectableMethods,
10
- usePreselectSoleMethod,
11
- } from "./method-capability";
6
+ import { useMethodChoice } from "./method-choice";
12
7
  import { MethodPicker } from "./method-picker";
13
8
  import { PaymentErrorPanel } from "./payment-error-panel";
14
9
  import { PayerSummary } from "./payer-summary";
@@ -22,6 +17,7 @@ import type {
22
17
  OrderStatus,
23
18
  PaymentMethod,
24
19
  } from "./types";
20
+ import type { DadosStepCopy, EmptyCartCopy } from "./view-copy";
25
21
  import { useCheckoutComponents } from "./ui";
26
22
 
27
23
  /**
@@ -42,8 +38,8 @@ function useAutoRaiseOrder(
42
38
  requestedFor.current = null;
43
39
  return;
44
40
  }
45
- // No method chosen yet ⇒ show only the picker; raise the order once the
46
- // buyer selects PIX or card.
41
+ // No method chosen yet ⇒ show only the picker (or, for a hand-off store,
42
+ // its "Seguir para o pagamento"); raise the order once the buyer commits.
47
43
  if (!method || creating || createError || requestedFor.current === method) return;
48
44
  requestedFor.current = method;
49
45
  onGenerate(method);
@@ -70,6 +66,8 @@ function PaymentBody({
70
66
  method,
71
67
  tenantSlug,
72
68
  onResolved,
69
+ onStart,
70
+ creating,
73
71
  pollIntervalMs,
74
72
  validateApplePayMerchant,
75
73
  }: {
@@ -79,6 +77,9 @@ function PaymentBody({
79
77
  method: PaymentMethod | null;
80
78
  tenantSlug?: string;
81
79
  onResolved: (status: OrderStatus) => void;
80
+ /** Set only when the shell hid its picker — see {@link PaymentStep}. */
81
+ onStart?: () => void;
82
+ creating: boolean;
82
83
  pollIntervalMs?: number;
83
84
  validateApplePayMerchant?: (validationURL: string) => Promise<unknown>;
84
85
  }): JSX.Element | null {
@@ -91,6 +92,8 @@ function PaymentBody({
91
92
  method={method}
92
93
  tenantSlug={tenantSlug}
93
94
  onResolved={onResolved}
95
+ onStart={onStart}
96
+ creating={creating}
94
97
  pollIntervalMs={pollIntervalMs}
95
98
  validateApplePayMerchant={validateApplePayMerchant}
96
99
  />
@@ -98,15 +101,15 @@ function PaymentBody({
98
101
  }
99
102
 
100
103
  /** Empty-cart state shown when there's nothing to check out. */
101
- export function EmptyCart({ onBack }: { onBack: () => void }): JSX.Element {
104
+ export function EmptyCart({ copy, onBack }: { copy: EmptyCartCopy; onBack: () => void }): JSX.Element {
102
105
  const { Button, Text } = useCheckoutComponents();
103
106
  return (
104
107
  <Box data-testid="checkout-empty" sx={{ py: 8, textAlign: "center", display: "flex", flexDirection: "column", gap: 2, alignItems: "center" }}>
105
108
  <Text variant="heading" size="md" as="p">
106
- Seu carrinho está vazio.
109
+ {copy.title}
107
110
  </Text>
108
111
  <Button variant="solid" color="primary" size="md" onClick={onBack} dataTestId="checkout-empty-back">
109
- Ver cardápio
112
+ {copy.action}
110
113
  </Button>
111
114
  </Box>
112
115
  );
@@ -156,6 +159,7 @@ function PayBarTotal({
156
159
  * — no charge yet.
157
160
  */
158
161
  export function DadosStep({
162
+ copy,
159
163
  buyer,
160
164
  onBuyerChange,
161
165
  saveProfile,
@@ -168,6 +172,8 @@ export function DadosStep({
168
172
  discountLines,
169
173
  totalOverride,
170
174
  }: {
175
+ /** The step's own sentences — the HOST's words (see `./view-copy`). */
176
+ copy: DadosStepCopy;
171
177
  buyer: BuyerInfo;
172
178
  onBuyerChange: (buyer: BuyerInfo) => void;
173
179
  saveProfile: boolean;
@@ -204,12 +210,13 @@ export function DadosStep({
204
210
  <Checkbox
205
211
  checked={saveProfile}
206
212
  onChange={(_event, checked) => onSaveProfileChange(checked)}
207
- label="Salvar meus dados para a próxima compra"
213
+ label={copy.saveProfile}
208
214
  data-testid="buyer-save-profile"
209
215
  />
210
216
  </Box>
211
217
 
212
218
  <DadosPayBar
219
+ copy={copy}
213
220
  totalLabel={totalLabel}
214
221
  totalItems={totalItems}
215
222
  createError={createError}
@@ -225,12 +232,14 @@ export function DadosStep({
225
232
 
226
233
  /** The sticky "Continuar" bar: the refusal, the money, and the one action. */
227
234
  function DadosPayBar({
235
+ copy,
228
236
  totalLabel,
229
237
  totalItems,
230
238
  createError,
231
239
  onContinue,
232
240
  children,
233
241
  }: {
242
+ copy: DadosStepCopy;
234
243
  totalLabel: string;
235
244
  totalItems: number;
236
245
  createError: string | null;
@@ -242,20 +251,22 @@ function DadosPayBar({
242
251
  <ActionBar dataTestId="checkout-pay-bar">
243
252
  <Box sx={{ width: "100%", display: "flex", flexDirection: "column", gap: 1.5 }}>
244
253
  {createError ? (
245
- <Alert variant="danger" title="Não foi possível continuar" description={createError} showIcon data-testid="checkout-error" />
254
+ <Alert variant="danger" title={copy.cannotContinueTitle} description={createError} showIcon data-testid="checkout-error" />
246
255
  ) : null}
247
256
  <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
248
257
  <PayBarTotal totalLabel={totalLabel} totalItems={totalItems}>{children}</PayBarTotal>
249
258
  <Box sx={{ flex: 1, display: "flex", flexDirection: "column", alignItems: "flex-end", gap: 0.5, minWidth: 0 }}>
250
259
  <Button variant="solid" color="primary" size="lg" fullWidth onClick={onContinue} dataTestId="checkout-continue">
251
- Continuar
260
+ {copy.continueAction}
252
261
  </Button>
253
- <Box sx={{ display: "flex", alignItems: "center", gap: 0.5, color: "text.secondary" }}>
254
- <LockOutlinedIcon sx={{ fontSize: 13 }} />
255
- <Text variant="caption" size="xs" color="secondary" as="span">
256
- Pagamento seguro
257
- </Text>
258
- </Box>
262
+ {copy.secureNotice ? (
263
+ <Box sx={{ display: "flex", alignItems: "center", gap: 0.5, color: "text.secondary" }}>
264
+ <LockOutlinedIcon sx={{ fontSize: 13 }} />
265
+ <Text variant="caption" size="xs" color="secondary" as="span">
266
+ {copy.secureNotice}
267
+ </Text>
268
+ </Box>
269
+ ) : null}
259
270
  </Box>
260
271
  </Box>
261
272
  </Box>
@@ -303,6 +314,17 @@ interface PaymentStepProps {
303
314
  * Step 2 "Pagamento" — pick PIX or card and pay on the SAME page. Selecting a
304
315
  * method auto-raises its order and reveals its UI (PIX QR / card form) with no
305
316
  * intermediate tap; switching method clears the previous order (controller).
317
+ *
318
+ * ## Unless the choice is not ours to ask
319
+ *
320
+ * A store that finishes checkout on the provider's own page gets NO picker
321
+ * here (`methodChosenAtProvider`). Its screen renders a single "Seguir para o
322
+ * pagamento" instead, and pressing it selects the store's hand-off method —
323
+ * which is the same event a tile press is, so the auto-raise, the error panel
324
+ * and the retry below all keep working unchanged. Preselection is suppressed
325
+ * for the same flow, and deliberately: it exists to spare a buyer a tap that
326
+ * buys them nothing, but here the tap is the buyer's consent to LEAVE, and
327
+ * taking it for them would redirect a checkout the moment it rendered.
306
328
  */
307
329
  export function PaymentStep({
308
330
  method,
@@ -323,35 +345,42 @@ export function PaymentStep({
323
345
  onResolved,
324
346
  }: PaymentStepProps): JSX.Element {
325
347
  const { LoadingState } = useCheckoutComponents();
326
- const cardUnavailable = !cardPathAvailable(providerConfig ?? null);
327
- const offered = offeredMethods(providerConfig ?? null);
348
+ const config = providerConfig ?? null;
349
+ const choice = useMethodChoice(config, method, onMethodChange);
328
350
  useAutoRaiseOrder(order, method, creating, createError, onGenerate);
329
- usePreselectSoleMethod(selectableMethods(offered, cardUnavailable), method, onMethodChange);
330
351
 
331
352
  return (
332
353
  <Box sx={{ display: "flex", flexDirection: "column", gap: 3 }}>
333
354
  {/* Self-hiding: renders only for a flow whose Dados step was skipped. */}
334
355
  <PayerSummary name={buyer.name} taxId={buyer.taxId} onEdit={onEditBuyer} />
335
356
 
336
- <MethodPicker
337
- value={method}
338
- onChange={onMethodChange}
339
- cardUnavailable={cardUnavailable}
340
- offered={offered}
341
- />
357
+ {choice.atProvider ? null : (
358
+ <MethodPicker
359
+ value={method}
360
+ onChange={onMethodChange}
361
+ cardUnavailable={choice.cardUnavailable}
362
+ offered={choice.offered}
363
+ />
364
+ )}
342
365
 
343
366
  <PaymentBody
344
367
  order={order}
345
368
  buyer={buyer}
346
- providerConfig={providerConfig ?? null}
369
+ providerConfig={config}
347
370
  method={method}
348
371
  tenantSlug={tenantSlug}
349
372
  onResolved={onResolved}
373
+ onStart={choice.onStart}
374
+ creating={creating}
350
375
  pollIntervalMs={pollIntervalMs}
351
376
  validateApplePayMerchant={validateApplePayMerchant}
352
377
  />
353
378
 
354
- {!order && creating ? (
379
+ {/* The shell's own busy spinner, SUPPRESSED for a hand-off screen: that
380
+ screen renders its own "Preparando o pagamento" while the charge is
381
+ raised, and two stacked spinners saying the same thing is what the
382
+ buyer actually saw. */}
383
+ {!order && creating && !choice.atProvider ? (
355
384
  <LoadingState variant="spinner" size="md" message="Gerando pagamento…" dataTestId="payment-generating" />
356
385
  ) : null}
357
386
 
@@ -216,6 +216,33 @@ export function selectableMethods(
216
216
  );
217
217
  }
218
218
 
219
+ /**
220
+ * The method a HAND-OFF checkout raises on the buyer's behalf, when the buyer
221
+ * was never asked (see `providers/registry.ts`'s `methodChosenAtProvider`).
222
+ *
223
+ * A charge still has to be raised with SOME method — that is the wire's shape,
224
+ * and the link is minted by raising it — but for a hand-off store the value is
225
+ * provisional by construction: the buyer picks for real on the provider's page,
226
+ * and the settlement reports back which one they actually used. So the choice
227
+ * here is about which request the SERVER will honour, not about what the buyer
228
+ * gets.
229
+ *
230
+ * PIX when the chain offers it, because PIX is the one method whose first
231
+ * charge is always raised immediately: a CARD request at a store that can
232
+ * tokenize somewhere in its chain is answered with the bare order instead, and
233
+ * there is no hand-off link on that answer to send anyone anywhere. Otherwise
234
+ * the chain's first declared method, since a store that cannot PIX would have
235
+ * the walk refuse a PIX charge outright.
236
+ *
237
+ * `null` offered — still loading, or a fetch blip — cannot reach here: the
238
+ * picker is only hidden for a store whose config already said it hands over.
239
+ * PIX is the safe reading of it anyway, for the reason above.
240
+ */
241
+ export function handOffMethod(offered: PaymentMethod[] | null): PaymentMethod {
242
+ if (!offered || offered.includes("PIX")) return "PIX";
243
+ return offered[0] ?? "PIX";
244
+ }
245
+
219
246
  /**
220
247
  * A SOLE remaining method is not a choice — take it (FUT-697 review, widened by
221
248
  * FUT-741).
@@ -0,0 +1,64 @@
1
+ /**
2
+ * WHO asks the buyer PIX-or-card, and what the Pagamento step does about it.
3
+ *
4
+ * Its own module because the answer is a set of derived facts that must move
5
+ * together: a picker rendered for a store that hands the buyer over, or a
6
+ * hand-off CTA rendered beside a picker, are both a checkout asking one
7
+ * question twice — which is the defect this seam exists to make impossible.
8
+ */
9
+ import {
10
+ cardPathAvailable,
11
+ handOffMethod,
12
+ offeredMethods,
13
+ selectableMethods,
14
+ usePreselectSoleMethod,
15
+ } from "./method-capability";
16
+ import { methodChosenAtProvider } from "./providers/registry";
17
+ import type { CheckoutProviderConfig, PaymentMethod } from "./types";
18
+
19
+ /** What the Pagamento step needs to know about WHO asks the buyer for a method. */
20
+ interface MethodChoice {
21
+ /** The store's active provider has no card path in this browser. */
22
+ cardUnavailable: boolean;
23
+ /** The methods the chain declares it can charge, or `null` while unknown. */
24
+ offered: PaymentMethod[] | null;
25
+ /** The choice is made on the provider's page ⇒ render no picker here. */
26
+ atProvider: boolean;
27
+ /**
28
+ * The hand-off screen's "start paying" port, or `undefined` when the picker
29
+ * is on the page and owns that job instead. It SELECTS the store's hand-off
30
+ * method, which is the same event a tile press is — so the auto-raise, the
31
+ * error panel and its retry all keep working with no second code path.
32
+ */
33
+ onStart?: () => void;
34
+ }
35
+
36
+ /**
37
+ * Resolve who asks the buyer PIX-or-card, and preselect a sole method when the
38
+ * question is ours to ask.
39
+ */
40
+ export function useMethodChoice(
41
+ config: CheckoutProviderConfig | null,
42
+ method: PaymentMethod | null,
43
+ onMethodChange: (method: PaymentMethod) => void,
44
+ ): MethodChoice {
45
+ const cardUnavailable = !cardPathAvailable(config);
46
+ const offered = offeredMethods(config);
47
+ const atProvider = methodChosenAtProvider(config?.chain?.[0]?.checkoutScreen, config);
48
+ // Nothing to preselect when the screen owns the choice: the whole point of
49
+ // its button is that the buyer presses it. Preselection exists to spare them
50
+ // a tap that buys them nothing, and here the tap is their consent to LEAVE —
51
+ // taking it for them would redirect a checkout the moment it rendered.
52
+ usePreselectSoleMethod(
53
+ atProvider ? [] : selectableMethods(offered, cardUnavailable),
54
+ method,
55
+ onMethodChange,
56
+ );
57
+ if (!atProvider) return { cardUnavailable, offered, atProvider };
58
+ return {
59
+ cardUnavailable,
60
+ offered,
61
+ atProvider,
62
+ onStart: () => onMethodChange(handOffMethod(offered)),
63
+ };
64
+ }