@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.
@@ -1,67 +1,77 @@
1
- import { GoogleTag } from "./ga4"
2
- import { Gtm } from "./gtm"
3
- import { MetaPixel } from "./meta-pixel"
4
- import { TikTokPixel } from "./tiktok-pixel"
5
- import { getTrackingConfig } from "./get-tracking-config"
6
- import type { StorefrontClient } from "../api/http"
7
- import type { TrackingConfig } from "./types"
8
-
9
- /**
10
- * StorefrontTags every marketing tag the store has configured, mounted
11
- * from the store's own config.
12
- *
13
- * THE POINT: a merchant who saves their pixel ids in the admin gets those
14
- * tags on their storefront, with nothing else to do and no code to write.
15
- * Before this existed the package EXPORTED tags and no storefront mounted
16
- * them, so the admin could serve a Meta pixel id, a GA4 id and a Google
17
- * Ads conversion id that reached the browser and did nothing. Adding a
18
- * vendor is one line HERE now, and every store built on the package gains
19
- * it at the next deploy.
20
- *
21
- * Consent: nothing extra to wire. <ConsentInit> sets the Consent Mode v2
22
- * defaults synchronously ahead of these, the Meta and TikTok snippets read
23
- * the shared consent cookie before they are allowed to write cookies, and
24
- * `applyConsent()` relays a live decision to all three vendors. Mount this
25
- * AFTER <ConsentInit> and the gate holds.
26
- *
27
- * Google is deliberately ONE tag with two destinations rather than two
28
- * loaders see the comment block in ./ga4.tsx. It is also why the Ads
29
- * conversion inherits the consent gate for free.
30
- *
31
- * Server component: pass a `client` and it fetches the config (cached),
32
- * or pass an already-fetched `config` when the layout has one in hand.
33
- * Renders nothing for a vendor the store has not configured, so a store
34
- * running only GA4 ships exactly one tag.
35
- */
36
- export async function StorefrontTags({
37
- client,
38
- config,
39
- }: {
40
- /** Used to fetch the tracking config when `config` is not supplied. */
41
- client?: StorefrontClient
42
- /** Pre-fetched tracking block, e.g. from a layout that already has it. */
43
- config?: TrackingConfig
44
- }) {
45
- let tracking: TrackingConfig | undefined = config
46
- if (!tracking) {
47
- if (!client) return null
48
- try {
49
- tracking = await getTrackingConfig(client)
50
- } catch {
51
- // A tracking-config failure must never take a storefront down.
52
- return null
53
- }
54
- }
55
-
56
- return (
57
- <>
58
- <MetaPixel pixelId={tracking.facebookPixel?.pixelId} />
59
- <TikTokPixel pixelId={tracking.tiktok?.pixelId} />
60
- <GoogleTag
61
- measurementId={tracking.ga4?.measurementId}
62
- adsConversionId={tracking.googleAds?.conversionId}
63
- />
64
- <Gtm containerId={tracking.gtm?.containerId} />
65
- </>
66
- )
67
- }
1
+ import { GoogleTag } from "./ga4"
2
+ import { LiveHeartbeat } from "./live-heartbeat"
3
+ import { PageViews } from "./page-views"
4
+ import { Gtm } from "./gtm"
5
+ import { MetaPixel } from "./meta-pixel"
6
+ import { TikTokPixel } from "./tiktok-pixel"
7
+ import { ChatGptPixel } from "./chatgpt-pixel"
8
+ import { getTrackingConfig } from "./get-tracking-config"
9
+ import type { StorefrontClient } from "../api/http"
10
+ import type { TrackingConfig } from "./types"
11
+
12
+ /**
13
+ * StorefrontTags every marketing tag the store has configured, mounted
14
+ * from the store's own config.
15
+ *
16
+ * THE POINT: a merchant who saves their pixel ids in the admin gets those
17
+ * tags on their storefront, with nothing else to do and no code to write.
18
+ * Before this existed the package EXPORTED tags and no storefront mounted
19
+ * them, so the admin could serve a Meta pixel id, a GA4 id and a Google
20
+ * Ads conversion id that reached the browser and did nothing. Adding a
21
+ * vendor is one line HERE now, and every store built on the package gains
22
+ * it at the next deploy.
23
+ *
24
+ * Consent: nothing extra to wire. <ConsentInit> sets the Consent Mode v2
25
+ * defaults synchronously ahead of these, the Meta and TikTok snippets read
26
+ * the shared consent cookie before they are allowed to write cookies, and
27
+ * `applyConsent()` relays a live decision to all three vendors. Mount this
28
+ * AFTER <ConsentInit> and the gate holds.
29
+ *
30
+ * Google is deliberately ONE tag with two destinations rather than two
31
+ * loaders see the comment block in ./ga4.tsx. It is also why the Ads
32
+ * conversion inherits the consent gate for free.
33
+ *
34
+ * Server component: pass a `client` and it fetches the config (cached),
35
+ * or pass an already-fetched `config` when the layout has one in hand.
36
+ * Renders nothing for a vendor the store has not configured, so a store
37
+ * running only GA4 ships exactly one tag.
38
+ */
39
+ export async function StorefrontTags({
40
+ client,
41
+ config,
42
+ }: {
43
+ /** Used to fetch the tracking config when `config` is not supplied. */
44
+ client?: StorefrontClient
45
+ /** Pre-fetched tracking block, e.g. from a layout that already has it. */
46
+ config?: TrackingConfig
47
+ }) {
48
+ let tracking: TrackingConfig | undefined = config
49
+ if (!tracking) {
50
+ if (!client) return null
51
+ try {
52
+ tracking = await getTrackingConfig(client)
53
+ } catch {
54
+ // A tracking-config failure must never take a storefront down.
55
+ return null
56
+ }
57
+ }
58
+
59
+ return (
60
+ <>
61
+ {/* Cartbase's own analytics, always on and never a merchant setting:
62
+ it is what feeds the merchant's own dashboard, so it does not
63
+ depend on any vendor id being configured. The vendor tags below
64
+ only appear when the merchant has entered their ids. */}
65
+ <PageViews />
66
+ <LiveHeartbeat />
67
+ <MetaPixel pixelId={tracking.facebookPixel?.pixelId} />
68
+ <TikTokPixel pixelId={tracking.tiktok?.pixelId} />
69
+ <ChatGptPixel pixelId={tracking.chatgptAds?.pixelId} />
70
+ <GoogleTag
71
+ measurementId={tracking.ga4?.measurementId}
72
+ adsConversionId={tracking.googleAds?.conversionId}
73
+ />
74
+ <Gtm containerId={tracking.gtm?.containerId} />
75
+ </>
76
+ )
77
+ }
@@ -44,6 +44,10 @@ export type TrackingConfig = {
44
44
  * read by the order.placed forwarder, exactly like Meta's CAPI token,
45
45
  * which has never traversed this surface. */
46
46
  tiktok?: { pixelId: string }
47
+ /** ChatGPT Ads, pixel id ONLY — the Conversions API key is a bearer
48
+ * token and stays on the platform, read by the order.placed forwarder
49
+ * (chatgpt-ads-pixel card). */
50
+ chatgptAds?: { pixelId: string }
47
51
  /** True when the store's consent CMP is enabled — client tags must
48
52
  * mount through the consent gate (`_1c_consent` / Consent Mode v2). */
49
53
  consent_required?: boolean
@@ -1,73 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
-
5
- import { cn } from "./utils"
6
- import { convertToLocale } from "./money"
7
-
8
- /**
9
- * DualPrice — render a EUR price with BGN next to it.
10
- *
11
- * Bulgarian law (Дв, effective through 2026-08) requires showing both EUR
12
- * and BGN on every price displayed to consumers. Fixed statutory rate:
13
- * 1 EUR = 1.95583 BGN
14
- *
15
- * For non-EUR currencies the BGN leg is skipped and only the primary locale
16
- * currency is shown. The component is stateless and side-effect-free — safe
17
- * to render in server components and client components alike.
18
- *
19
- * The EUR-to-BGN rate and the law's expiry date are baked in for now. When
20
- * the law expires (or when a store outside Bulgaria consumes this library),
21
- * we'll move both to a BrandingContext value so each store can override.
22
- *
23
- * @example
24
- * <DualPrice amount={19.99} currencyCode="eur" />
25
- * // → "€19.99 39,10 лв."
26
- */
27
-
28
- const EUR_TO_BGN = 1.95583
29
-
30
- function toBGN(eurAmount: number): string {
31
- const bgn = eurAmount * EUR_TO_BGN
32
- return bgn.toFixed(2).replace(".", ",") + " лв."
33
- }
34
-
35
- export type DualPriceProps = {
36
- /** Amount in major units of the primary currency (e.g. 19.99 for €19.99) */
37
- amount: number
38
- /** ISO currency code (case-insensitive). Only "eur" triggers the BGN leg. */
39
- currencyCode: string
40
- /** Optional class for the outer span */
41
- className?: string
42
- /** Optional class for the BGN leg (defaults to small muted) */
43
- bgnClassName?: string
44
- }
45
-
46
- export function DualPrice({
47
- amount,
48
- currencyCode,
49
- className,
50
- bgnClassName,
51
- }: DualPriceProps): React.ReactElement {
52
- const isEur = currencyCode?.toLowerCase() === "eur"
53
-
54
- if (!isEur) {
55
- return (
56
- <span className={className}>
57
- {convertToLocale({ amount, currency_code: currencyCode })}
58
- </span>
59
- )
60
- }
61
-
62
- return (
63
- <span className={className}>
64
- {convertToLocale({ amount, currency_code: currencyCode })}
65
- <span className={cn("ml-1.5 text-muted-foreground/70 font-normal", bgnClassName)}>
66
- {toBGN(amount)}
67
- </span>
68
- </span>
69
- )
70
- }
71
-
72
- /** Exposed for advanced callers that need the rate directly. */
73
- export const EUR_TO_BGN_RATE = EUR_TO_BGN