@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.
Files changed (106) hide show
  1. package/README.md +3 -3
  2. package/package.json +231 -226
  3. package/src/api/auth.ts +1 -1
  4. package/src/api/carts.ts +5 -3
  5. package/src/api/checkout.ts +91 -40
  6. package/src/api/consent.ts +2 -2
  7. package/src/api/integrations.ts +4 -17
  8. package/src/api/regions.ts +1 -1
  9. package/src/cart-drawer/context.tsx +4 -4
  10. package/src/cart-drawer/cross-sell-carousel.tsx +1 -1
  11. package/src/cart-drawer/cross-sell-sidebar.tsx +1 -1
  12. package/src/cart-drawer/header.tsx +1 -1
  13. package/src/cart-drawer/item/index.tsx +2 -2
  14. package/src/cart-drawer/item/quantity.tsx +2 -2
  15. package/src/cart-drawer/item/upsell.tsx +3 -3
  16. package/src/cart-drawer/item/variant.tsx +2 -2
  17. package/src/cart-drawer/labels-bg.ts +2 -2
  18. package/src/cart-drawer/labels.ts +119 -119
  19. package/src/cart-drawer/summary-breakdown.tsx +197 -196
  20. package/src/cart-drawer/template.tsx +2 -2
  21. package/src/cart-drawer/tiered-progress.tsx +1 -1
  22. package/src/checkout/address-error-copy.ts +3 -3
  23. package/src/checkout/address-form.tsx +2 -2
  24. package/src/checkout/address-select.tsx +1 -1
  25. package/src/checkout/boxnow-locker-selector.tsx +3 -3
  26. package/src/checkout/checkout-client.tsx +15 -12
  27. package/src/checkout/company-details.tsx +1 -1
  28. package/src/checkout/compare-addresses.ts +1 -1
  29. package/src/checkout/discount-section.tsx +5 -5
  30. package/src/checkout/econt-office-selector.tsx +2 -2
  31. package/src/checkout/gift-card-section.tsx +2 -2
  32. package/src/checkout/index.ts +0 -1
  33. package/src/checkout/labels-bg.ts +2 -2
  34. package/src/checkout/labels.ts +263 -263
  35. package/src/checkout/line-item-card.tsx +2 -2
  36. package/src/checkout/order-summary.tsx +48 -48
  37. package/src/checkout/payment-button.tsx +4 -4
  38. package/src/checkout/payment-error-copy.ts +6 -6
  39. package/src/checkout/payment-method-list.tsx +93 -33
  40. package/src/checkout/payment-wrapper.tsx +8 -11
  41. package/src/checkout/promotion-error-copy.ts +2 -2
  42. package/src/checkout/shipping-method-list.tsx +2 -2
  43. package/src/checkout/stripe-wrapper.tsx +1 -1
  44. package/src/checkout/use-checkout-orchestration.ts +108 -98
  45. package/src/common/cart-button-client.tsx +1 -1
  46. package/src/common/cart-button.tsx +1 -1
  47. package/src/common/country-select.tsx +2 -2
  48. package/src/common/delete-button.tsx +1 -1
  49. package/src/common/index.ts +1 -1
  50. package/src/common/language-select.tsx +2 -2
  51. package/src/common/localized-link.tsx +1 -1
  52. package/src/lib/cart-helpers.ts +2 -2
  53. package/src/lib/cookie-names.ts +41 -0
  54. package/src/lib/get-product-price.ts +3 -3
  55. package/src/lib/payment-constants.ts +53 -66
  56. package/src/lib/store-api-error.ts +1 -1
  57. package/src/order/index.ts +1 -1
  58. package/src/order/labels-bg.ts +39 -39
  59. package/src/order/labels.ts +79 -79
  60. package/src/order/order-address-card.tsx +1 -1
  61. package/src/order/order-completed-template.tsx +20 -12
  62. package/src/order/order-confirmation-header.tsx +1 -1
  63. package/src/order/order-delivery-card.tsx +3 -3
  64. package/src/order/order-item.tsx +2 -2
  65. package/src/order/order-items-list.tsx +3 -3
  66. package/src/order/order-payment-card.tsx +27 -36
  67. package/src/order/order-timeline.tsx +2 -2
  68. package/src/order/order-totals.tsx +250 -245
  69. package/src/platform/identity.ts +18 -0
  70. package/src/platform/index.ts +8 -0
  71. package/src/platform/metadata.ts +26 -0
  72. package/src/platform/platform-init.tsx +46 -0
  73. package/src/products/image-gallery.tsx +1 -1
  74. package/src/products/mobile-actions.tsx +1 -1
  75. package/src/products/option-select.tsx +1 -1
  76. package/src/products/preview-price.tsx +1 -1
  77. package/src/products/product-actions.tsx +6 -6
  78. package/src/products/product-info.tsx +1 -1
  79. package/src/products/product-preview.tsx +1 -1
  80. package/src/products/product-price.tsx +2 -2
  81. package/src/products/product-tabs.tsx +1 -1
  82. package/src/products/purchase-options.tsx +1 -1
  83. package/src/products/related-products.tsx +3 -3
  84. package/src/products/variant-matching.ts +4 -4
  85. package/src/reviews-ui/labels.ts +1 -1
  86. package/src/reviews-ui/photo-upload.tsx +1 -1
  87. package/src/reviews-ui/review-list.tsx +1 -1
  88. package/src/reviews-ui/review-widget.tsx +1 -1
  89. package/src/reviews-ui/review-wizard.tsx +1 -1
  90. package/src/reviews-ui/wizard-state.ts +1 -1
  91. package/src/store/category-template.tsx +2 -2
  92. package/src/store/collection-template.tsx +2 -2
  93. package/src/store/labels.ts +2 -2
  94. package/src/store/paginated-products.tsx +1 -1
  95. package/src/store/search-params.ts +1 -1
  96. package/src/store/search-template.tsx +1 -1
  97. package/src/tracking/consent-banner.tsx +1 -1
  98. package/src/tracking/consent-init.tsx +1 -1
  99. package/src/tracking/consent.ts +6 -6
  100. package/src/tracking/fbq.ts +2 -2
  101. package/src/tracking/get-tracking-attribution.ts +1 -1
  102. package/src/tracking/get-tracking-config.ts +2 -2
  103. package/src/tracking/index.ts +2 -2
  104. package/src/tracking/meta-pixel.tsx +1 -1
  105. package/src/tracking/rybbit.tsx +1 -1
  106. 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 barter data seam:
