@cartbase/storefront 0.7.0 → 0.9.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 +4 -5
- package/package.json +248 -243
- package/src/api/carts.ts +16 -1
- package/src/api/http.ts +17 -9
- package/src/api/index.ts +1 -0
- package/src/api/store.ts +35 -0
- package/src/cart-drawer/cross-sell-carousel.tsx +2 -3
- package/src/cart-drawer/gift-wrap.tsx +82 -83
- package/src/cart-drawer/item/index.tsx +4 -6
- package/src/cart-drawer/sticky-footer.tsx +73 -73
- package/src/checkout/geocode.ts +1 -1
- package/src/checkout/gift-card-section.tsx +2 -2
- package/src/checkout/line-item-card.tsx +3 -3
- package/src/checkout/order-summary.tsx +11 -12
- package/src/checkout/payment-button.tsx +2 -3
- package/src/checkout/shipping-method-list.tsx +2 -2
- package/src/lib/cookie-names.ts +45 -0
- package/src/lib/money.ts +1 -1
- package/src/lib/platform.ts +13 -0
- package/src/lib/price.tsx +39 -0
- package/src/lib/visitor.ts +74 -0
- package/src/order/order-delivery-card.tsx +2 -2
- package/src/order/order-item.tsx +3 -3
- package/src/order/order-totals.tsx +4 -4
- package/src/tracking/attribution.ts +134 -105
- package/src/tracking/chatgpt-pixel.tsx +92 -0
- package/src/tracking/consent.ts +12 -0
- package/src/tracking/events.ts +56 -2
- package/src/tracking/get-tracking-attribution.ts +16 -3
- package/src/tracking/index.ts +23 -0
- package/src/tracking/live-heartbeat.tsx +63 -0
- package/src/tracking/oaiq.ts +206 -0
- package/src/tracking/page-views.tsx +96 -0
- package/src/tracking/storefront-tags.tsx +77 -67
- package/src/tracking/types.ts +4 -0
- package/src/lib/dual-price.tsx +0 -73
package/src/api/http.ts
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* Every SDK function in this package is a thin typed wrapper over
|
|
5
5
|
* `storeFetch()` from a `StorefrontClient`. Auth model (store-api.md):
|
|
6
6
|
*
|
|
7
|
-
* - `x-
|
|
8
|
-
*
|
|
9
|
-
* sales channels
|
|
7
|
+
* - `x-publishable-api-key` — THE store's key: names the store on its own
|
|
8
|
+
* (one key, 2026-09-07) and scopes catalog and
|
|
9
|
+
* carts to the key's sales channels
|
|
10
|
+
* - `x-client-id` — the platform's own door (hosted builds carry it);
|
|
11
|
+
* optional, wins over the key when both are sent
|
|
10
12
|
* - `authorization: Bearer <jwt>` — customer session (passwordless code
|
|
11
13
|
* flow or supabase password login)
|
|
12
14
|
* - `x-locale` — optional storefront locale hint
|
|
@@ -19,12 +21,16 @@
|
|
|
19
21
|
import { StoreApiError } from "./types"
|
|
20
22
|
|
|
21
23
|
export interface StorefrontClientConfig {
|
|
22
|
-
/** Deployment origin, e.g. `https://admin.
|
|
24
|
+
/** Deployment origin, e.g. `https://admin.cartbase.ai` */
|
|
23
25
|
baseUrl: string
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The store's publishable key: names the store and scopes the catalog to
|
|
28
|
+
* the key's sales channels. The one input a merchant hands out; at least
|
|
29
|
+
* one of `publishableKey` and `clientId` is required.
|
|
30
|
+
*/
|
|
27
31
|
publishableKey?: string
|
|
32
|
+
/** Tenant id — the platform's own door. Optional when a key is given. */
|
|
33
|
+
clientId?: string
|
|
28
34
|
/** Called per request; return the customer JWT or null for guests. */
|
|
29
35
|
getAuthToken?: () => string | null | Promise<string | null>
|
|
30
36
|
/** Called per request; return the active locale code or null. */
|
|
@@ -50,7 +56,9 @@ export interface RequestOptions {
|
|
|
50
56
|
export class StorefrontClient {
|
|
51
57
|
constructor(private readonly config: StorefrontClientConfig) {
|
|
52
58
|
if (!config.baseUrl) throw new Error("StorefrontClient: baseUrl is required")
|
|
53
|
-
if (!config.
|
|
59
|
+
if (!config.publishableKey && !config.clientId) {
|
|
60
|
+
throw new Error("StorefrontClient: publishableKey (or clientId) is required")
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/** The low-level typed request. Domain modules call this — apps rarely should. */
|
|
@@ -72,7 +80,7 @@ export class StorefrontClient {
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
const headers: Record<string, string> = {
|
|
75
|
-
"x-client-id": this.config.clientId,
|
|
83
|
+
...(this.config.clientId ? { "x-client-id": this.config.clientId } : {}),
|
|
76
84
|
...(this.config.publishableKey
|
|
77
85
|
? { "x-publishable-api-key": this.config.publishableKey }
|
|
78
86
|
: {}),
|
package/src/api/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * as products from "./products"
|
|
|
13
13
|
export * as collections from "./collections"
|
|
14
14
|
export * as categories from "./categories"
|
|
15
15
|
export * as regions from "./regions"
|
|
16
|
+
export * as store from "./store"
|
|
16
17
|
export * as carts from "./carts"
|
|
17
18
|
export * as giftCards from "./gift-cards"
|
|
18
19
|
export * as checkout from "./checkout"
|
package/src/api/store.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @cartbase/storefront/api/store — the store's identity.
|
|
3
|
+
*
|
|
4
|
+
* Name, slug and brand (the merchant's Settings → Brand): what a
|
|
5
|
+
* storefront needs to title, head and foot itself without hardcoding a
|
|
6
|
+
* name. One anonymous read at layout level. Docs: docs/storefront/store.md.
|
|
7
|
+
*/
|
|
8
|
+
import type { StorefrontClient } from "./http"
|
|
9
|
+
|
|
10
|
+
export interface StoreBrand {
|
|
11
|
+
/** Primary logo, public URL, or null. */
|
|
12
|
+
logo_url: string | null
|
|
13
|
+
/** Square mark, public URL, or null. */
|
|
14
|
+
logo_square_url: string | null
|
|
15
|
+
/** `#rrggbb` or null. */
|
|
16
|
+
color_primary: string | null
|
|
17
|
+
/** `#rrggbb` or null. */
|
|
18
|
+
color_secondary: string | null
|
|
19
|
+
slogan: string | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StoreIdentity {
|
|
23
|
+
/** The store's public id: what `<PlatformInit storeId>` mounts. */
|
|
24
|
+
id: string
|
|
25
|
+
/** The store's display name. */
|
|
26
|
+
name: string
|
|
27
|
+
/** The store's slug; its address is `{slug}.cartbase.net` until a custom domain. */
|
|
28
|
+
slug: string
|
|
29
|
+
brand: StoreBrand
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** GET /api/store/store — anon. */
|
|
33
|
+
export function getStore(client: StorefrontClient): Promise<{ store: StoreIdentity }> {
|
|
34
|
+
return client.request<{ store: StoreIdentity }>("/api/store/store")
|
|
35
|
+
}
|
|
@@ -4,7 +4,7 @@ import Image from "next/image"
|
|
|
4
4
|
import Link from "next/link"
|
|
5
5
|
import { useRef, useState } from "react"
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { Price } from "../lib/price"
|
|
8
8
|
import { cn } from "../lib/utils"
|
|
9
9
|
import { useCartDrawer } from "./context"
|
|
10
10
|
|
|
@@ -178,11 +178,10 @@ export function CartCrossSellCarousel({
|
|
|
178
178
|
>
|
|
179
179
|
{product.title}
|
|
180
180
|
</Link>
|
|
181
|
-
<
|
|
181
|
+
<Price
|
|
182
182
|
amount={product.price}
|
|
183
183
|
currencyCode={product.currencyCode}
|
|
184
184
|
className="text-[13px] font-bold text-foreground block mt-0.5"
|
|
185
|
-
bgnClassName="text-muted-foreground text-[10px] ml-1"
|
|
186
185
|
/>
|
|
187
186
|
</div>
|
|
188
187
|
|
|
@@ -1,83 +1,82 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useState } from "react"
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { cn } from "../lib/utils"
|
|
7
|
-
import { useCartDrawer } from "./context"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* CartGiftWrap — inline toggle row for adding gift wrapping to the cart.
|
|
11
|
-
*
|
|
12
|
-
* Ported from `@1click/ui/src/cart-drawer/gift-wrap.tsx` (v2.3.1);
|
|
13
|
-
* originally extracted from mindpages-storefront
|
|
14
|
-
* src/modules/cart-drawer/cart-gift-wrap.tsx. `price` is EUR major units.
|
|
15
|
-
* The store owns the effect: wire `onToggle` to add/remove the store's
|
|
16
|
-
* gift-wrap variant via the provider's `addItem` / `removeItem`.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
type CartGiftWrapProps = {
|
|
20
|
-
price: number
|
|
21
|
-
currencyCode: string
|
|
22
|
-
enabled?: boolean
|
|
23
|
-
onToggle?: (enabled: boolean) => void
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function CartGiftWrap({
|
|
27
|
-
price,
|
|
28
|
-
currencyCode,
|
|
29
|
-
enabled = false,
|
|
30
|
-
onToggle,
|
|
31
|
-
}: CartGiftWrapProps) {
|
|
32
|
-
const { labels } = useCartDrawer()
|
|
33
|
-
const [checked, setChecked] = useState(enabled)
|
|
34
|
-
|
|
35
|
-
const handleToggle = () => {
|
|
36
|
-
const next = !checked
|
|
37
|
-
setChecked(next)
|
|
38
|
-
onToggle?.(next)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="px-5 sm:px-6 py-2">
|
|
43
|
-
<button
|
|
44
|
-
type="button"
|
|
45
|
-
onClick={handleToggle}
|
|
46
|
-
className="flex items-center gap-3 w-full text-left py-2 min-h-[44px] group"
|
|
47
|
-
>
|
|
48
|
-
<div
|
|
49
|
-
className={cn(
|
|
50
|
-
"w-5 h-5 rounded border-[1.5px] flex items-center justify-center transition-all flex-shrink-0",
|
|
51
|
-
checked
|
|
52
|
-
? "bg-foreground border-text-base"
|
|
53
|
-
: "border-border group-hover:border-text-muted group-active:border-text-muted"
|
|
54
|
-
)}
|
|
55
|
-
>
|
|
56
|
-
{checked && (
|
|
57
|
-
<svg
|
|
58
|
-
width="10"
|
|
59
|
-
height="10"
|
|
60
|
-
viewBox="0 0 10 10"
|
|
61
|
-
fill="none"
|
|
62
|
-
stroke="white"
|
|
63
|
-
strokeWidth="2"
|
|
64
|
-
strokeLinecap="round"
|
|
65
|
-
strokeLinejoin="round"
|
|
66
|
-
>
|
|
67
|
-
<path d="M2 5l2 2 4-4" />
|
|
68
|
-
</svg>
|
|
69
|
-
)}
|
|
70
|
-
</div>
|
|
71
|
-
<span className="text-[13px] text-foreground flex-1">
|
|
72
|
-
{labels.addGiftWrap}
|
|
73
|
-
</span>
|
|
74
|
-
<
|
|
75
|
-
amount={price}
|
|
76
|
-
currencyCode={currencyCode}
|
|
77
|
-
className="text-[13px] text-muted-foreground font-medium"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
|
|
5
|
+
import { Price } from "../lib/price"
|
|
6
|
+
import { cn } from "../lib/utils"
|
|
7
|
+
import { useCartDrawer } from "./context"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CartGiftWrap — inline toggle row for adding gift wrapping to the cart.
|
|
11
|
+
*
|
|
12
|
+
* Ported from `@1click/ui/src/cart-drawer/gift-wrap.tsx` (v2.3.1);
|
|
13
|
+
* originally extracted from mindpages-storefront
|
|
14
|
+
* src/modules/cart-drawer/cart-gift-wrap.tsx. `price` is EUR major units.
|
|
15
|
+
* The store owns the effect: wire `onToggle` to add/remove the store's
|
|
16
|
+
* gift-wrap variant via the provider's `addItem` / `removeItem`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
type CartGiftWrapProps = {
|
|
20
|
+
price: number
|
|
21
|
+
currencyCode: string
|
|
22
|
+
enabled?: boolean
|
|
23
|
+
onToggle?: (enabled: boolean) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function CartGiftWrap({
|
|
27
|
+
price,
|
|
28
|
+
currencyCode,
|
|
29
|
+
enabled = false,
|
|
30
|
+
onToggle,
|
|
31
|
+
}: CartGiftWrapProps) {
|
|
32
|
+
const { labels } = useCartDrawer()
|
|
33
|
+
const [checked, setChecked] = useState(enabled)
|
|
34
|
+
|
|
35
|
+
const handleToggle = () => {
|
|
36
|
+
const next = !checked
|
|
37
|
+
setChecked(next)
|
|
38
|
+
onToggle?.(next)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="px-5 sm:px-6 py-2">
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={handleToggle}
|
|
46
|
+
className="flex items-center gap-3 w-full text-left py-2 min-h-[44px] group"
|
|
47
|
+
>
|
|
48
|
+
<div
|
|
49
|
+
className={cn(
|
|
50
|
+
"w-5 h-5 rounded border-[1.5px] flex items-center justify-center transition-all flex-shrink-0",
|
|
51
|
+
checked
|
|
52
|
+
? "bg-foreground border-text-base"
|
|
53
|
+
: "border-border group-hover:border-text-muted group-active:border-text-muted"
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{checked && (
|
|
57
|
+
<svg
|
|
58
|
+
width="10"
|
|
59
|
+
height="10"
|
|
60
|
+
viewBox="0 0 10 10"
|
|
61
|
+
fill="none"
|
|
62
|
+
stroke="white"
|
|
63
|
+
strokeWidth="2"
|
|
64
|
+
strokeLinecap="round"
|
|
65
|
+
strokeLinejoin="round"
|
|
66
|
+
>
|
|
67
|
+
<path d="M2 5l2 2 4-4" />
|
|
68
|
+
</svg>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
<span className="text-[13px] text-foreground flex-1">
|
|
72
|
+
{labels.addGiftWrap}
|
|
73
|
+
</span>
|
|
74
|
+
<Price
|
|
75
|
+
amount={price}
|
|
76
|
+
currencyCode={currencyCode}
|
|
77
|
+
className="text-[13px] text-muted-foreground font-medium"
|
|
78
|
+
/>
|
|
79
|
+
</button>
|
|
80
|
+
</div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -5,7 +5,7 @@ import Link from "next/link"
|
|
|
5
5
|
import { type ReactNode } from "react"
|
|
6
6
|
|
|
7
7
|
import type { CartLineItem } from "../../api/carts"
|
|
8
|
-
import {
|
|
8
|
+
import { Price } from "../../lib/price"
|
|
9
9
|
import { cn } from "../../lib/utils"
|
|
10
10
|
import { useCartDrawer } from "../context"
|
|
11
11
|
import { CartItemQuantity } from "./quantity"
|
|
@@ -13,7 +13,7 @@ import { CartItemVariant } from "./variant"
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* CartItem — a single cart line row: thumbnail, title, variant, quantity
|
|
16
|
-
* stepper, price (with
|
|
16
|
+
* stepper, price (with optional strikethrough), remove button.
|
|
17
17
|
*
|
|
18
18
|
* Ported from `@1click/ui/src/cart-drawer/item/index.tsx` (v2.3.1). Cartbase
|
|
19
19
|
* data seam: `item` is the SDK's decorated `CartLineItem`
|
|
@@ -111,22 +111,20 @@ export function CartItem({ item, currencyCode, children }: CartItemProps) {
|
|
|
111
111
|
<div className="text-right flex-shrink-0">
|
|
112
112
|
{hasDiscount && (
|
|
113
113
|
<span className="text-xs text-muted-foreground line-through block leading-none mb-0.5">
|
|
114
|
-
<
|
|
114
|
+
<Price
|
|
115
115
|
amount={originalTotal}
|
|
116
116
|
currencyCode={currencyCode}
|
|
117
117
|
className="text-xs text-muted-foreground"
|
|
118
|
-
bgnClassName="hidden"
|
|
119
118
|
/>
|
|
120
119
|
</span>
|
|
121
120
|
)}
|
|
122
|
-
<
|
|
121
|
+
<Price
|
|
123
122
|
amount={total}
|
|
124
123
|
currencyCode={currencyCode}
|
|
125
124
|
className={cn(
|
|
126
125
|
"text-sm font-bold",
|
|
127
126
|
hasDiscount ? "text-success" : "text-foreground"
|
|
128
127
|
)}
|
|
129
|
-
bgnClassName="text-muted-foreground text-[10px] ml-1"
|
|
130
128
|
/>
|
|
131
129
|
</div>
|
|
132
130
|
</div>
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import Link from "next/link"
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import { productTotal } from "../lib/cart-helpers"
|
|
7
|
-
import { useCartDrawer } from "./context"
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* CartStickyFooter — bottom-of-drawer subtotal + checkout CTA with
|
|
11
|
-
* iPhone home-indicator safe-area padding.
|
|
12
|
-
*
|
|
13
|
-
* Ported from `@1click/ui/src/cart-drawer/sticky-footer.tsx` (v2.3.1);
|
|
14
|
-
* originally extracted from mindpages-storefront
|
|
15
|
-
* src/modules/cart-drawer/cart-sticky-footer.tsx. Amounts EUR major units.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export function CartStickyFooter() {
|
|
19
|
-
const { cart, close, labels, hrefs } = useCartDrawer()
|
|
20
|
-
if (!cart || !cart.items?.length) return null
|
|
21
|
-
|
|
22
|
-
// Drawer is pre-checkout context — never read `cart.total` here.
|
|
23
|
-
// `cart.total` includes shipping/tax/COD fee, all of which are
|
|
24
|
-
// checkout-context state that must not leak into the shopping
|
|
25
|
-
// drawer. Sum of product line totals only.
|
|
26
|
-
const total = productTotal(cart.items)
|
|
27
|
-
const currencyCode = cart.currency_code
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div
|
|
31
|
-
className="flex-shrink-0 bg-card px-5 sm:px-6 pt-4 sm:pt-5 relative z-10"
|
|
32
|
-
style={{
|
|
33
|
-
boxShadow: "0 -4px 12px rgba(0,0,0,0.04)",
|
|
34
|
-
paddingBottom: "max(1.25rem, env(safe-area-inset-bottom, 1.25rem))",
|
|
35
|
-
}}
|
|
36
|
-
>
|
|
37
|
-
<div className="flex items-baseline justify-between mb-1">
|
|
38
|
-
<span className="text-[15px] font-bold text-foreground">
|
|
39
|
-
{labels.subtotal}
|
|
40
|
-
</span>
|
|
41
|
-
<
|
|
42
|
-
amount={total}
|
|
43
|
-
currencyCode={currencyCode}
|
|
44
|
-
className="text-xl font-bold text-foreground tracking-tight"
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
47
|
-
<p className="text-[11px] text-muted-foreground mb-3.5 sm:mb-4">
|
|
48
|
-
{labels.taxAndShipping}
|
|
49
|
-
</p>
|
|
50
|
-
<Link href={hrefs.checkout} onClick={close}>
|
|
51
|
-
<button
|
|
52
|
-
type="button"
|
|
53
|
-
className="w-full h-[52px] bg-foreground hover:bg-foreground/90 active:bg-foreground/95 text-card text-[15px] sm:text-sm font-semibold rounded-[2px] flex items-center justify-center gap-2.5 transition-colors active:scale-[0.98]"
|
|
54
|
-
>
|
|
55
|
-
<svg
|
|
56
|
-
width="16"
|
|
57
|
-
height="16"
|
|
58
|
-
viewBox="0 0 16 16"
|
|
59
|
-
fill="none"
|
|
60
|
-
stroke="currentColor"
|
|
61
|
-
strokeWidth="1.5"
|
|
62
|
-
strokeLinecap="round"
|
|
63
|
-
strokeLinejoin="round"
|
|
64
|
-
>
|
|
65
|
-
<rect x="3" y="6" width="10" height="8" rx="1.5" />
|
|
66
|
-
<path d="M5.5 6V4.5a2.5 2.5 0 015 0V6" />
|
|
67
|
-
</svg>
|
|
68
|
-
{labels.secureCheckout}
|
|
69
|
-
</button>
|
|
70
|
-
</Link>
|
|
71
|
-
</div>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Link from "next/link"
|
|
4
|
+
|
|
5
|
+
import { Price } from "../lib/price"
|
|
6
|
+
import { productTotal } from "../lib/cart-helpers"
|
|
7
|
+
import { useCartDrawer } from "./context"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* CartStickyFooter — bottom-of-drawer subtotal + checkout CTA with
|
|
11
|
+
* iPhone home-indicator safe-area padding.
|
|
12
|
+
*
|
|
13
|
+
* Ported from `@1click/ui/src/cart-drawer/sticky-footer.tsx` (v2.3.1);
|
|
14
|
+
* originally extracted from mindpages-storefront
|
|
15
|
+
* src/modules/cart-drawer/cart-sticky-footer.tsx. Amounts EUR major units.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export function CartStickyFooter() {
|
|
19
|
+
const { cart, close, labels, hrefs } = useCartDrawer()
|
|
20
|
+
if (!cart || !cart.items?.length) return null
|
|
21
|
+
|
|
22
|
+
// Drawer is pre-checkout context — never read `cart.total` here.
|
|
23
|
+
// `cart.total` includes shipping/tax/COD fee, all of which are
|
|
24
|
+
// checkout-context state that must not leak into the shopping
|
|
25
|
+
// drawer. Sum of product line totals only.
|
|
26
|
+
const total = productTotal(cart.items)
|
|
27
|
+
const currencyCode = cart.currency_code
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div
|
|
31
|
+
className="flex-shrink-0 bg-card px-5 sm:px-6 pt-4 sm:pt-5 relative z-10"
|
|
32
|
+
style={{
|
|
33
|
+
boxShadow: "0 -4px 12px rgba(0,0,0,0.04)",
|
|
34
|
+
paddingBottom: "max(1.25rem, env(safe-area-inset-bottom, 1.25rem))",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<div className="flex items-baseline justify-between mb-1">
|
|
38
|
+
<span className="text-[15px] font-bold text-foreground">
|
|
39
|
+
{labels.subtotal}
|
|
40
|
+
</span>
|
|
41
|
+
<Price
|
|
42
|
+
amount={total}
|
|
43
|
+
currencyCode={currencyCode}
|
|
44
|
+
className="text-xl font-bold text-foreground tracking-tight"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
<p className="text-[11px] text-muted-foreground mb-3.5 sm:mb-4">
|
|
48
|
+
{labels.taxAndShipping}
|
|
49
|
+
</p>
|
|
50
|
+
<Link href={hrefs.checkout} onClick={close}>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
className="w-full h-[52px] bg-foreground hover:bg-foreground/90 active:bg-foreground/95 text-card text-[15px] sm:text-sm font-semibold rounded-[2px] flex items-center justify-center gap-2.5 transition-colors active:scale-[0.98]"
|
|
54
|
+
>
|
|
55
|
+
<svg
|
|
56
|
+
width="16"
|
|
57
|
+
height="16"
|
|
58
|
+
viewBox="0 0 16 16"
|
|
59
|
+
fill="none"
|
|
60
|
+
stroke="currentColor"
|
|
61
|
+
strokeWidth="1.5"
|
|
62
|
+
strokeLinecap="round"
|
|
63
|
+
strokeLinejoin="round"
|
|
64
|
+
>
|
|
65
|
+
<rect x="3" y="6" width="10" height="8" rx="1.5" />
|
|
66
|
+
<path d="M5.5 6V4.5a2.5 2.5 0 015 0V6" />
|
|
67
|
+
</svg>
|
|
68
|
+
{labels.secureCheckout}
|
|
69
|
+
</button>
|
|
70
|
+
</Link>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
package/src/checkout/geocode.ts
CHANGED
|
@@ -141,7 +141,7 @@ export async function geocodeAddress(
|
|
|
141
141
|
url.searchParams.set("limit", "1")
|
|
142
142
|
|
|
143
143
|
const res = await fetch(url.toString(), {
|
|
144
|
-
headers: { "User-Agent": "
|
|
144
|
+
headers: { "User-Agent": "cartbase-storefront/1.0" },
|
|
145
145
|
})
|
|
146
146
|
const data = await res.json()
|
|
147
147
|
if (data?.[0]) {
|
|
@@ -5,7 +5,7 @@ import { useState } from "react"
|
|
|
5
5
|
import type { StorefrontClient } from "../api/http"
|
|
6
6
|
import type { Cart } from "../api/carts"
|
|
7
7
|
import { applyGiftCard, removeGiftCard } from "../api/gift-cards"
|
|
8
|
-
import {
|
|
8
|
+
import { Price } from "../lib/price"
|
|
9
9
|
import { cn } from "../lib/utils"
|
|
10
10
|
import { useCheckoutLabels } from "./context"
|
|
11
11
|
import { translateGiftCardError } from "./payment-error-copy"
|
|
@@ -201,7 +201,7 @@ export function GiftCardSection({
|
|
|
201
201
|
remainder didn't shrink. */}
|
|
202
202
|
<span className="text-sm font-medium text-success">
|
|
203
203
|
-
|
|
204
|
-
<
|
|
204
|
+
<Price
|
|
205
205
|
amount={gc.amount}
|
|
206
206
|
currencyCode={cart.currency_code}
|
|
207
207
|
/>
|
|
@@ -4,14 +4,14 @@ import { useState } from "react"
|
|
|
4
4
|
|
|
5
5
|
import type { StorefrontClient } from "../api/http"
|
|
6
6
|
import { updateLineItem, type Cart, type CartLineItem } from "../api/carts"
|
|
7
|
-
import {
|
|
7
|
+
import { Price } from "../lib/price"
|
|
8
8
|
import { cn } from "../lib/utils"
|
|
9
9
|
import { useCheckoutLabels } from "./context"
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* LineItemCard — a single cart line shown inside the checkout order
|
|
13
13
|
* summary card. Includes thumbnail, title, variant, quantity select,
|
|
14
|
-
* and
|
|
14
|
+
* and price.
|
|
15
15
|
*
|
|
16
16
|
* Ported from `@1click/ui/src/checkout/line-item-card.tsx` (v2.3.1) with
|
|
17
17
|
* the Cartbase data seam: the cookie-bound `updateLineItem` server action →
|
|
@@ -141,7 +141,7 @@ export function LineItemCard({
|
|
|
141
141
|
</div>
|
|
142
142
|
|
|
143
143
|
<div className="flex flex-col items-end justify-center flex-shrink-0">
|
|
144
|
-
<
|
|
144
|
+
<Price
|
|
145
145
|
amount={total}
|
|
146
146
|
currencyCode={currencyCode}
|
|
147
147
|
className="text-sm font-bold text-foreground"
|
|
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react"
|
|
|
4
4
|
|
|
5
5
|
import type { StorefrontClient } from "../api/http"
|
|
6
6
|
import { updateLineItem, type Cart, type CartLineItem } from "../api/carts"
|
|
7
|
-
import {
|
|
7
|
+
import { Price } from "../lib/price"
|
|
8
8
|
import { isProductLine } from "../lib/cart-helpers"
|
|
9
9
|
import { cn } from "../lib/utils"
|
|
10
10
|
import { useCheckoutLabels } from "./context"
|
|
@@ -170,7 +170,7 @@ export function CheckoutLineItem({
|
|
|
170
170
|
<div className="hidden sm:flex flex-1 min-w-0 flex-col gap-2.5">
|
|
171
171
|
<div className="flex items-baseline justify-between gap-3">
|
|
172
172
|
{titleBlock}
|
|
173
|
-
<
|
|
173
|
+
<Price
|
|
174
174
|
amount={item.total ?? 0}
|
|
175
175
|
currencyCode={currencyCode}
|
|
176
176
|
className="text-sm font-bold text-foreground text-right flex-shrink-0"
|
|
@@ -185,7 +185,7 @@ export function CheckoutLineItem({
|
|
|
185
185
|
{titleBlock}
|
|
186
186
|
<div className="flex items-center justify-between gap-3">
|
|
187
187
|
{qtyPill}
|
|
188
|
-
<
|
|
188
|
+
<Price
|
|
189
189
|
amount={item.total ?? 0}
|
|
190
190
|
currencyCode={currencyCode}
|
|
191
191
|
className="text-sm font-bold text-foreground text-right"
|
|
@@ -376,7 +376,7 @@ export function OrderSummary({
|
|
|
376
376
|
<div className="px-5 sm:px-6 space-y-3">
|
|
377
377
|
<div className="flex justify-between text-sm">
|
|
378
378
|
<span className="text-muted-foreground">{labels.subtotal}</span>
|
|
379
|
-
<
|
|
379
|
+
<Price
|
|
380
380
|
amount={productSubtotal}
|
|
381
381
|
currencyCode={cart.currency_code}
|
|
382
382
|
className="font-medium text-foreground"
|
|
@@ -391,7 +391,7 @@ export function OrderSummary({
|
|
|
391
391
|
{labels.shippingFree}
|
|
392
392
|
</span>
|
|
393
393
|
) : (
|
|
394
|
-
<
|
|
394
|
+
<Price
|
|
395
395
|
amount={shippingCost ?? 0}
|
|
396
396
|
currencyCode={cart.currency_code}
|
|
397
397
|
className="font-medium text-foreground"
|
|
@@ -409,7 +409,7 @@ export function OrderSummary({
|
|
|
409
409
|
<span className="text-muted-foreground">
|
|
410
410
|
{methodFeeLabel ?? cart.payment_method_fee_label ?? labels.paymentMethodFee}
|
|
411
411
|
</span>
|
|
412
|
-
<
|
|
412
|
+
<Price
|
|
413
413
|
amount={methodFeeAmount}
|
|
414
414
|
currencyCode={cart.currency_code}
|
|
415
415
|
className="font-medium text-foreground"
|
|
@@ -422,7 +422,7 @@ export function OrderSummary({
|
|
|
422
422
|
<span>{labels.discount}</span>
|
|
423
423
|
<span className="font-medium">
|
|
424
424
|
-
|
|
425
|
-
<
|
|
425
|
+
<Price
|
|
426
426
|
amount={cart.discount_total}
|
|
427
427
|
currencyCode={cart.currency_code}
|
|
428
428
|
/>
|
|
@@ -450,7 +450,7 @@ export function OrderSummary({
|
|
|
450
450
|
{labels.taxTooltip}
|
|
451
451
|
</span>
|
|
452
452
|
</span>
|
|
453
|
-
<
|
|
453
|
+
<Price
|
|
454
454
|
amount={cart.tax_total ?? 0}
|
|
455
455
|
currencyCode={cart.currency_code}
|
|
456
456
|
className="font-medium text-foreground"
|
|
@@ -464,11 +464,10 @@ export function OrderSummary({
|
|
|
464
464
|
{labels.total}
|
|
465
465
|
</span>
|
|
466
466
|
<div className="text-right">
|
|
467
|
-
<
|
|
467
|
+
<Price
|
|
468
468
|
amount={displayTotal ?? 0}
|
|
469
469
|
currencyCode={cart.currency_code}
|
|
470
470
|
className="text-2xl font-bold text-foreground tracking-tight"
|
|
471
|
-
bgnClassName="ml-2 text-sm text-muted-foreground/70 font-normal"
|
|
472
471
|
/>
|
|
473
472
|
</div>
|
|
474
473
|
</div>
|
|
@@ -482,7 +481,7 @@ export function OrderSummary({
|
|
|
482
481
|
<span>{labels.giftCardApplied}</span>
|
|
483
482
|
<span className="font-medium">
|
|
484
483
|
-
|
|
485
|
-
<
|
|
484
|
+
<Price
|
|
486
485
|
amount={giftCardTotal}
|
|
487
486
|
currencyCode={cart.currency_code}
|
|
488
487
|
/>
|
|
@@ -492,7 +491,7 @@ export function OrderSummary({
|
|
|
492
491
|
<span className="text-muted-foreground">
|
|
493
492
|
{labels.giftCardRemaining}
|
|
494
493
|
</span>
|
|
495
|
-
<
|
|
494
|
+
<Price
|
|
496
495
|
amount={giftCardRemainder}
|
|
497
496
|
currencyCode={cart.currency_code}
|
|
498
497
|
className="font-semibold text-foreground"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useElements, useStripe } from "@stripe/react-stripe-js"
|
|
4
4
|
import { useContext, useEffect, useRef, useState } from "react"
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Price } from "../lib/price"
|
|
7
7
|
import { cn } from "../lib/utils"
|
|
8
8
|
import { translatePaymentError } from "./payment-error-copy"
|
|
9
9
|
import { useCheckoutLabels } from "./context"
|
|
@@ -153,11 +153,10 @@ function OrderButton({
|
|
|
153
153
|
{total !== undefined && currencyCode && (
|
|
154
154
|
<span className="inline-flex items-center gap-1">
|
|
155
155
|
<span aria-hidden="true"> · </span>
|
|
156
|
-
<
|
|
156
|
+
<Price
|
|
157
157
|
amount={total}
|
|
158
158
|
currencyCode={currencyCode}
|
|
159
159
|
className="font-semibold"
|
|
160
|
-
bgnClassName="text-card/70 text-[11px] ml-1"
|
|
161
160
|
/>
|
|
162
161
|
</span>
|
|
163
162
|
)}
|