@cartbase/storefront 0.1.2 → 0.3.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/README.md +3 -3
- package/package.json +231 -226
- package/src/api/auth.ts +1 -1
- package/src/api/carts.ts +5 -3
- package/src/api/checkout.ts +91 -40
- package/src/api/consent.ts +2 -2
- package/src/api/integrations.ts +4 -17
- package/src/api/regions.ts +1 -1
- package/src/cart-drawer/context.tsx +4 -4
- package/src/cart-drawer/cross-sell-carousel.tsx +1 -1
- package/src/cart-drawer/cross-sell-sidebar.tsx +1 -1
- package/src/cart-drawer/header.tsx +1 -1
- package/src/cart-drawer/item/index.tsx +2 -2
- package/src/cart-drawer/item/quantity.tsx +2 -2
- package/src/cart-drawer/item/upsell.tsx +3 -3
- package/src/cart-drawer/item/variant.tsx +2 -2
- package/src/cart-drawer/labels-bg.ts +2 -2
- package/src/cart-drawer/labels.ts +119 -119
- package/src/cart-drawer/summary-breakdown.tsx +197 -196
- package/src/cart-drawer/template.tsx +2 -2
- package/src/cart-drawer/tiered-progress.tsx +1 -1
- package/src/checkout/address-error-copy.ts +3 -3
- package/src/checkout/address-form.tsx +2 -2
- package/src/checkout/address-select.tsx +1 -1
- package/src/checkout/boxnow-locker-selector.tsx +3 -3
- package/src/checkout/checkout-client.tsx +15 -12
- package/src/checkout/company-details.tsx +1 -1
- package/src/checkout/compare-addresses.ts +1 -1
- package/src/checkout/discount-section.tsx +5 -5
- package/src/checkout/econt-office-selector.tsx +2 -2
- package/src/checkout/gift-card-section.tsx +2 -2
- package/src/checkout/index.ts +0 -1
- package/src/checkout/labels-bg.ts +2 -2
- package/src/checkout/labels.ts +263 -263
- package/src/checkout/line-item-card.tsx +2 -2
- package/src/checkout/order-summary.tsx +48 -48
- package/src/checkout/payment-button.tsx +4 -4
- package/src/checkout/payment-error-copy.ts +6 -6
- package/src/checkout/payment-method-list.tsx +93 -33
- package/src/checkout/payment-wrapper.tsx +8 -11
- package/src/checkout/promotion-error-copy.ts +2 -2
- package/src/checkout/shipping-method-list.tsx +2 -2
- package/src/checkout/stripe-wrapper.tsx +1 -1
- package/src/checkout/use-checkout-orchestration.ts +108 -98
- package/src/common/cart-button-client.tsx +1 -1
- package/src/common/cart-button.tsx +1 -1
- package/src/common/country-select.tsx +2 -2
- package/src/common/delete-button.tsx +1 -1
- package/src/common/index.ts +1 -1
- package/src/common/language-select.tsx +2 -2
- package/src/common/localized-link.tsx +1 -1
- package/src/lib/cart-helpers.ts +2 -2
- package/src/lib/cookie-names.ts +41 -0
- package/src/lib/get-product-price.ts +3 -3
- package/src/lib/payment-constants.ts +53 -66
- package/src/lib/store-api-error.ts +1 -1
- package/src/order/index.ts +1 -1
- package/src/order/labels-bg.ts +39 -39
- package/src/order/labels.ts +79 -79
- package/src/order/order-address-card.tsx +1 -1
- package/src/order/order-completed-template.tsx +20 -12
- package/src/order/order-confirmation-header.tsx +1 -1
- package/src/order/order-delivery-card.tsx +3 -3
- package/src/order/order-item.tsx +2 -2
- package/src/order/order-items-list.tsx +3 -3
- package/src/order/order-payment-card.tsx +27 -36
- package/src/order/order-timeline.tsx +2 -2
- package/src/order/order-totals.tsx +250 -245
- package/src/platform/identity.ts +18 -0
- package/src/platform/index.ts +8 -0
- package/src/platform/metadata.ts +26 -0
- package/src/platform/platform-init.tsx +46 -0
- package/src/products/image-gallery.tsx +1 -1
- package/src/products/mobile-actions.tsx +1 -1
- package/src/products/option-select.tsx +1 -1
- package/src/products/preview-price.tsx +1 -1
- package/src/products/product-actions.tsx +6 -6
- package/src/products/product-info.tsx +1 -1
- package/src/products/product-preview.tsx +1 -1
- package/src/products/product-price.tsx +2 -2
- package/src/products/product-tabs.tsx +1 -1
- package/src/products/purchase-options.tsx +1 -1
- package/src/products/related-products.tsx +3 -3
- package/src/products/variant-matching.ts +4 -4
- package/src/reviews-ui/labels.ts +1 -1
- package/src/reviews-ui/photo-upload.tsx +1 -1
- package/src/reviews-ui/review-list.tsx +1 -1
- package/src/reviews-ui/review-widget.tsx +1 -1
- package/src/reviews-ui/review-wizard.tsx +1 -1
- package/src/reviews-ui/wizard-state.ts +1 -1
- package/src/store/category-template.tsx +2 -2
- package/src/store/collection-template.tsx +2 -2
- package/src/store/labels.ts +2 -2
- package/src/store/paginated-products.tsx +1 -1
- package/src/store/search-params.ts +1 -1
- package/src/store/search-template.tsx +1 -1
- package/src/tracking/consent-banner.tsx +1 -1
- package/src/tracking/consent-init.tsx +1 -1
- package/src/tracking/consent.ts +6 -6
- package/src/tracking/fbq.ts +2 -2
- package/src/tracking/get-tracking-attribution.ts +1 -1
- package/src/tracking/get-tracking-config.ts +2 -2
- package/src/tracking/index.ts +2 -2
- package/src/tracking/meta-pixel.tsx +1 -1
- package/src/tracking/rybbit.tsx +1 -1
- package/src/tracking/types.ts +6 -6
|
@@ -23,9 +23,11 @@ import {
|
|
|
23
23
|
prepareCheckout,
|
|
24
24
|
refreshPaymentIfTerminal as refreshPaymentIfTerminalApi,
|
|
25
25
|
syncPaymentAmount as syncPaymentAmountApi,
|
|
26
|
+
isMethodEntry,
|
|
26
27
|
type PrepareCheckoutInput,
|
|
27
28
|
type RefreshPaymentResult,
|
|
28
|
-
type
|
|
29
|
+
type StorePaymentEntry,
|
|
30
|
+
type StorePaymentMethodEntry,
|
|
29
31
|
type StoreShippingOption,
|
|
30
32
|
type SyncPaymentAmountResult,
|
|
31
33
|
} from "../api/checkout"
|
|
@@ -35,8 +37,7 @@ import {
|
|
|
35
37
|
type StoreCustomer,
|
|
36
38
|
type UpdateCustomerInput,
|
|
37
39
|
} from "../api/customers"
|
|
38
|
-
import
|
|
39
|
-
import { isManual, isStripeLike } from "../lib/payment-constants"
|
|
40
|
+
import { isStripeLike } from "../lib/payment-constants"
|
|
40
41
|
import compareAddresses from "./compare-addresses"
|
|
41
42
|
import { translateAddressError } from "./address-error-copy"
|
|
42
43
|
import { translatePaymentError } from "./payment-error-copy"
|
|
@@ -54,7 +55,7 @@ import type { BoxNowLocker } from "../api/integrations"
|
|
|
54
55
|
* persistence, custom summary).
|
|
55
56
|
*
|
|
56
57
|
* Ported from `@1click/ui/src/checkout/use-checkout-orchestration.ts`
|
|
57
|
-
* (v2.3.1, the deferred-intent architecture) with the
|
|
58
|
+
* (v2.3.1, the deferred-intent architecture) with the Cartbase data seam:
|
|
58
59
|
*
|
|
59
60
|
* - Server actions → `@cartbase/storefront/api` calls on a caller-supplied
|
|
60
61
|
* `StorefrontClient` (`updateCart`, `prepareCheckout`,
|
|
@@ -63,24 +64,25 @@ import type { BoxNowLocker } from "../api/integrations"
|
|
|
63
64
|
* - `placeOrder` (server action with baked-in redirect) →
|
|
64
65
|
* `completeCart()` + template navigation (`orderConfirmedPath` with
|
|
65
66
|
* `{id}`/`{country}` substitution) or the `onOrderPlaced` callback.
|
|
66
|
-
* - **Zero-remainder gift path** (
|
|
67
|
+
* - **Zero-remainder gift path** (Cartbase gift-card tender,
|
|
67
68
|
* docs/storefront/checkout.md): when applied gift cards cover the whole
|
|
68
69
|
* total, `prepareCheckout` returns `client_secret: null` +
|
|
69
70
|
* `provider_id: null` and the card path SKIPS `stripe.confirmPayment`
|
|
70
71
|
* entirely — the cart completes on the gift session alone. The @1click
|
|
71
|
-
* original threw on a missing client_secret;
|
|
72
|
+
* original threw on a missing client_secret; Cartbase treats
|
|
72
73
|
* null-secret + null-provider as the documented gift path.
|
|
73
|
-
* -
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
74
|
+
* - The payment method fee: CART-LEVEL decoration
|
|
75
|
+
* (`payment_method_fee_total` / `payment_method_fee_label`, folded into
|
|
76
|
+
* `cart.total` while a live method session exists) — not a
|
|
77
|
+
* metadata-flagged line item. ANY method may carry a fee; the optimistic
|
|
78
|
+
* prediction reads `fee_amount` off the selected LISTING entry (the
|
|
79
|
+
* integrations `cod` block died 2026-08-11).
|
|
80
|
+
* - Processor ids: `pp_stripe` exactly (lib/payment-constants); merchant
|
|
81
|
+
* methods have no provider id — sessions ride `payment_method_id`.
|
|
80
82
|
* - `logCheckoutError`/`logEvent` (Supabase-side sinks in @1click) have no
|
|
81
|
-
*
|
|
83
|
+
* Cartbase store endpoint — the hook takes an optional `logError` callback
|
|
82
84
|
* so stores wire their own sink; all production log points are kept.
|
|
83
|
-
* -
|
|
85
|
+
* - Cartbase regions carry NO embedded countries array (api/regions.ts
|
|
84
86
|
* divergence note) — the country list is a `countries` option with a
|
|
85
87
|
* `countryCode` single-entry fallback.
|
|
86
88
|
*
|
|
@@ -93,11 +95,10 @@ import type { BoxNowLocker } from "../api/integrations"
|
|
|
93
95
|
* class of fork-rot bug structurally impossible.
|
|
94
96
|
*/
|
|
95
97
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
export type PaymentProviderLike = StorePaymentProvider | { id: string }
|
|
98
|
+
/** A payable entry the hook accepts: a processor row (`{id}`) or a
|
|
99
|
+
* merchant-method row (`{payment_method_id, name, kind}`) — the two wire
|
|
100
|
+
* shapes of GET /api/store/payment-providers since the pp_* kill. */
|
|
101
|
+
export type PaymentProviderLike = StorePaymentEntry | { id: string }
|
|
101
102
|
|
|
102
103
|
export type CheckoutLogError = (
|
|
103
104
|
errorType: string,
|
|
@@ -115,7 +116,7 @@ export type UseCheckoutOrchestrationOptions = {
|
|
|
115
116
|
/** Default country code when the cart has no shipping address yet. */
|
|
116
117
|
countryCode?: string
|
|
117
118
|
/**
|
|
118
|
-
* Countries offered in the address form.
|
|
119
|
+
* Countries offered in the address form. Cartbase regions do NOT embed a
|
|
119
120
|
* countries array (store-API divergence), so the host app supplies the
|
|
120
121
|
* list; omitted → a single entry derived from `countryCode`.
|
|
121
122
|
*/
|
|
@@ -128,13 +129,6 @@ export type UseCheckoutOrchestrationOptions = {
|
|
|
128
129
|
methods: PaymentProviderLike[] | null,
|
|
129
130
|
selectedShippingOption: StoreShippingOption | null
|
|
130
131
|
) => PaymentProviderLike[] | null
|
|
131
|
-
/**
|
|
132
|
-
* Optional cash-on-delivery configuration — pass the integrations config
|
|
133
|
-
* `cod` block (`getIntegrationsConfig(client).cod`). When present, the
|
|
134
|
-
* hook paints `optimisticCodFee` on tab-toggle so the totals row updates
|
|
135
|
-
* before the server-side fee (applied at prepare) arrives.
|
|
136
|
-
*/
|
|
137
|
-
codConfig?: CheckoutCodConfig
|
|
138
132
|
/**
|
|
139
133
|
* Order-confirmed redirect path template (`{id}` → order.id,
|
|
140
134
|
* `{country}` → shipping country). Falls back to the CheckoutProvider
|
|
@@ -159,7 +153,7 @@ export type UseCheckoutOrchestrationOptions = {
|
|
|
159
153
|
resolveTrackingMetadata?: () => Record<string, unknown> | undefined
|
|
160
154
|
/**
|
|
161
155
|
* Operational-visibility sink (successor of @1click's logCheckoutError /
|
|
162
|
-
* logEvent Supabase writers —
|
|
156
|
+
* logEvent Supabase writers — Cartbase has no store-side log endpoint).
|
|
163
157
|
* Called with (errorType, message, context). Optional; defaults to no-op.
|
|
164
158
|
*/
|
|
165
159
|
logError?: CheckoutLogError
|
|
@@ -171,7 +165,7 @@ export type UseCheckoutOrchestrationOptions = {
|
|
|
171
165
|
*
|
|
172
166
|
* `shipping_address.phone` is required — Bulgarian carriers (Econt,
|
|
173
167
|
* BoxNow) need it to contact the customer; it's the courier's primary
|
|
174
|
-
* recovery channel when the address is ambiguous. (
|
|
168
|
+
* recovery channel when the address is ambiguous. (Cartbase's
|
|
175
169
|
* prepare-checkout schema requires it too — docs/storefront/checkout.md.)
|
|
176
170
|
*/
|
|
177
171
|
const REQUIRED_ADDRESS_FIELDS = [
|
|
@@ -194,12 +188,18 @@ const REQUIRED_ADDRESS_FIELDS = [
|
|
|
194
188
|
*/
|
|
195
189
|
const ADDRESS_AUTO_SAVE_DEBOUNCE_MS = 600
|
|
196
190
|
|
|
197
|
-
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
/** The offline tab's tender: a merchant METHOD entry (pp_* kill). The COD
|
|
192
|
+
* method (kind 'cod') wins; any manual method serves as the offline tab
|
|
193
|
+
* when no COD switch is on. The entry carries its own fee for prediction. */
|
|
194
|
+
const findOfflineMethod = (
|
|
195
|
+
entries: PaymentProviderLike[] | null | undefined
|
|
196
|
+
): StorePaymentMethodEntry | null => {
|
|
197
|
+
const methods = (entries ?? []).filter(
|
|
198
|
+
(m): m is Extract<PaymentProviderLike, { payment_method_id: string }> =>
|
|
199
|
+
"payment_method_id" in m && isMethodEntry(m as StorePaymentEntry)
|
|
200
|
+
)
|
|
201
|
+
return methods.find((m) => m.kind === "cod") ?? methods[0] ?? null
|
|
202
|
+
}
|
|
203
203
|
|
|
204
204
|
/**
|
|
205
205
|
* Snapshot the address-relevant subset of formData. Used to skip
|
|
@@ -250,7 +250,6 @@ export function useCheckoutOrchestration({
|
|
|
250
250
|
countryCode = "",
|
|
251
251
|
countries,
|
|
252
252
|
paymentMethodFilter,
|
|
253
|
-
codConfig,
|
|
254
253
|
orderConfirmedPath: orderConfirmedPathProp,
|
|
255
254
|
onOrderPlaced,
|
|
256
255
|
resolveTrackingMetadata,
|
|
@@ -322,7 +321,7 @@ export function useCheckoutOrchestration({
|
|
|
322
321
|
: true
|
|
323
322
|
)
|
|
324
323
|
|
|
325
|
-
//
|
|
324
|
+
// Cartbase regions have no embedded countries — the caller-supplied list
|
|
326
325
|
// (or the countryCode fallback) is the authority for both the select
|
|
327
326
|
// options and the saved-address region filter.
|
|
328
327
|
const regionCountries = useMemo(
|
|
@@ -500,7 +499,7 @@ export function useCheckoutOrchestration({
|
|
|
500
499
|
lastSavedSnapshotRef.current = snapshot
|
|
501
500
|
|
|
502
501
|
if (customer) {
|
|
503
|
-
// Sync the profile best-effort.
|
|
502
|
+
// Sync the profile best-effort. Cartbase has first-class company
|
|
504
503
|
// fields on the customer (company_name / company_eik —
|
|
505
504
|
// api/customers.ts), so the invoice data also lands on the
|
|
506
505
|
// profile — an upgrade over the @1click cart-metadata-only
|
|
@@ -702,7 +701,7 @@ export function useCheckoutOrchestration({
|
|
|
702
701
|
[paymentMethodFilter, availablePaymentMethods, selectedShippingOption]
|
|
703
702
|
)
|
|
704
703
|
|
|
705
|
-
// Calculated-rate price resolution.
|
|
704
|
+
// Calculated-rate price resolution. Cartbase serves flat prices today
|
|
706
705
|
// (`price_type: "flat"` always — api/checkout.ts), so this effect
|
|
707
706
|
// normally resolves immediately; the calculated branch is kept for
|
|
708
707
|
// forward-compat with calculated-rate carriers (the /calculate route
|
|
@@ -794,20 +793,20 @@ export function useCheckoutOrchestration({
|
|
|
794
793
|
// ── Payment ─────────────────────────────────────────────────────────
|
|
795
794
|
const [paymentError, setPaymentError] = useState<string | null>(null)
|
|
796
795
|
|
|
797
|
-
const hasCard = !!effectiveAvailablePaymentMethods?.some(
|
|
798
|
-
isStripeLike(m.id)
|
|
799
|
-
)
|
|
800
|
-
const hasCod = !!effectiveAvailablePaymentMethods?.some(
|
|
801
|
-
(m) => isCodLikeId(m.id) || m.id === "pp_manual"
|
|
796
|
+
const hasCard = !!effectiveAvailablePaymentMethods?.some(
|
|
797
|
+
(m) => "id" in m && isStripeLike(m.id)
|
|
802
798
|
)
|
|
803
|
-
const cardId =
|
|
804
|
-
|
|
799
|
+
const cardId = (
|
|
800
|
+
effectiveAvailablePaymentMethods?.find(
|
|
801
|
+
(m) => "id" in m && isStripeLike(m.id)
|
|
802
|
+
) as { id: string } | undefined
|
|
805
803
|
)?.id
|
|
806
|
-
//
|
|
807
|
-
//
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
804
|
+
// The offline tab = a merchant METHOD (the COD switch, else the first
|
|
805
|
+
// manual method) — sessions initiate by payment_method_id, provider NULL.
|
|
806
|
+
const offlineMethod = findOfflineMethod(effectiveAvailablePaymentMethods)
|
|
807
|
+
const hasCod = !!offlineMethod
|
|
808
|
+
const codMethodId = offlineMethod?.payment_method_id
|
|
809
|
+
const offlineIsCodKind = offlineMethod?.kind === "cod"
|
|
811
810
|
|
|
812
811
|
// Default tab: card when available, else COD. The eager-session model
|
|
813
812
|
// used to seed from the cart's pending session provider; in the
|
|
@@ -816,14 +815,15 @@ export function useCheckoutOrchestration({
|
|
|
816
815
|
hasCard ? "card" : "cod"
|
|
817
816
|
)
|
|
818
817
|
|
|
819
|
-
// Optimistic
|
|
820
|
-
// totals row shows the predicted fee BEFORE the server applies
|
|
821
|
-
//
|
|
818
|
+
// Optimistic method-fee state. Painted instantly on tab toggle so the
|
|
819
|
+
// totals row shows the predicted fee BEFORE the server applies it (at
|
|
820
|
+
// prepare, when the method session is minted). Any method may carry a
|
|
821
|
+
// fee — the prediction reads the selected entry's own fee_amount.
|
|
822
822
|
// Three values:
|
|
823
823
|
// - null → no prediction; render whatever the cart says
|
|
824
|
-
// - 0 → predict no fee (toggling
|
|
825
|
-
// - positive → predict the fee at this amount
|
|
826
|
-
const [
|
|
824
|
+
// - 0 → predict no fee (toggling to a fee-less tender)
|
|
825
|
+
// - positive → predict the fee at this amount
|
|
826
|
+
const [optimisticMethodFee, setOptimisticMethodFee] = useState<number | null>(
|
|
827
827
|
null
|
|
828
828
|
)
|
|
829
829
|
|
|
@@ -840,30 +840,28 @@ export function useCheckoutOrchestration({
|
|
|
840
840
|
setPaymentTab(tab)
|
|
841
841
|
setPaymentError(null)
|
|
842
842
|
|
|
843
|
-
// Optimistic
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
codConfig.fee_currency.toLowerCase() ===
|
|
851
|
-
(cart.currency_code || "").toLowerCase()
|
|
852
|
-
if (tab === "cod" && codCurrencyMatches && codId === "pp_cod") {
|
|
853
|
-
setOptimisticCodFee(codConfig.fee_amount)
|
|
854
|
-
} else {
|
|
855
|
-
setOptimisticCodFee(0)
|
|
856
|
-
}
|
|
843
|
+
// Optimistic fee prediction so the totals row updates instantly —
|
|
844
|
+
// the selected METHOD entry carries its own fee; processors charge
|
|
845
|
+
// none. Mirrors the server exactly (resolveMethodFee reads the same
|
|
846
|
+
// row the listing serialized).
|
|
847
|
+
if (offlineMethod) {
|
|
848
|
+
const fee = Number(offlineMethod.fee_amount ?? 0)
|
|
849
|
+
setOptimisticMethodFee(tab === "cod" && fee > 0 ? fee : 0)
|
|
857
850
|
}
|
|
858
851
|
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
852
|
+
const tender =
|
|
853
|
+
tab === "card"
|
|
854
|
+
? cardId
|
|
855
|
+
? { provider_id: cardId }
|
|
856
|
+
: null
|
|
857
|
+
: codMethodId
|
|
858
|
+
? { payment_method_id: codMethodId }
|
|
859
|
+
: null
|
|
860
|
+
if (tender) {
|
|
861
|
+
void syncPaymentAmountApi(client, cart.id, tender).catch(() => {})
|
|
864
862
|
}
|
|
865
863
|
},
|
|
866
|
-
[cart.
|
|
864
|
+
[cart.id, client, cardId, codMethodId, offlineMethod]
|
|
867
865
|
)
|
|
868
866
|
|
|
869
867
|
// Shipping selection is client state only. No addShippingMethod call,
|
|
@@ -1088,26 +1086,26 @@ export function useCheckoutOrchestration({
|
|
|
1088
1086
|
// start = cart.total (covers subtotal + tax + any already-written
|
|
1089
1087
|
// shipping/fee)
|
|
1090
1088
|
// if optimisticShippingCost set: replace cart.shipping_total with it
|
|
1091
|
-
// if
|
|
1092
|
-
// (
|
|
1093
|
-
//
|
|
1089
|
+
// if optimisticMethodFee set: replace cart.payment_method_fee_total
|
|
1090
|
+
// with it (the fee is CART-LEVEL decoration folded into total,
|
|
1091
|
+
// not a fee line item)
|
|
1094
1092
|
// When nothing is optimistic, this collapses to cart.total.
|
|
1095
1093
|
const optimisticTotal = useMemo(() => {
|
|
1096
|
-
const
|
|
1094
|
+
const realMethodFeeAmount = cart?.payment_method_fee_total ?? 0
|
|
1097
1095
|
let total = cart?.total ?? 0
|
|
1098
1096
|
if (optimisticShippingCost !== null) {
|
|
1099
1097
|
total = total - (cart?.shipping_total ?? 0) + optimisticShippingCost
|
|
1100
1098
|
}
|
|
1101
|
-
if (
|
|
1102
|
-
total = total -
|
|
1099
|
+
if (optimisticMethodFee !== null && optimisticMethodFee !== undefined) {
|
|
1100
|
+
total = total - realMethodFeeAmount + optimisticMethodFee
|
|
1103
1101
|
}
|
|
1104
1102
|
return total
|
|
1105
1103
|
}, [
|
|
1106
1104
|
cart?.total,
|
|
1107
1105
|
cart?.shipping_total,
|
|
1108
|
-
cart?.
|
|
1106
|
+
cart?.payment_method_fee_total,
|
|
1109
1107
|
optimisticShippingCost,
|
|
1110
|
-
|
|
1108
|
+
optimisticMethodFee,
|
|
1111
1109
|
])
|
|
1112
1110
|
|
|
1113
1111
|
// ── Optimistic total in cents (for Stripe Elements deferred-intent) ─
|
|
@@ -1157,9 +1155,18 @@ export function useCheckoutOrchestration({
|
|
|
1157
1155
|
if (!shippingMethodId) {
|
|
1158
1156
|
throw new Error("No shipping method selected")
|
|
1159
1157
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1158
|
+
// The tender: card tab → the processor; offline tab → the merchant
|
|
1159
|
+
// method (provider-less session, pp_* kill).
|
|
1160
|
+
const tender =
|
|
1161
|
+
paymentTab === "card"
|
|
1162
|
+
? cardId
|
|
1163
|
+
? { payment_provider: cardId }
|
|
1164
|
+
: null
|
|
1165
|
+
: codMethodId
|
|
1166
|
+
? { payment_method_id: codMethodId }
|
|
1167
|
+
: null
|
|
1168
|
+
if (!tender) {
|
|
1169
|
+
throw new Error("No payment method available")
|
|
1163
1170
|
}
|
|
1164
1171
|
|
|
1165
1172
|
return {
|
|
@@ -1175,7 +1182,7 @@ export function useCheckoutOrchestration({
|
|
|
1175
1182
|
},
|
|
1176
1183
|
shipping_method_id: shippingMethodId,
|
|
1177
1184
|
carrier_metadata: carrierMetadata,
|
|
1178
|
-
|
|
1185
|
+
...tender,
|
|
1179
1186
|
}
|
|
1180
1187
|
}, [
|
|
1181
1188
|
formData,
|
|
@@ -1184,7 +1191,7 @@ export function useCheckoutOrchestration({
|
|
|
1184
1191
|
selectedBoxnowLocker,
|
|
1185
1192
|
paymentTab,
|
|
1186
1193
|
cardId,
|
|
1187
|
-
|
|
1194
|
+
codMethodId,
|
|
1188
1195
|
])
|
|
1189
1196
|
|
|
1190
1197
|
// ── Buy click ───────────────────────────────────────────────────────
|
|
@@ -1229,7 +1236,7 @@ export function useCheckoutOrchestration({
|
|
|
1229
1236
|
cart_id: cart.id,
|
|
1230
1237
|
paymentTab,
|
|
1231
1238
|
cardId,
|
|
1232
|
-
|
|
1239
|
+
codMethodId,
|
|
1233
1240
|
selectedShippingMethod,
|
|
1234
1241
|
selectedEcontOffice: selectedEcontOffice
|
|
1235
1242
|
? { code: selectedEcontOffice.code, name: selectedEcontOffice.name }
|
|
@@ -1268,7 +1275,8 @@ export function useCheckoutOrchestration({
|
|
|
1268
1275
|
// eslint-disable-next-line no-console
|
|
1269
1276
|
console.log("[buy-click] PAYLOAD →", payload)
|
|
1270
1277
|
logError?.("other", "buy_click_payload", {
|
|
1271
|
-
provider_id: payload.payment_provider,
|
|
1278
|
+
provider_id: payload.payment_provider ?? null,
|
|
1279
|
+
payment_method_id: payload.payment_method_id ?? null,
|
|
1272
1280
|
shipping_method_id: payload.shipping_method_id,
|
|
1273
1281
|
carrier_metadata_keys: Object.keys(payload.carrier_metadata ?? {}),
|
|
1274
1282
|
country_code: payload.shipping_address.country_code,
|
|
@@ -1287,7 +1295,7 @@ export function useCheckoutOrchestration({
|
|
|
1287
1295
|
provider_id: prep.provider_id,
|
|
1288
1296
|
})
|
|
1289
1297
|
|
|
1290
|
-
// ── Zero-remainder gift path (
|
|
1298
|
+
// ── Zero-remainder gift path (Cartbase gift-card tender) ──────────
|
|
1291
1299
|
// When applied gift cards cover the whole total, prepare skips the
|
|
1292
1300
|
// provider session: client_secret AND provider_id come back null
|
|
1293
1301
|
// and the cart completes on the internal pp_giftcard session alone
|
|
@@ -1420,7 +1428,7 @@ export function useCheckoutOrchestration({
|
|
|
1420
1428
|
cart.id,
|
|
1421
1429
|
paymentTab,
|
|
1422
1430
|
cardId,
|
|
1423
|
-
|
|
1431
|
+
codMethodId,
|
|
1424
1432
|
selectedShippingMethod,
|
|
1425
1433
|
selectedEcontOffice,
|
|
1426
1434
|
selectedBoxnowLocker,
|
|
@@ -1478,12 +1486,14 @@ export function useCheckoutOrchestration({
|
|
|
1478
1486
|
hasCard,
|
|
1479
1487
|
hasCod,
|
|
1480
1488
|
cardId,
|
|
1481
|
-
|
|
1489
|
+
/** The offline tab's method (the COD switch or a manual method). */
|
|
1490
|
+
codMethodId,
|
|
1491
|
+
offlineMethod,
|
|
1482
1492
|
paymentError,
|
|
1483
1493
|
setPaymentError,
|
|
1484
1494
|
deliveryReady,
|
|
1485
|
-
|
|
1486
|
-
|
|
1495
|
+
optimisticMethodFee,
|
|
1496
|
+
setOptimisticMethodFee,
|
|
1487
1497
|
handlePaymentTab,
|
|
1488
1498
|
handlePaymentElementChange,
|
|
1489
1499
|
|
|
@@ -9,7 +9,7 @@ import { useCartDrawer } from "../cart-drawer/context"
|
|
|
9
9
|
* Header cart button (badge + opens the cart drawer). Ported from
|
|
10
10
|
* `@1click/ui/src/common/cart-button-client.tsx` (v2.3.1); seams:
|
|
11
11
|
*
|
|
12
|
-
* - `cart` is the
|
|
12
|
+
* - `cart` is the Cartbase decorated cart (`api/carts` `Cart`).
|
|
13
13
|
* - Badge count uses `productItemCount` (fee-line-aware) instead of the
|
|
14
14
|
* source's raw `reduce` — keeps the badge consistent with the cart
|
|
15
15
|
* drawer's own count, which already excludes backend-injected fee
|
|
@@ -5,7 +5,7 @@ import { CartButtonClient } from "./cart-button-client"
|
|
|
5
5
|
/**
|
|
6
6
|
* Server wrapper for the header cart button. Ported from
|
|
7
7
|
* `@1click/ui/src/common/cart-button.tsx` (v2.3.1); data seam: the source
|
|
8
|
-
* called the cookie-reading `retrieveCart()` server action —
|
|
8
|
+
* called the cookie-reading `retrieveCart()` server action — Cartbase takes
|
|
9
9
|
* the `StorefrontClient` + the app-persisted cart id explicitly (the app
|
|
10
10
|
* owns the cart-id cookie, see docs/storefront/carts.md) and calls
|
|
11
11
|
* `api/carts.retrieveCart`. Any fetch failure (stale id, 404) degrades to
|
|
@@ -9,12 +9,12 @@ import type { StoreRegion } from "../api/regions"
|
|
|
9
9
|
* change forced by the SDK (SDK wins over the source):
|
|
10
10
|
*
|
|
11
11
|
* - Medusa regions embedded a `countries[]` array and the select listed
|
|
12
|
-
* countries;
|
|
12
|
+
* countries; Cartbase regions carry NO countries on the store surface
|
|
13
13
|
* (divergence documented in `api/regions` — tax scoping is by
|
|
14
14
|
* `tax_regions` server-side). The select therefore lists the REGIONS
|
|
15
15
|
* themselves (`regions.listRegions`), valued by region id.
|
|
16
16
|
* - The source called the `updateRegion(countryCode, path)` server
|
|
17
|
-
* action bound to URL `[countryCode]` routing;
|
|
17
|
+
* action bound to URL `[countryCode]` routing; Cartbase region
|
|
18
18
|
* persistence is app-owned (usually `carts.updateCart(client, cartId,
|
|
19
19
|
* {region_id})` + a cookie) — so the change surfaces via `onChange`.
|
|
20
20
|
*/
|
|
@@ -10,7 +10,7 @@ import { cn } from "../lib/utils"
|
|
|
10
10
|
* Cart-line delete button with spinner. Ported from
|
|
11
11
|
* `@1click/ui/src/common/delete-button.tsx` (v2.3.1); data seam: the
|
|
12
12
|
* source called the cookie-scoped `deleteLineItem(id)` server action —
|
|
13
|
-
*
|
|
13
|
+
* Cartbase calls `api/carts.deleteLineItem(client, cartId, lineItemId)`
|
|
14
14
|
* (idempotent server-side) and hands the refreshed cart to `onDeleted`
|
|
15
15
|
* so the app can update its state / `router.refresh()`. The spinner
|
|
16
16
|
* resets on failure exactly like the source's `.catch`.
|
package/src/common/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @cartbase/storefront/common — shared storefront chrome. Ported from
|
|
3
3
|
* `@1click/ui/src/common` (v2.3.1) + `LocalizedLink` (from the
|
|
4
|
-
* mindpages-storefront production component);
|
|
4
|
+
* mindpages-storefront production component); Cartbase data seams per
|
|
5
5
|
* module JSDoc.
|
|
6
6
|
*/
|
|
7
7
|
export { LocalizedLink, type LocalizedLinkProps } from "./localized-link"
|
|
@@ -7,13 +7,13 @@ import { useTransition } from "react"
|
|
|
7
7
|
* `@1click/ui/src/common/language-select.tsx` (v2.3.1); data seam:
|
|
8
8
|
*
|
|
9
9
|
* - The locale list feeds from `api/regions.listLocales(client)` — an
|
|
10
|
-
* array of bare codes, store default first (
|
|
10
|
+
* array of bare codes, store default first (Cartbase has no `{code,
|
|
11
11
|
* name}` locale objects on the store surface; SDK wins). Display
|
|
12
12
|
* names resolve via `localeDisplayName()` below (native-language
|
|
13
13
|
* autonym through `Intl.DisplayNames`), overridable per store with
|
|
14
14
|
* the `labels` map.
|
|
15
15
|
* - The source awaited an `updateLocale(code)` server action then
|
|
16
|
-
* `router.refresh()`;
|
|
16
|
+
* `router.refresh()`; Cartbase locale persistence is app-owned (the
|
|
17
17
|
* cookie read by `StorefrontClient.getLocale`) — the async `onChange`
|
|
18
18
|
* callback carries it, and the `useTransition` pending-disable UX is
|
|
19
19
|
* preserved around it.
|
|
@@ -10,7 +10,7 @@ import type { ReactNode } from "react"
|
|
|
10
10
|
* `LocalizedClientLink` (src/modules/common/components/localized-client-link)
|
|
11
11
|
* — the component `@1click/ui` consumers used for the same job.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* Cartbase seam: Cartbase storefronts default to COOKIE-based locale
|
|
14
14
|
* (`StorefrontClient.getLocale`, see docs/storefront/BUILD-A-STOREFRONT.md)
|
|
15
15
|
* with no mandatory URL segment — so the prefix is applied ONLY when the
|
|
16
16
|
* route actually has a `[countryCode]` (or custom `paramName`) dynamic
|
package/src/lib/cart-helpers.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Ported from `@1click/ui/src/lib/cart-helpers.ts` (v2.3.1) with its
|
|
5
5
|
* production reasoning intact; the `@medusajs/types` generic constraint is
|
|
6
6
|
* replaced by the structural `LineLike` shape so this module stays decoupled
|
|
7
|
-
* from any SDK layer (
|
|
7
|
+
* from any SDK layer (Cartbase cart/order line items are structurally
|
|
8
8
|
* compatible: they carry `metadata`, `total`, `quantity`).
|
|
9
9
|
*
|
|
10
10
|
* Why this exists. There is no native "fee" / "surcharge" primitive in the
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* `findFeeLine`. Customer SHOULD see the fee in checkout / on the
|
|
30
30
|
* order; they should NOT see it in the cart drawer pre-checkout.
|
|
31
31
|
*
|
|
32
|
-
* Money note (
|
|
32
|
+
* Money note (Cartbase): amounts are decimal EUR major units everywhere in
|
|
33
33
|
* the store API — the arithmetic below is unit-agnostic and unchanged.
|
|
34
34
|
*/
|
|
35
35
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie names the store API + @cartbase/storefront agree on
|
|
3
|
+
* (platform-fingerprints card, fingerprint #4 — cookie names are a
|
|
4
|
+
* primary Wappalyzer/BuiltWith detection signal).
|
|
5
|
+
*
|
|
6
|
+
* The app still OWNS *setting* the cart cookie — carts.md is explicit that
|
|
7
|
+
* "the SDK never persists the cart" — this module is the single source of
|
|
8
|
+
* the NAME so every consumer emits the same wire fingerprint instead of
|
|
9
|
+
* inventing its own prefix (the reference app previously hardcoded
|
|
10
|
+
* `_barter_cart_id` locally in `examples/storefront/src/lib/config.ts`).
|
|
11
|
+
*
|
|
12
|
+
* Renamed from the `_barter_*` prefix (2026-08-04, docs/features/
|
|
13
|
+
* cartbase-rename.md). `readCartCookie` accepts the legacy name for the
|
|
14
|
+
* transition window; every write uses the new name only.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Current cart-id cookie name. */
|
|
18
|
+
export const CART_COOKIE = "_cartbase_cart"
|
|
19
|
+
|
|
20
|
+
/** @deprecated Legacy name — read-only compat, never written. */
|
|
21
|
+
export const LEGACY_CART_COOKIE = "_barter_cart_id"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reserved for a future cookie-backed customer session. Auth today is pure
|
|
25
|
+
* Bearer-JWT, persisted by the consuming app (auth.md — "the Cartbase API
|
|
26
|
+
* does not proxy token refresh"); nothing sets this cookie yet. Defined now
|
|
27
|
+
* so a future cookie-based session launches with the fingerprint-correct
|
|
28
|
+
* name from day one rather than inventing a new prefix later.
|
|
29
|
+
*/
|
|
30
|
+
export const SESSION_COOKIE = "_cartbase_session"
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read a cart id given a cookie-name → value lookup, preferring the
|
|
34
|
+
* current name and falling back to the legacy `_barter_cart_id` name so an
|
|
35
|
+
* existing visitor's cart survives the rename.
|
|
36
|
+
*/
|
|
37
|
+
export function readCartCookie(
|
|
38
|
+
get: (name: string) => string | undefined
|
|
39
|
+
): string | undefined {
|
|
40
|
+
return get(CART_COOKIE) ?? get(LEGACY_CART_COOKIE)
|
|
41
|
+
}
|
|
@@ -6,11 +6,11 @@ import { convertToLocale } from "./money"
|
|
|
6
6
|
* `@1click/ui/src/lib/get-product-price.ts` (v2.3.1). The
|
|
7
7
|
* `@medusajs/types` `HttpTypes.StoreProduct` parameter is replaced by the
|
|
8
8
|
* structural shapes below so this module stays decoupled from the SDK
|
|
9
|
-
* domain modules (
|
|
9
|
+
* domain modules (Cartbase's `variant.calculated_price` is the b2b-v1
|
|
10
10
|
* pricing context documented in `@cartbase/storefront/api/types`
|
|
11
11
|
* `CalculatedPrice`, decimal EUR major units).
|
|
12
12
|
*
|
|
13
|
-
* Wire-shape note (code truth, `src/lib/products/pricing.ts`):
|
|
13
|
+
* Wire-shape note (code truth, `src/lib/products/pricing.ts`): Cartbase serves
|
|
14
14
|
* `calculated_price.price_list_type` FLAT on the calculated_price object.
|
|
15
15
|
* The Medusa wire shape ALSO nests a `calculated_price.calculated_price`
|
|
16
16
|
* detail object carrying `price_list_type` — the original library read the
|
|
@@ -71,7 +71,7 @@ export const getPricesForVariant = (
|
|
|
71
71
|
currency_code: cp.currency_code,
|
|
72
72
|
}),
|
|
73
73
|
currency_code: cp.currency_code,
|
|
74
|
-
// Flat key (
|
|
74
|
+
// Flat key (Cartbase wire shape) first — an explicit null stays null;
|
|
75
75
|
// the nested Medusa detail is the fallback only when the flat key is
|
|
76
76
|
// absent entirely (the original @1click read path).
|
|
77
77
|
price_type:
|