20
+ * the Cartbase data seam:
21
21
  *
22
- * - COD fee is CART-LEVEL decoration on the barter wire —
23
- * `cart.cod_fee_total` / `cart.cod_fee_label`, folded into `cart.total`
24
- * while a live pp_cod session exists (docs/storefront/checkout.md) —
25
- * NOT a metadata-flagged fee line item. The fee row + optimistic math
26
- * read those fields; `isProductLine` is kept as a defensive filter for
27
- * any legacy fee-line carts.
28
- * - Gift-card tender rows (barter feature): `gift_card_total` renders as
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 COD-fee prediction. Three values:
214
+ * Optimistic method-fee prediction. Three values:
215
215
  * - null → no prediction; render whatever the cart says.
216
- * - 0 → predict no fee (toggling away from COD).
216
+ * - 0 → predict no fee (toggling to a fee-less tender).
217
217
  * - positive → predict the fee at this amount.
218
- * Pass `useCheckoutOrchestration`'s `optimisticCodFee`. Needed because
219
- * the deferred-checkout architecture defers the pp_cod session (and so
220
- * the native fee) until Buy click.
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
- optimisticCodFee?: number | null
223
- onOptimisticCodFeeClear?: () => void
222
+ optimisticMethodFee?: number | null
223
+ onOptimisticMethodFeeClear?: () => void
224
224
  /**
225
- * Override label for the cash-on-delivery fee row. Optional — falls
226
- * back to the cart's server-provided `cod_fee_label` (admin-configured
227
- * on the COD integration), then to `labels.codFee`.
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
- codFeeLabel?: string
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
- optimisticCodFee,
250
- onOptimisticCodFeeClear,
251
- codFeeLabel,
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. Barter's COD fee is cart-level decoration (never a
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 COD fee: optimistic prediction wins until the cart catches
272
- // up. Three states (null / 0 / positive). Barter server truth is
273
- // `cart.cod_fee_total` (folded into cart.total while a pp_cod session
274
- // lives — docs/storefront/checkout.md).
275
- const realCodFeeAmount = cart.cod_fee_total ?? 0
276
- const effectiveCodFee =
277
- optimisticCodFee !== null && optimisticCodFee !== undefined
278
- ? optimisticCodFee
279
- : realCodFeeAmount
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 (optimisticCodFee !== null && optimisticCodFee !== undefined) {
287
- total = total - realCodFeeAmount + effectiveCodFee
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
- optimisticCodFee,
295
- effectiveCodFee,
296
- realCodFeeAmount,
294
+ optimisticMethodFee,
295
+ effectiveMethodFee,
296
+ realMethodFeeAmount,
297
297
  ])
298
298
 
299
- // Subtotal: barter's `item_total` is products-only (the COD fee is not a
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 codFeeAmount = effectiveCodFee
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 COD fee once the cart's decoration matches the
321
- // prediction (barter: cod_fee_total, not a fee line item).
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 (optimisticCodFee === null || optimisticCodFee === undefined) return
324
- const synced = realCodFeeAmount === optimisticCodFee
325
- if (synced) onOptimisticCodFeeClear?.()
326
- }, [optimisticCodFee, realCodFeeAmount, onOptimisticCodFeeClear])
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
- {codFeeAmount > 0 && (
407
+ {methodFeeAmount > 0 && (
408
408
  <div className="flex justify-between text-sm">
409
409
  <span className="text-muted-foreground">
410
- {codFeeLabel ?? cart.cod_fee_label ?? labels.codFee}
410
+ {methodFeeLabel ?? cart.payment_method_fee_label ?? labels.paymentMethodFee}
411
411
  </span>
412
412
  <DualPrice
413
- amount={codFeeAmount}
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 barter data seam: the `cart` prop is structural (only `id`, `total`,
22
- * `currency_code` are read — any decorated barter cart satisfies it), and
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 (barter has no store-side log endpoint; every production log
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 barter default path navigates via
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 barter error envelope: the store API throws `StoreApiError`
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 barter code) and any legacy wire.
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
- * Barter store-API error codes → BG copy. Covers EVERY code documented for
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 barter `code`, so this layer still does
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 barter error code out of any thrown value. */
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 (barter `StoreApiError.code`), then Stripe substring patterns,
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 barter data seam: the `cart` prop is structural, and 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. Provider semantics on the barter wire: the "card" tab is
25
- * `pp_stripe`, the "cod" tab is `pp_cod` (native fee via the integrations
26
- * `cod` block) or `pp_manual` (offline, no fee) the orchestration hook
27
- * resolves the ids (`hasCard`/`hasCod`/`cardId`/`codId`); both listings
28
- * arrive rule-filtered server-side (`checkout.listPaymentProviders`,
29
- * docs/storefront/checkout.md). COD stays a separate rail — it's a plain
30
- * provider session, not a Stripe payment method.
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={() => onPaymentTab("card")}
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={() => onPaymentTab("cod")}
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
- <div className="mt-5">
348
- <PaymentButton
349
- cart={cart}
350
- paymentTab={paymentTab}
351
- notReady={buyButtonNotReady}
352
- performBuyClick={performBuyClick}
353
- paymentElementComplete={
354
- paymentTab === "cod" ? true : paymentElementComplete
355
- }
356
- total={total}
357
- logError={logError}
358
- data-testid="submit-order-button"
359
- />
360
- </div>
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
- * Barter adaptations: the `cart` prop is the barter `Cart` DTO (only
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); env var naming gains `NEXT_PUBLIC_STRIPE_KEY` as the
16
- * canonical name, with the Medusa-era fallbacks kept so ported storefronts
17
- * keep working unchanged.
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 OR
34
- * NEXT_PUBLIC_MEDUSA_PAYMENTS_PUBLISHABLE_KEY (legacy fallback)
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.NEXT_PUBLIC_MEDUSA_PAYMENTS_ACCOUNT_ID
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 barter error envelope: the cart-promotions route
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
- * Barter promotion error codes → BG copy (POST/DELETE
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 barter data seam: options are `StoreShippingOption` rows from
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. (Barter serves flat prices today;
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
- * barter's POST /api/store/carts/:id/prepare-checkout.
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