@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
|
@@ -17,15 +17,15 @@ import { GiftCardSection } from "./gift-card-section"
|
|
|
17
17
|
* tax / total, optional optimistic overrides used during method selection.
|
|
18
18
|
*
|
|
19
19
|
* Ported from `@1click/ui/src/checkout/order-summary.tsx` (v2.3.1) with
|
|
20
|
-
* the
|
|
20
|
+
* the Cartbase data seam:
|
|
21
21
|
*
|
|
22
|
-
* -
|
|
23
|
-
* `cart.
|
|
24
|
-
* while a live
|
|
25
|
-
* NOT a metadata-flagged fee line item.
|
|
26
|
-
* read those fields; `isProductLine` is
|
|
27
|
-
* any legacy fee-line carts.
|
|
28
|
-
* - Gift-card tender rows (
|
|
22
|
+
* - The payment method fee is CART-LEVEL decoration on the Cartbase wire —
|
|
23
|
+
* `cart.payment_method_fee_total` / `payment_method_fee_label`, folded
|
|
24
|
+
* into `cart.total` while a live method session exists
|
|
25
|
+
* (docs/storefront/checkout.md) — NOT a metadata-flagged fee line item.
|
|
26
|
+
* The fee row + optimistic math read those fields; `isProductLine` is
|
|
27
|
+
* kept as a defensive filter for any legacy fee-line carts.
|
|
28
|
+
* - Gift-card tender rows (Cartbase feature): `gift_card_total` renders as
|
|
29
29
|
* a negative tender row and `gift_card_remainder` as "Remaining to pay"
|
|
30
30
|
* UNDER the (unchanged) total — tender never moves totals
|
|
31
31
|
* (docs/storefront/gift-cards.md). `GiftCardSection` mounts next to
|
|
@@ -211,22 +211,22 @@ type OrderSummaryProps = {
|
|
|
211
211
|
optimisticShippingCost: number | null
|
|
212
212
|
onOptimisticShippingClear?: () => void
|
|
213
213
|
/**
|
|
214
|
-
* Optimistic
|
|
214
|
+
* Optimistic method-fee prediction. Three values:
|
|
215
215
|
* - null → no prediction; render whatever the cart says.
|
|
216
|
-
* - 0 → predict no fee (toggling
|
|
216
|
+
* - 0 → predict no fee (toggling to a fee-less tender).
|
|
217
217
|
* - positive → predict the fee at this amount.
|
|
218
|
-
* Pass `useCheckoutOrchestration`'s `
|
|
219
|
-
* the deferred-checkout architecture defers the
|
|
220
|
-
* the
|
|
218
|
+
* Pass `useCheckoutOrchestration`'s `optimisticMethodFee`. Needed because
|
|
219
|
+
* the deferred-checkout architecture defers the method session (and so
|
|
220
|
+
* the fee) until Buy click.
|
|
221
221
|
*/
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
optimisticMethodFee?: number | null
|
|
223
|
+
onOptimisticMethodFeeClear?: () => void
|
|
224
224
|
/**
|
|
225
|
-
* Override label for the
|
|
226
|
-
*
|
|
227
|
-
*
|
|
225
|
+
* Override label for the payment-method fee row. Optional — falls back
|
|
226
|
+
* to the cart's server-provided `payment_method_fee_label` (the method's
|
|
227
|
+
* own fee_label), then to `labels.paymentMethodFee`.
|
|
228
228
|
*/
|
|
229
|
-
|
|
229
|
+
methodFeeLabel?: string
|
|
230
230
|
/** Show the gift-card widget. Default true; pass false for stores that
|
|
231
231
|
* haven't enabled gift cards (the widget itself is harmless either way —
|
|
232
232
|
* an unknown code gets the generic refusal). */
|
|
@@ -246,15 +246,15 @@ export function OrderSummary({
|
|
|
246
246
|
cart,
|
|
247
247
|
optimisticShippingCost,
|
|
248
248
|
onOptimisticShippingClear,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
optimisticMethodFee,
|
|
250
|
+
onOptimisticMethodFeeClear,
|
|
251
|
+
methodFeeLabel,
|
|
252
252
|
showGiftCards = true,
|
|
253
253
|
onCartChange,
|
|
254
254
|
}: OrderSummaryProps) {
|
|
255
255
|
const labels = useCheckoutLabels()
|
|
256
256
|
|
|
257
|
-
// Product rows only.
|
|
257
|
+
// Product rows only. Cartbase's COD fee is cart-level decoration (never a
|
|
258
258
|
// line item), so this filter is defensive — it also hides any
|
|
259
259
|
// metadata-flagged fee line a legacy/ported cart might still carry.
|
|
260
260
|
const allItems = cart.items ?? []
|
|
@@ -268,39 +268,39 @@ export function OrderSummary({
|
|
|
268
268
|
optimisticShippingCost !== null ? optimisticShippingCost : cart.shipping_total
|
|
269
269
|
const shippingKnown = shippingCost !== null && shippingCost !== undefined
|
|
270
270
|
|
|
271
|
-
// Effective
|
|
272
|
-
// up. Three states (null / 0 / positive).
|
|
273
|
-
// `cart.
|
|
274
|
-
//
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
?
|
|
279
|
-
:
|
|
271
|
+
// Effective method fee: optimistic prediction wins until the cart
|
|
272
|
+
// catches up. Three states (null / 0 / positive). Server truth is
|
|
273
|
+
// `cart.payment_method_fee_total` (folded into cart.total while a live
|
|
274
|
+
// method session exists — docs/storefront/checkout.md).
|
|
275
|
+
const realMethodFeeAmount = cart.payment_method_fee_total ?? 0
|
|
276
|
+
const effectiveMethodFee =
|
|
277
|
+
optimisticMethodFee !== null && optimisticMethodFee !== undefined
|
|
278
|
+
? optimisticMethodFee
|
|
279
|
+
: realMethodFeeAmount
|
|
280
280
|
|
|
281
281
|
const displayTotal = useMemo(() => {
|
|
282
282
|
let total = cart.total ?? 0
|
|
283
283
|
if (optimisticShippingCost !== null) {
|
|
284
284
|
total = total - (cart.shipping_total ?? 0) + optimisticShippingCost
|
|
285
285
|
}
|
|
286
|
-
if (
|
|
287
|
-
total = total -
|
|
286
|
+
if (optimisticMethodFee !== null && optimisticMethodFee !== undefined) {
|
|
287
|
+
total = total - realMethodFeeAmount + effectiveMethodFee
|
|
288
288
|
}
|
|
289
289
|
return total
|
|
290
290
|
}, [
|
|
291
291
|
cart.total,
|
|
292
292
|
cart.shipping_total,
|
|
293
293
|
optimisticShippingCost,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
optimisticMethodFee,
|
|
295
|
+
effectiveMethodFee,
|
|
296
|
+
realMethodFeeAmount,
|
|
297
297
|
])
|
|
298
298
|
|
|
299
|
-
// Subtotal:
|
|
299
|
+
// Subtotal: Cartbase's `item_total` is products-only (the fee is not a
|
|
300
300
|
// line item), so no fee subtraction is needed — direct server truth.
|
|
301
301
|
const productSubtotal = cart.item_total ?? 0
|
|
302
302
|
|
|
303
|
-
const
|
|
303
|
+
const methodFeeAmount = effectiveMethodFee
|
|
304
304
|
|
|
305
305
|
// Gift-card tender decoration — SERVER truth, rendered as-is. Totals
|
|
306
306
|
// never move; the tender rows sit under the total.
|
|
@@ -317,13 +317,13 @@ export function OrderSummary({
|
|
|
317
317
|
}
|
|
318
318
|
}, [cart.shipping_total, optimisticShippingCost, onOptimisticShippingClear])
|
|
319
319
|
|
|
320
|
-
// Clear optimistic
|
|
321
|
-
// prediction (
|
|
320
|
+
// Clear the optimistic fee once the cart's decoration matches the
|
|
321
|
+
// prediction (payment_method_fee_total, not a fee line item).
|
|
322
322
|
useEffect(() => {
|
|
323
|
-
if (
|
|
324
|
-
const synced =
|
|
325
|
-
if (synced)
|
|
326
|
-
}, [
|
|
323
|
+
if (optimisticMethodFee === null || optimisticMethodFee === undefined) return
|
|
324
|
+
const synced = realMethodFeeAmount === optimisticMethodFee
|
|
325
|
+
if (synced) onOptimisticMethodFeeClear?.()
|
|
326
|
+
}, [optimisticMethodFee, realMethodFeeAmount, onOptimisticMethodFeeClear])
|
|
327
327
|
|
|
328
328
|
return (
|
|
329
329
|
<div>
|
|
@@ -404,13 +404,13 @@ export function OrderSummary({
|
|
|
404
404
|
)}
|
|
405
405
|
</div>
|
|
406
406
|
|
|
407
|
-
{
|
|
407
|
+
{methodFeeAmount > 0 && (
|
|
408
408
|
<div className="flex justify-between text-sm">
|
|
409
409
|
<span className="text-muted-foreground">
|
|
410
|
-
{
|
|
410
|
+
{methodFeeLabel ?? cart.payment_method_fee_label ?? labels.paymentMethodFee}
|
|
411
411
|
</span>
|
|
412
412
|
<DualPrice
|
|
413
|
-
amount={
|
|
413
|
+
amount={methodFeeAmount}
|
|
414
414
|
currencyCode={cart.currency_code}
|
|
415
415
|
className="font-medium text-foreground"
|
|
416
416
|
/>
|
|
@@ -18,10 +18,10 @@ const PROCESSING_MESSAGE_INTERVAL_MS = 1800
|
|
|
18
18
|
* PaymentButton — top-level "place order" button.
|
|
19
19
|
*
|
|
20
20
|
* Ported from `@1click/ui/src/checkout/payment-button.tsx` (v2.3.1) with
|
|
21
|
-
* the
|
|
22
|
-
* `currency_code` are read — any decorated
|
|
21
|
+
* the Cartbase data seam: the `cart` prop is structural (only `id`, `total`,
|
|
22
|
+
* `currency_code` are read — any decorated Cartbase cart satisfies it), and
|
|
23
23
|
* the `logCheckoutError` Supabase writer becomes the optional `logError`
|
|
24
|
-
* callback (
|
|
24
|
+
* callback (Cartbase has no store-side log endpoint; every production log
|
|
25
25
|
* point is preserved through the callback).
|
|
26
26
|
*
|
|
27
27
|
* The button delegates entirely to the orchestration hook's
|
|
@@ -270,7 +270,7 @@ export function PaymentButton({
|
|
|
270
270
|
setSubmitting(false)
|
|
271
271
|
} catch (err: unknown) {
|
|
272
272
|
// Next.js's `redirect()` throws a NEXT_REDIRECT error — that is
|
|
273
|
-
// success, not failure. The
|
|
273
|
+
// success, not failure. The Cartbase default path navigates via
|
|
274
274
|
// window.location.assign (no throw), but a store routing
|
|
275
275
|
// `onOrderPlaced` through a server action still hits this; don't
|
|
276
276
|
// translate it — let it propagate so the navigation happens.
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* Bulgarian copy for the checkout payment surface.
|
|
4
4
|
*
|
|
5
5
|
* Ported from `@1click/ui/src/checkout/payment-error-copy.ts` (v2.3.1) and
|
|
6
|
-
* adapted to the
|
|
6
|
+
* adapted to the Cartbase error envelope: the store API throws `StoreApiError`
|
|
7
7
|
* with a STRUCTURED `code` (docs/storefront/checkout.md lists every code per
|
|
8
8
|
* endpoint), so translation is **code-first** — the substring patterns from
|
|
9
9
|
* the Medusa era are kept as the fallback layer for Stripe.js browser errors
|
|
10
|
-
* (which have no
|
|
10
|
+
* (which have no Cartbase code) and any legacy wire.
|
|
11
11
|
*
|
|
12
12
|
* Why this exists: Stripe and API errors are technical English strings
|
|
13
13
|
* ("Your card's security code is incorrect.", "fetch failed"). Showing them
|
|
@@ -26,7 +26,7 @@ const GENERIC_COD =
|
|
|
26
26
|
"Поръчката не може да бъде финализирана в момента. Моля, опитайте отново след малко."
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Cartbase store-API error codes → BG copy. Covers EVERY code documented for
|
|
30
30
|
* POST /api/store/carts/:id/complete, prepare-checkout,
|
|
31
31
|
* sync-payment-amount and refresh-payment-if-terminal
|
|
32
32
|
* (docs/storefront/checkout.md + carts.completeCart JSDoc). Code-first:
|
|
@@ -110,7 +110,7 @@ const GIFT_CARD_GENERIC =
|
|
|
110
110
|
* Map of Stripe error codes / decline codes / common substrings to BG copy.
|
|
111
111
|
* Substring match is case-insensitive. First match wins, so order from
|
|
112
112
|
* most-specific to most-generic. Kept verbatim from the production port —
|
|
113
|
-
* Stripe.js browser errors carry no
|
|
113
|
+
* Stripe.js browser errors carry no Cartbase `code`, so this layer still does
|
|
114
114
|
* the heavy lifting on the card path.
|
|
115
115
|
*
|
|
116
116
|
* Stripe error code reference:
|
|
@@ -203,7 +203,7 @@ const API_PATTERNS: Array<{ match: RegExp; copy: string }> = [
|
|
|
203
203
|
},
|
|
204
204
|
]
|
|
205
205
|
|
|
206
|
-
/** Pull the structured
|
|
206
|
+
/** Pull the structured Cartbase error code out of any thrown value. */
|
|
207
207
|
function extractCode(err: unknown): string | null {
|
|
208
208
|
if (err && typeof err === "object") {
|
|
209
209
|
const code = (err as { code?: unknown }).code
|
|
@@ -214,7 +214,7 @@ function extractCode(err: unknown): string | null {
|
|
|
214
214
|
|
|
215
215
|
/**
|
|
216
216
|
* Translate any thrown / returned error into customer-facing Bulgarian.
|
|
217
|
-
* Code-first (
|
|
217
|
+
* Code-first (Cartbase `StoreApiError.code`), then Stripe substring patterns,
|
|
218
218
|
* then legacy API substrings, then the per-context generic.
|
|
219
219
|
*
|
|
220
220
|
* @param err - the error from a try/catch or a Stripe error response
|
|
@@ -19,15 +19,16 @@ import type { CheckoutLogError } from "./use-checkout-orchestration"
|
|
|
19
19
|
* per-method code here.
|
|
20
20
|
*
|
|
21
21
|
* Ported from `@1click/ui/src/checkout/payment-method-list.tsx` (v2.3.1)
|
|
22
|
-
* with the
|
|
22
|
+
* with the Cartbase data seam: the `cart` prop is structural, and the
|
|
23
23
|
* `logCheckoutError` Supabase writer becomes the optional `logError`
|
|
24
|
-
* callback.
|
|
25
|
-
* `pp_stripe
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
24
|
+
* callback. Tender semantics on the Cartbase wire (pp_* kill): the "card"
|
|
25
|
+
* tab is the connected processor (`pp_stripe`); the "cod" tab is a
|
|
26
|
+
* merchant METHOD — the COD switch (native fee via the integrations `cod`
|
|
27
|
+
* block) or a manual method (no fee) — the orchestration hook resolves
|
|
28
|
+
* them (`hasCard`/`hasCod`/`cardId`/`codMethodId`); both listings arrive
|
|
29
|
+
* rule-filtered server-side (`checkout.listPaymentProviders`,
|
|
30
|
+
* docs/storefront/checkout.md). The method tab stays a separate rail —
|
|
31
|
+
* it's a provider-less session, not a Stripe payment method.
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
34
|
type CheckoutPaymentMethodListProps = {
|
|
@@ -87,6 +88,15 @@ type CheckoutPaymentMethodListProps = {
|
|
|
87
88
|
total?: number
|
|
88
89
|
/** Operational-visibility sink (successor of @1click's logCheckoutError). */
|
|
89
90
|
logError?: CheckoutLogError
|
|
91
|
+
/**
|
|
92
|
+
* Additional payment tabs rendered as radio cards in the same visual
|
|
93
|
+
* language as the built-in card/cod tabs. The seam for embedded
|
|
94
|
+
* third-party providers (a myPOS-style frame carries its own pay
|
|
95
|
+
* action, so while an extra tab is active the PaymentButton is hidden
|
|
96
|
+
* and the tab's `content` owns completion). Absent = exact previous
|
|
97
|
+
* behavior.
|
|
98
|
+
*/
|
|
99
|
+
extraTabs?: Array<{ id: string; label: string; content: React.ReactNode }>
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
export function CheckoutPaymentMethodList({
|
|
@@ -105,9 +115,14 @@ export function CheckoutPaymentMethodList({
|
|
|
105
115
|
beforePaymentButton,
|
|
106
116
|
total,
|
|
107
117
|
logError,
|
|
118
|
+
extraTabs,
|
|
108
119
|
}: CheckoutPaymentMethodListProps) {
|
|
109
120
|
const labels = useCheckoutLabels()
|
|
110
121
|
|
|
122
|
+
// Which extra tab is active, if any. Selecting card/cod clears it; the
|
|
123
|
+
// radio group stays single-select across built-in and extra tabs.
|
|
124
|
+
const [extraTab, setExtraTab] = useState<string | null>(null)
|
|
125
|
+
|
|
111
126
|
// When the store opts out of gating (gatePaymentUntilDelivery=false),
|
|
112
127
|
// the payment section is always shown + interactive regardless of
|
|
113
128
|
// delivery readiness. The Buy button remains independently gated by
|
|
@@ -203,23 +218,26 @@ export function CheckoutPaymentMethodList({
|
|
|
203
218
|
<div
|
|
204
219
|
className={cn(
|
|
205
220
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
206
|
-
paymentTab === "card"
|
|
221
|
+
paymentTab === "card" && !extraTab
|
|
207
222
|
? "border-primary bg-primary/5"
|
|
208
223
|
: "border-border bg-card hover:border-muted-foreground"
|
|
209
224
|
)}
|
|
210
225
|
>
|
|
211
226
|
<button
|
|
212
227
|
type="button"
|
|
213
|
-
onClick={() =>
|
|
228
|
+
onClick={() => {
|
|
229
|
+
setExtraTab(null)
|
|
230
|
+
onPaymentTab("card")
|
|
231
|
+
}}
|
|
214
232
|
className="flex items-center w-full px-4 py-3.5 text-left"
|
|
215
233
|
>
|
|
216
234
|
<div
|
|
217
235
|
className={cn(
|
|
218
236
|
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 mr-3 transition-colors",
|
|
219
|
-
paymentTab === "card" ? "border-primary" : "border-border"
|
|
237
|
+
paymentTab === "card" && !extraTab ? "border-primary" : "border-border"
|
|
220
238
|
)}
|
|
221
239
|
>
|
|
222
|
-
{paymentTab === "card" && (
|
|
240
|
+
{paymentTab === "card" && !extraTab && (
|
|
223
241
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
224
242
|
)}
|
|
225
243
|
</div>
|
|
@@ -228,7 +246,7 @@ export function CheckoutPaymentMethodList({
|
|
|
228
246
|
</span>
|
|
229
247
|
</button>
|
|
230
248
|
|
|
231
|
-
{paymentTab === "card" && (
|
|
249
|
+
{paymentTab === "card" && !extraTab && (
|
|
232
250
|
<div className="px-4 pb-4 pt-2">
|
|
233
251
|
{/*
|
|
234
252
|
Skeleton vs PaymentElement gated by stripeReady.
|
|
@@ -279,23 +297,26 @@ export function CheckoutPaymentMethodList({
|
|
|
279
297
|
<div
|
|
280
298
|
className={cn(
|
|
281
299
|
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
282
|
-
paymentTab === "cod"
|
|
300
|
+
paymentTab === "cod" && !extraTab
|
|
283
301
|
? "border-primary bg-primary/5"
|
|
284
302
|
: "border-border bg-card hover:border-muted-foreground"
|
|
285
303
|
)}
|
|
286
304
|
>
|
|
287
305
|
<button
|
|
288
306
|
type="button"
|
|
289
|
-
onClick={() =>
|
|
307
|
+
onClick={() => {
|
|
308
|
+
setExtraTab(null)
|
|
309
|
+
onPaymentTab("cod")
|
|
310
|
+
}}
|
|
290
311
|
className="flex items-center w-full px-4 py-3.5 text-left"
|
|
291
312
|
>
|
|
292
313
|
<div
|
|
293
314
|
className={cn(
|
|
294
315
|
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 mr-3 transition-colors",
|
|
295
|
-
paymentTab === "cod" ? "border-primary" : "border-border"
|
|
316
|
+
paymentTab === "cod" && !extraTab ? "border-primary" : "border-border"
|
|
296
317
|
)}
|
|
297
318
|
>
|
|
298
|
-
{paymentTab === "cod" && (
|
|
319
|
+
{paymentTab === "cod" && !extraTab && (
|
|
299
320
|
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
300
321
|
)}
|
|
301
322
|
</div>
|
|
@@ -303,7 +324,7 @@ export function CheckoutPaymentMethodList({
|
|
|
303
324
|
{labels.cashOnDelivery}
|
|
304
325
|
</span>
|
|
305
326
|
</button>
|
|
306
|
-
{paymentTab === "cod" && (
|
|
327
|
+
{paymentTab === "cod" && !extraTab && (
|
|
307
328
|
<div className="px-4 pb-3 pt-0">
|
|
308
329
|
<p className="text-xs text-muted-foreground ml-[30px]">
|
|
309
330
|
{labels.codNote}
|
|
@@ -312,6 +333,41 @@ export function CheckoutPaymentMethodList({
|
|
|
312
333
|
)}
|
|
313
334
|
</div>
|
|
314
335
|
)}
|
|
336
|
+
|
|
337
|
+
{extraTabs?.map((tab) => (
|
|
338
|
+
<div
|
|
339
|
+
key={tab.id}
|
|
340
|
+
className={cn(
|
|
341
|
+
"rounded-lg border overflow-hidden transition-colors duration-150",
|
|
342
|
+
extraTab === tab.id
|
|
343
|
+
? "border-primary bg-primary/5"
|
|
344
|
+
: "border-border bg-card hover:border-muted-foreground"
|
|
345
|
+
)}
|
|
346
|
+
>
|
|
347
|
+
<button
|
|
348
|
+
type="button"
|
|
349
|
+
onClick={() => setExtraTab(tab.id)}
|
|
350
|
+
className="flex items-center w-full px-4 py-3.5 text-left"
|
|
351
|
+
>
|
|
352
|
+
<div
|
|
353
|
+
className={cn(
|
|
354
|
+
"w-[18px] h-[18px] rounded-full border-2 flex items-center justify-center flex-shrink-0 mr-3 transition-colors",
|
|
355
|
+
extraTab === tab.id ? "border-primary" : "border-border"
|
|
356
|
+
)}
|
|
357
|
+
>
|
|
358
|
+
{extraTab === tab.id && (
|
|
359
|
+
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
<span className="text-sm font-medium text-foreground">
|
|
363
|
+
{tab.label}
|
|
364
|
+
</span>
|
|
365
|
+
</button>
|
|
366
|
+
{extraTab === tab.id && (
|
|
367
|
+
<div className="px-4 pb-4 pt-2">{tab.content}</div>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
))}
|
|
315
371
|
</div>
|
|
316
372
|
|
|
317
373
|
<ErrorMessage
|
|
@@ -323,7 +379,7 @@ export function CheckoutPaymentMethodList({
|
|
|
323
379
|
<div className="mt-5">{beforePaymentButton}</div>
|
|
324
380
|
)}
|
|
325
381
|
|
|
326
|
-
{buyButtonNotReady && buyButtonNotReadyReason && (
|
|
382
|
+
{!extraTab && buyButtonNotReady && buyButtonNotReadyReason && (
|
|
327
383
|
<div className="mt-4 flex items-center justify-center gap-1.5 text-muted-foreground">
|
|
328
384
|
<svg
|
|
329
385
|
viewBox="0 0 24 24"
|
|
@@ -344,20 +400,24 @@ export function CheckoutPaymentMethodList({
|
|
|
344
400
|
</div>
|
|
345
401
|
)}
|
|
346
402
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
403
|
+
{/* An active extra tab owns its own pay action (embedded
|
|
404
|
+
provider frames ship their own button), so ours hides. */}
|
|
405
|
+
{!extraTab && (
|
|
406
|
+
<div className="mt-5">
|
|
407
|
+
<PaymentButton
|
|
408
|
+
cart={cart}
|
|
409
|
+
paymentTab={paymentTab}
|
|
410
|
+
notReady={buyButtonNotReady}
|
|
411
|
+
performBuyClick={performBuyClick}
|
|
412
|
+
paymentElementComplete={
|
|
413
|
+
paymentTab === "cod" ? true : paymentElementComplete
|
|
414
|
+
}
|
|
415
|
+
total={total}
|
|
416
|
+
logError={logError}
|
|
417
|
+
data-testid="submit-order-button"
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
)}
|
|
361
421
|
</StripeElementsScope>
|
|
362
422
|
)}
|
|
363
423
|
</div>
|
|
@@ -10,11 +10,11 @@ import { StripeScopeProvider } from "./stripe-wrapper"
|
|
|
10
10
|
* PaymentWrapper — provides Stripe Elements scope context to descendants.
|
|
11
11
|
*
|
|
12
12
|
* Ported from `@1click/ui/src/checkout/payment-wrapper.tsx` (v2.3.1).
|
|
13
|
-
*
|
|
13
|
+
* Cartbase adaptations: the `cart` prop is the Cartbase `Cart` DTO (only
|
|
14
14
|
* `currency_code` is read — typed structurally so any decorated cart
|
|
15
|
-
* satisfies it);
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* satisfies it); `NEXT_PUBLIC_STRIPE_KEY` is the canonical env name
|
|
16
|
+
* (the Medusa-era fallback was dropped 2026-08-04, before any external
|
|
17
|
+
* consumer existed).
|
|
18
18
|
*
|
|
19
19
|
* As of the deferred-checkout architecture, this wrapper supplies an
|
|
20
20
|
* optimistic `amount` + `currency` (computed by the orchestration hook)
|
|
@@ -30,16 +30,13 @@ import { StripeScopeProvider } from "./stripe-wrapper"
|
|
|
30
30
|
* </PaymentWrapper>
|
|
31
31
|
*
|
|
32
32
|
* Environment variables (read at module load time):
|
|
33
|
-
* - NEXT_PUBLIC_STRIPE_KEY
|
|
34
|
-
*
|
|
35
|
-
* - NEXT_PUBLIC_MEDUSA_PAYMENTS_ACCOUNT_ID (optional, connect accounts)
|
|
33
|
+
* - NEXT_PUBLIC_STRIPE_KEY
|
|
34
|
+
* - NEXT_PUBLIC_STRIPE_ACCOUNT_ID (optional, connect accounts)
|
|
36
35
|
*/
|
|
37
36
|
|
|
38
|
-
const stripeKey =
|
|
39
|
-
process.env.NEXT_PUBLIC_STRIPE_KEY ||
|
|
40
|
-
process.env.NEXT_PUBLIC_MEDUSA_PAYMENTS_PUBLISHABLE_KEY
|
|
37
|
+
const stripeKey = process.env.NEXT_PUBLIC_STRIPE_KEY
|
|
41
38
|
|
|
42
|
-
const stripeAccountId = process.env.
|
|
39
|
+
const stripeAccountId = process.env.NEXT_PUBLIC_STRIPE_ACCOUNT_ID
|
|
43
40
|
|
|
44
41
|
const stripePromise = stripeKey
|
|
45
42
|
? loadStripe(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* checkout discount-code surface.
|
|
4
4
|
*
|
|
5
5
|
* Ported from `@1click/ui/src/checkout/promotion-error-copy.ts` (v2.3.1)
|
|
6
|
-
* and adapted to the
|
|
6
|
+
* and adapted to the Cartbase error envelope: the cart-promotions route
|
|
7
7
|
* (POST /api/store/carts/:id/promotions) throws structured codes —
|
|
8
8
|
* `promotion_not_found`, `promotion_inactive`, `promotion_misconfigured`,
|
|
9
9
|
* `promotion_unsupported_type`, plus the cart-level `cart_not_found` /
|
|
@@ -34,7 +34,7 @@ const EXHAUSTED =
|
|
|
34
34
|
"Промоцията е изчерпана и вече не може да бъде използвана."
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Cartbase promotion error codes → BG copy (POST/DELETE
|
|
38
38
|
* /api/store/carts/:id/promotions + src/lib/promotions/apply.ts).
|
|
39
39
|
*/
|
|
40
40
|
export const PROMOTION_ERROR_CODE_COPY: Record<string, string> = {
|
|
@@ -19,7 +19,7 @@ import { ErrorMessage } from "./error-message"
|
|
|
19
19
|
* matching picker expands inline inside that row.
|
|
20
20
|
*
|
|
21
21
|
* Ported from `@1click/ui/src/checkout/shipping-method-list.tsx` (v2.3.1)
|
|
22
|
-
* with the
|
|
22
|
+
* with the Cartbase data seam: options are `StoreShippingOption` rows from
|
|
23
23
|
* `checkout.listShippingOptions(client, {cart_id})` — rule-filtered and
|
|
24
24
|
* display-ordered SERVER-SIDE (checkout rules + `checkout_method_order`,
|
|
25
25
|
* docs/storefront/checkout.md); this component only renders what arrives.
|
|
@@ -183,7 +183,7 @@ export function CheckoutShippingMethodList({
|
|
|
183
183
|
// mid-courier-switch — the in-flight switch would otherwise
|
|
184
184
|
// leave a brief window where prices are calculated=true but
|
|
185
185
|
// the new option's price hasn't landed in the map yet,
|
|
186
|
-
// making it look broken. (
|
|
186
|
+
// making it look broken. (Cartbase serves flat prices today;
|
|
187
187
|
// the calculated branch is forward-compat —
|
|
188
188
|
// docs/storefront/checkout.md.)
|
|
189
189
|
const cantCalc =
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
*
|
|
19
19
|
* Ported from `@1click/ui/src/checkout/stripe-wrapper.tsx` (v2.3.1)
|
|
20
20
|
* unchanged — no data seam; the PaymentIntent seam moved server-side into
|
|
21
|
-
*
|
|
21
|
+
* Cartbase's POST /api/store/carts/:id/prepare-checkout.
|
|
22
22
|
*
|
|
23
23
|
* `<Elements>` is mounted with `mode: 'payment'`, `amount`, `currency` — NO
|
|
24
24
|
* PaymentIntent exists on the backend yet. The PI is minted at Buy click by
|