@cartbase/storefront 0.23.0 → 0.24.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 +1 -1
- package/src/api/integrations.ts +5 -3
- package/src/checkout/checkout-client.tsx +37 -19
- package/src/checkout/order-summary.tsx +30 -21
- package/src/checkout/payment-method-list.tsx +66 -29
- package/src/checkout/pickup-point-selector.tsx +5 -4
- package/src/checkout/use-checkout-orchestration.ts +40 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cartbase/storefront",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Storefront SDK + UI component library for Cartbase stores: typed API client, checkout orchestration, cart drawer, product/catalog components, tracking. Source-shipped TypeScript — add it to transpilePackages.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/api/integrations.ts
CHANGED
|
@@ -177,9 +177,11 @@ export interface StorePickupPoint {
|
|
|
177
177
|
provider: string
|
|
178
178
|
mode: "office" | "locker"
|
|
179
179
|
/** THEIR id, exactly as the order will carry it. */
|
|
180
|
-
id: string
|
|
181
|
-
name: string
|
|
182
|
-
|
|
180
|
+
id: string
|
|
181
|
+
name: string
|
|
182
|
+
/** Alternate script/transliteration supplied by the carrier. */
|
|
183
|
+
secondary_name?: string
|
|
184
|
+
city: string
|
|
183
185
|
address: string
|
|
184
186
|
postal_code: string
|
|
185
187
|
phone: string
|
|
@@ -5,7 +5,8 @@ import type { Appearance, StripeElementsOptions } from "@stripe/stripe-js"
|
|
|
5
5
|
import type { StorefrontClient } from "../api/http"
|
|
6
6
|
import type { Cart } from "../api/carts"
|
|
7
7
|
import type { StoreShippingOption } from "../api/checkout"
|
|
8
|
-
import type { StoreCustomer } from "../api/customers"
|
|
8
|
+
import type { StoreCustomer } from "../api/customers"
|
|
9
|
+
import type { PaymentMethod } from "../common/payment-icons"
|
|
9
10
|
import { CheckoutAddressForm } from "./address-form"
|
|
10
11
|
import { DiscountSection } from "./discount-section"
|
|
11
12
|
import { MobileCheckoutBottomBar } from "./mobile-checkout-bottom-bar"
|
|
@@ -59,7 +60,11 @@ type CheckoutClientProps = {
|
|
|
59
60
|
cart: Cart
|
|
60
61
|
customer: StoreCustomer | null
|
|
61
62
|
availableShippingMethods: StoreShippingOption[] | null
|
|
62
|
-
availablePaymentMethods: PaymentProviderLike[] | null
|
|
63
|
+
availablePaymentMethods: PaymentProviderLike[] | null
|
|
64
|
+
/** Card and wallet marks returned by `GET /api/store/payment-marks`. */
|
|
65
|
+
paymentMarks?: readonly PaymentMethod[]
|
|
66
|
+
/** Store-owned terms notice, including links to its content pages. */
|
|
67
|
+
termsContent?: React.ReactNode
|
|
63
68
|
/**
|
|
64
69
|
* The country to PRESELECT when the cart has no shipping address yet.
|
|
65
70
|
* Optional, like the hook's own: the offer comes from the store
|
|
@@ -76,8 +81,10 @@ type CheckoutClientProps = {
|
|
|
76
81
|
methods: PaymentProviderLike[] | null,
|
|
77
82
|
selectedShippingOption: StoreShippingOption | null
|
|
78
83
|
) => PaymentProviderLike[] | null
|
|
79
|
-
/** Show the gift-card widget in the summary (default true). */
|
|
80
|
-
showGiftCards?: boolean
|
|
84
|
+
/** Show the gift-card widget in the summary (default true). */
|
|
85
|
+
showGiftCards?: boolean
|
|
86
|
+
/** Show the security reassurance below the desktop total (default true). */
|
|
87
|
+
showSecureCheckout?: boolean
|
|
81
88
|
logoByFulfillmentOptionId?: Record<string, { src: string; alt: string }>
|
|
82
89
|
/** Stripe Elements appearance + fonts forwarded to PaymentWrapper. */
|
|
83
90
|
appearance?: Appearance
|
|
@@ -107,11 +114,14 @@ export function CheckoutClient({
|
|
|
107
114
|
cart,
|
|
108
115
|
customer,
|
|
109
116
|
availableShippingMethods,
|
|
110
|
-
availablePaymentMethods,
|
|
117
|
+
availablePaymentMethods,
|
|
118
|
+
paymentMarks,
|
|
119
|
+
termsContent,
|
|
111
120
|
countryCode,
|
|
112
121
|
countries,
|
|
113
122
|
paymentMethodFilter,
|
|
114
|
-
showGiftCards = true,
|
|
123
|
+
showGiftCards = true,
|
|
124
|
+
showSecureCheckout = true,
|
|
115
125
|
logoByFulfillmentOptionId,
|
|
116
126
|
appearance,
|
|
117
127
|
fonts,
|
|
@@ -165,9 +175,10 @@ export function CheckoutClient({
|
|
|
165
175
|
<MobileCheckoutTopBar
|
|
166
176
|
client={client}
|
|
167
177
|
cart={o.summaryCart}
|
|
168
|
-
optimisticShippingCost={o.optimisticShippingCost}
|
|
169
|
-
optimisticMethodFee={o.optimisticMethodFee}
|
|
170
|
-
|
|
178
|
+
optimisticShippingCost={o.optimisticShippingCost}
|
|
179
|
+
optimisticMethodFee={o.optimisticMethodFee}
|
|
180
|
+
methodFeeLabel={o.paymentTab === "cod" ? o.offlineMethod?.fee_label ?? undefined : undefined}
|
|
181
|
+
showGiftCards={showGiftCards}
|
|
171
182
|
onCartChange={handleCartChange}
|
|
172
183
|
/>
|
|
173
184
|
|
|
@@ -218,9 +229,13 @@ export function CheckoutClient({
|
|
|
218
229
|
|
|
219
230
|
<CheckoutPaymentMethodList
|
|
220
231
|
cart={cart}
|
|
221
|
-
hasCard={o.hasCard}
|
|
222
|
-
hasCod={o.hasCod}
|
|
223
|
-
|
|
232
|
+
hasCard={o.hasCard}
|
|
233
|
+
hasCod={o.hasCod}
|
|
234
|
+
offlineMethod={o.offlineMethod}
|
|
235
|
+
paymentMethodOrder={o.paymentMethodOrder}
|
|
236
|
+
paymentMarks={paymentMarks}
|
|
237
|
+
termsContent={termsContent}
|
|
238
|
+
paymentTab={o.paymentTab}
|
|
224
239
|
onPaymentTab={o.handlePaymentTab}
|
|
225
240
|
deliveryReady={o.deliveryReady}
|
|
226
241
|
nothingToPay={o.nothingToPay}
|
|
@@ -252,9 +267,10 @@ export function CheckoutClient({
|
|
|
252
267
|
<MobileCheckoutBottomBar
|
|
253
268
|
client={client}
|
|
254
269
|
cart={o.summaryCart}
|
|
255
|
-
optimisticShippingCost={o.optimisticShippingCost}
|
|
256
|
-
optimisticMethodFee={o.optimisticMethodFee}
|
|
257
|
-
|
|
270
|
+
optimisticShippingCost={o.optimisticShippingCost}
|
|
271
|
+
optimisticMethodFee={o.optimisticMethodFee}
|
|
272
|
+
methodFeeLabel={o.paymentTab === "cod" ? o.offlineMethod?.fee_label ?? undefined : undefined}
|
|
273
|
+
showGiftCards={showGiftCards}
|
|
258
274
|
hideDiscount
|
|
259
275
|
onCartChange={handleCartChange}
|
|
260
276
|
/>
|
|
@@ -273,10 +289,12 @@ export function CheckoutClient({
|
|
|
273
289
|
onOptimisticShippingClear={() =>
|
|
274
290
|
o.setOptimisticShippingCost(null)
|
|
275
291
|
}
|
|
276
|
-
optimisticMethodFee={o.optimisticMethodFee}
|
|
277
|
-
onOptimisticMethodFeeClear={() => o.setOptimisticMethodFee(null)}
|
|
278
|
-
|
|
279
|
-
|
|
292
|
+
optimisticMethodFee={o.optimisticMethodFee}
|
|
293
|
+
onOptimisticMethodFeeClear={() => o.setOptimisticMethodFee(null)}
|
|
294
|
+
methodFeeLabel={o.paymentTab === "cod" ? o.offlineMethod?.fee_label ?? undefined : undefined}
|
|
295
|
+
showGiftCards={showGiftCards}
|
|
296
|
+
showSecureCheckout={showSecureCheckout}
|
|
297
|
+
onCartChange={handleCartChange}
|
|
280
298
|
/>
|
|
281
299
|
</div>
|
|
282
300
|
</div>
|
|
@@ -168,7 +168,9 @@ type OrderSummaryProps = {
|
|
|
168
168
|
/** Show the gift-card widget. Default true; pass false for stores that
|
|
169
169
|
* haven't enabled gift cards (the widget itself is harmless either way —
|
|
170
170
|
* an unknown code gets the generic refusal). */
|
|
171
|
-
showGiftCards?: boolean
|
|
171
|
+
showGiftCards?: boolean
|
|
172
|
+
/** Show the security reassurance below the total. Default true. */
|
|
173
|
+
showSecureCheckout?: boolean
|
|
172
174
|
/**
|
|
173
175
|
* Receives every decorated cart returned by a summary mutation
|
|
174
176
|
* (quantity change, promo apply, gift-card apply/remove). Hosts update
|
|
@@ -186,9 +188,10 @@ export function OrderSummary({
|
|
|
186
188
|
onOptimisticShippingClear,
|
|
187
189
|
optimisticMethodFee,
|
|
188
190
|
onOptimisticMethodFeeClear,
|
|
189
|
-
methodFeeLabel,
|
|
190
|
-
showGiftCards = true,
|
|
191
|
-
|
|
191
|
+
methodFeeLabel,
|
|
192
|
+
showGiftCards = true,
|
|
193
|
+
showSecureCheckout = true,
|
|
194
|
+
onCartChange,
|
|
192
195
|
}: OrderSummaryProps) {
|
|
193
196
|
const labels = useCheckoutLabels()
|
|
194
197
|
|
|
@@ -377,7 +380,11 @@ export function OrderSummary({
|
|
|
377
380
|
)}
|
|
378
381
|
</div>
|
|
379
382
|
|
|
380
|
-
<div
|
|
383
|
+
<div
|
|
384
|
+
className={`mx-5 sm:mx-6 mt-4 pt-4 border-t border-border ${
|
|
385
|
+
showSecureCheckout ? "" : "pb-5"
|
|
386
|
+
}`}
|
|
387
|
+
>
|
|
381
388
|
<div className="flex justify-between items-center">
|
|
382
389
|
<span className="text-lg font-bold text-foreground">
|
|
383
390
|
{labels.total}
|
|
@@ -420,22 +427,24 @@ export function OrderSummary({
|
|
|
420
427
|
)}
|
|
421
428
|
</div>
|
|
422
429
|
|
|
423
|
-
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
430
|
+
{showSecureCheckout && (
|
|
431
|
+
<div className="px-5 sm:px-6 pb-5 pt-4 flex items-center justify-center gap-2 text-xs text-muted-foreground">
|
|
432
|
+
<svg
|
|
433
|
+
className="w-3.5 h-3.5"
|
|
434
|
+
fill="none"
|
|
435
|
+
viewBox="0 0 24 24"
|
|
436
|
+
stroke="currentColor"
|
|
437
|
+
strokeWidth={2}
|
|
438
|
+
>
|
|
439
|
+
<path
|
|
440
|
+
strokeLinecap="round"
|
|
441
|
+
strokeLinejoin="round"
|
|
442
|
+
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
|
443
|
+
/>
|
|
444
|
+
</svg>
|
|
445
|
+
<span>{labels.secureCheckout}</span>
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
439
448
|
</div>
|
|
440
449
|
</div>
|
|
441
450
|
)
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { PaymentElement } from "@stripe/react-stripe-js"
|
|
4
4
|
import type { StripePaymentElementChangeEvent } from "@stripe/stripe-js"
|
|
5
|
-
import { useContext, useEffect, useState } from "react"
|
|
6
|
-
|
|
7
|
-
import {
|
|
5
|
+
import { useContext, useEffect, useState } from "react"
|
|
6
|
+
|
|
7
|
+
import type { StorePaymentMethodEntry } from "../api/checkout"
|
|
8
|
+
import { PaymentIcon, type PaymentMethod } from "../common/payment-icons"
|
|
9
|
+
import { cn } from "../lib/utils"
|
|
8
10
|
import { useCheckoutLabels } from "./context"
|
|
9
11
|
import { ErrorMessage } from "./error-message"
|
|
10
12
|
import { PaymentButton } from "./payment-button"
|
|
@@ -34,9 +36,17 @@ import type { CheckoutLogError } from "./use-checkout-orchestration"
|
|
|
34
36
|
type CheckoutPaymentMethodListProps = {
|
|
35
37
|
/** Structural cart — forwarded to PaymentButton (`id`/`total`/`currency_code`). */
|
|
36
38
|
cart: { id: string; total?: number | null; currency_code: string }
|
|
37
|
-
hasCard: boolean
|
|
38
|
-
hasCod: boolean
|
|
39
|
-
|
|
39
|
+
hasCard: boolean
|
|
40
|
+
hasCod: boolean
|
|
41
|
+
/** The merchant-configured method shown by the offline/COD tab. */
|
|
42
|
+
offlineMethod: StorePaymentMethodEntry | null
|
|
43
|
+
/** Platform-defined order after checkout rules have been applied. */
|
|
44
|
+
paymentMethodOrder: Array<"card" | "cod">
|
|
45
|
+
/** Card and wallet marks enabled for the connected payment provider. */
|
|
46
|
+
paymentMarks?: readonly PaymentMethod[]
|
|
47
|
+
/** Store-owned terms notice. Falls back to the locale's plain text. */
|
|
48
|
+
termsContent?: React.ReactNode
|
|
49
|
+
paymentTab: "card" | "cod"
|
|
40
50
|
onPaymentTab: (tab: "card" | "cod") => void
|
|
41
51
|
deliveryReady: boolean
|
|
42
52
|
paymentError: string | null
|
|
@@ -117,9 +127,13 @@ type CheckoutPaymentMethodListProps = {
|
|
|
117
127
|
|
|
118
128
|
export function CheckoutPaymentMethodList({
|
|
119
129
|
cart,
|
|
120
|
-
hasCard,
|
|
121
|
-
hasCod,
|
|
122
|
-
|
|
130
|
+
hasCard,
|
|
131
|
+
hasCod,
|
|
132
|
+
offlineMethod,
|
|
133
|
+
paymentMethodOrder,
|
|
134
|
+
paymentMarks = [],
|
|
135
|
+
termsContent,
|
|
136
|
+
paymentTab,
|
|
123
137
|
onPaymentTab,
|
|
124
138
|
deliveryReady,
|
|
125
139
|
paymentError,
|
|
@@ -138,7 +152,13 @@ export function CheckoutPaymentMethodList({
|
|
|
138
152
|
|
|
139
153
|
// Which extra tab is active, if any. Selecting card/cod clears it; the
|
|
140
154
|
// radio group stays single-select across built-in and extra tabs.
|
|
141
|
-
const [extraTab, setExtraTab] = useState<string | null>(null)
|
|
155
|
+
const [extraTab, setExtraTab] = useState<string | null>(null)
|
|
156
|
+
const methodPosition = (method: "card" | "cod") => {
|
|
157
|
+
const position = paymentMethodOrder.indexOf(method)
|
|
158
|
+
return position === -1 ? paymentMethodOrder.length : position
|
|
159
|
+
}
|
|
160
|
+
const visiblePaymentMarks = paymentMarks.slice(0, 3)
|
|
161
|
+
const hiddenPaymentMarkCount = Math.max(0, paymentMarks.length - visiblePaymentMarks.length)
|
|
142
162
|
|
|
143
163
|
// When the store opts out of gating (gatePaymentUntilDelivery=false),
|
|
144
164
|
// the payment section is always shown + interactive regardless of
|
|
@@ -212,7 +232,9 @@ export function CheckoutPaymentMethodList({
|
|
|
212
232
|
<h2 className="text-lg font-semibold text-foreground mb-1 tracking-tight">
|
|
213
233
|
{labels.paymentMethod}
|
|
214
234
|
</h2>
|
|
215
|
-
<p className="text-xs text-muted-foreground mb-4">
|
|
235
|
+
<p className="text-xs text-muted-foreground mb-4">
|
|
236
|
+
{termsContent ?? labels.termsText}
|
|
237
|
+
</p>
|
|
216
238
|
|
|
217
239
|
{paymentGated ? (
|
|
218
240
|
<div className="p-4 bg-muted rounded-lg border border-border">
|
|
@@ -239,7 +261,7 @@ export function CheckoutPaymentMethodList({
|
|
|
239
261
|
// and tracking refs upstream are NOT torn down. That's the
|
|
240
262
|
// whole point of moving away from PaymentWrapper-wraps-all.
|
|
241
263
|
<StripeElementsScope passthrough>
|
|
242
|
-
<div className="
|
|
264
|
+
<div className="flex flex-col gap-2">
|
|
243
265
|
{nothingToPay && (
|
|
244
266
|
// One line instead of a tender list. The shopper owes
|
|
245
267
|
// nothing, so every radio here would be a question with no
|
|
@@ -250,8 +272,9 @@ export function CheckoutPaymentMethodList({
|
|
|
250
272
|
</div>
|
|
251
273
|
)}
|
|
252
274
|
{!nothingToPay && hasCard && (
|
|
253
|
-
<div
|
|
254
|
-
|
|
275
|
+
<div
|
|
276
|
+
style={{ order: methodPosition("card") }}
|
|
277
|
+
className={cn(
|
|
255
278
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
256
279
|
paymentTab === "card" && !extraTab
|
|
257
280
|
? "border-primary bg-primary/5"
|
|
@@ -276,10 +299,22 @@ export function CheckoutPaymentMethodList({
|
|
|
276
299
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
277
300
|
)}
|
|
278
301
|
</div>
|
|
279
|
-
<span className="text-sm font-medium text-foreground">
|
|
280
|
-
{labels.payOnline}
|
|
281
|
-
</span>
|
|
282
|
-
|
|
302
|
+
<span className="text-sm font-medium text-foreground">
|
|
303
|
+
{labels.payOnline}
|
|
304
|
+
</span>
|
|
305
|
+
{visiblePaymentMarks.length > 0 && (
|
|
306
|
+
<span className="ml-auto flex items-center gap-1.5 pl-3">
|
|
307
|
+
{visiblePaymentMarks.map((method) => (
|
|
308
|
+
<PaymentIcon key={method} method={method} className="h-6" />
|
|
309
|
+
))}
|
|
310
|
+
{hiddenPaymentMarkCount > 0 && (
|
|
311
|
+
<span className="flex h-6 min-w-8 items-center justify-center rounded border border-border bg-card px-1.5 text-xs font-medium text-foreground">
|
|
312
|
+
+{hiddenPaymentMarkCount}
|
|
313
|
+
</span>
|
|
314
|
+
)}
|
|
315
|
+
</span>
|
|
316
|
+
)}
|
|
317
|
+
</button>
|
|
283
318
|
|
|
284
319
|
{paymentTab === "card" && !extraTab && (
|
|
285
320
|
<div className="px-4 pb-4 pt-2">
|
|
@@ -329,8 +364,9 @@ export function CheckoutPaymentMethodList({
|
|
|
329
364
|
)}
|
|
330
365
|
|
|
331
366
|
{!nothingToPay && hasCod && (
|
|
332
|
-
<div
|
|
333
|
-
|
|
367
|
+
<div
|
|
368
|
+
style={{ order: methodPosition("cod") }}
|
|
369
|
+
className={cn(
|
|
334
370
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
335
371
|
paymentTab === "cod" && !extraTab
|
|
336
372
|
? "border-primary bg-primary/5"
|
|
@@ -355,23 +391,24 @@ export function CheckoutPaymentMethodList({
|
|
|
355
391
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
356
392
|
)}
|
|
357
393
|
</div>
|
|
358
|
-
<span className="text-sm font-medium text-foreground">
|
|
359
|
-
{labels.cashOnDelivery}
|
|
360
|
-
</span>
|
|
394
|
+
<span className="text-sm font-medium text-foreground">
|
|
395
|
+
{offlineMethod?.name ?? labels.cashOnDelivery}
|
|
396
|
+
</span>
|
|
361
397
|
</button>
|
|
362
398
|
{paymentTab === "cod" && !extraTab && (
|
|
363
399
|
<div className="px-4 pb-3 pt-0">
|
|
364
|
-
<p className="text-xs text-muted-foreground ml-[30px]">
|
|
365
|
-
{labels.codNote}
|
|
366
|
-
</p>
|
|
400
|
+
<p className="text-xs text-muted-foreground ml-[30px]">
|
|
401
|
+
{offlineMethod?.instructions ?? labels.codNote}
|
|
402
|
+
</p>
|
|
367
403
|
</div>
|
|
368
404
|
)}
|
|
369
405
|
</div>
|
|
370
406
|
)}
|
|
371
407
|
|
|
372
|
-
{!nothingToPay && extraTabs?.map((tab) => (
|
|
373
|
-
<div
|
|
374
|
-
key={tab.id}
|
|
408
|
+
{!nothingToPay && extraTabs?.map((tab, index) => (
|
|
409
|
+
<div
|
|
410
|
+
key={tab.id}
|
|
411
|
+
style={{ order: paymentMethodOrder.length + index }}
|
|
375
412
|
className={cn(
|
|
376
413
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
377
414
|
extraTab === tab.id
|
|
@@ -177,10 +177,11 @@ export function PickupPointSelector({
|
|
|
177
177
|
<div className="flex-1 min-w-0">
|
|
178
178
|
<p className="text-sm font-medium text-foreground leading-tight">
|
|
179
179
|
{point.name}
|
|
180
|
-
</p>
|
|
181
|
-
<p className="text-xs text-muted-foreground mt-0.5">
|
|
182
|
-
{
|
|
183
|
-
|
|
180
|
+
</p>
|
|
181
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
182
|
+
{point.secondary_name ||
|
|
183
|
+
[point.address, point.city].filter(Boolean).join(", ")}
|
|
184
|
+
</p>
|
|
184
185
|
</div>
|
|
185
186
|
|
|
186
187
|
{distance !== null && distance > 0 && (
|
|
@@ -977,9 +977,37 @@ export function useCheckoutOrchestration({
|
|
|
977
977
|
// The offline tab = a merchant METHOD (the COD switch, else the first
|
|
978
978
|
// manual method) — sessions initiate by payment_method_id, provider NULL.
|
|
979
979
|
const offlineMethod = findOfflineMethod(effectiveAvailablePaymentMethods)
|
|
980
|
-
const hasCod = !!offlineMethod
|
|
981
|
-
const codMethodId = offlineMethod?.payment_method_id
|
|
982
|
-
const offlineIsCodKind = offlineMethod?.kind === "cod"
|
|
980
|
+
const hasCod = !!offlineMethod
|
|
981
|
+
const codMethodId = offlineMethod?.payment_method_id
|
|
982
|
+
const offlineIsCodKind = offlineMethod?.kind === "cod"
|
|
983
|
+
|
|
984
|
+
// Preserve the platform listing order all the way into the checkout UI.
|
|
985
|
+
// The API has already applied checkout_method_order; collapsing the rows
|
|
986
|
+
// into card/COD booleans used to throw that order away.
|
|
987
|
+
const paymentMethodOrder = useMemo(() => {
|
|
988
|
+
const order: Array<"card" | "cod"> = []
|
|
989
|
+
for (const entry of effectiveAvailablePaymentMethods ?? []) {
|
|
990
|
+
if (
|
|
991
|
+
hasCard &&
|
|
992
|
+
"id" in entry &&
|
|
993
|
+
entry.id === cardEntry?.id &&
|
|
994
|
+
!order.includes("card")
|
|
995
|
+
) {
|
|
996
|
+
order.push("card")
|
|
997
|
+
}
|
|
998
|
+
if (
|
|
999
|
+
hasCod &&
|
|
1000
|
+
"payment_method_id" in entry &&
|
|
1001
|
+
entry.payment_method_id === codMethodId &&
|
|
1002
|
+
!order.includes("cod")
|
|
1003
|
+
) {
|
|
1004
|
+
order.push("cod")
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (hasCard && !order.includes("card")) order.push("card")
|
|
1008
|
+
if (hasCod && !order.includes("cod")) order.push("cod")
|
|
1009
|
+
return order
|
|
1010
|
+
}, [effectiveAvailablePaymentMethods, hasCard, cardEntry?.id, hasCod, codMethodId])
|
|
983
1011
|
|
|
984
1012
|
// The store offers a card it cannot mount: reported once, to the merchant,
|
|
985
1013
|
// because the shopper must never be the one who discovers it. Pre-2026-09-18
|
|
@@ -996,12 +1024,12 @@ export function useCheckoutOrchestration({
|
|
|
996
1024
|
)
|
|
997
1025
|
}, [cardEntry, cardOffer.mountable, logError])
|
|
998
1026
|
|
|
999
|
-
// Default tab:
|
|
1000
|
-
// used to seed from the cart's pending session provider; in the
|
|
1001
|
-
// deferred-intent model there is no session at mount.
|
|
1002
|
-
const [paymentTab, setPaymentTab] = useState<"card" | "cod">(
|
|
1003
|
-
hasCard ? "card" : "cod"
|
|
1004
|
-
)
|
|
1027
|
+
// Default tab: the first method in the merchant's checkout order. The eager-session model
|
|
1028
|
+
// used to seed from the cart's pending session provider; in the
|
|
1029
|
+
// deferred-intent model there is no session at mount.
|
|
1030
|
+
const [paymentTab, setPaymentTab] = useState<"card" | "cod">(
|
|
1031
|
+
paymentMethodOrder[0] ?? (hasCard ? "card" : "cod")
|
|
1032
|
+
)
|
|
1005
1033
|
|
|
1006
1034
|
// Optimistic method-fee state. Painted instantly on tab toggle so the
|
|
1007
1035
|
// totals row shows the predicted fee BEFORE the server applies it (at
|
|
@@ -1789,8 +1817,9 @@ export function useCheckoutOrchestration({
|
|
|
1789
1817
|
selectedPickup,
|
|
1790
1818
|
|
|
1791
1819
|
// Payment
|
|
1792
|
-
paymentTab,
|
|
1793
|
-
|
|
1820
|
+
paymentTab,
|
|
1821
|
+
paymentMethodOrder,
|
|
1822
|
+
hasCard,
|
|
1794
1823
|
hasCod,
|
|
1795
1824
|
cardId,
|
|
1796
1825
|
/** The store's own Stripe, off the card entry — what `PaymentWrapper`
|