@amboras-dev/snapchat-ads 0.1.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/dist/chunk-IP2VTHD6.mjs +27 -0
- package/dist/chunk-IP2VTHD6.mjs.map +1 -0
- package/dist/get-config-Dr_GV8ip.d.mts +93 -0
- package/dist/get-config-Dr_GV8ip.d.ts +93 -0
- package/dist/get-config.d.mts +1 -0
- package/dist/get-config.d.ts +1 -0
- package/dist/get-config.js +51 -0
- package/dist/get-config.js.map +1 -0
- package/dist/get-config.mjs +7 -0
- package/dist/get-config.mjs.map +1 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +251 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +194 -0
- package/dist/index.mjs.map +1 -0
- package/dist/slot-components/index.d.mts +117 -0
- package/dist/slot-components/index.d.ts +117 -0
- package/dist/slot-components/index.js +498 -0
- package/dist/slot-components/index.js.map +1 -0
- package/dist/slot-components/index.mjs +468 -0
- package/dist/slot-components/index.mjs.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/constants/index.ts","../src/lib/snaptr.ts","../src/lib/event-id.ts","../src/lib/cookies.ts","../src/get-config.ts","../src/lib/runtime-config.ts","../src/hooks/useSnapchatEvents.ts"],"sourcesContent":["export {\n trackPageView,\n trackViewContent,\n trackAddCart,\n trackStartCheckout,\n trackPurchase,\n sendSnapchatCapiEvent,\n} from './hooks/useSnapchatEvents'\n\nexport { getSnapchatConfig } from './get-config'\n\nexport { makeEventId, makeUniqueEventId } from './lib/event-id'\n\nexport { SNAP_EVENTS, COOKIE_NAMES, CAPI_RELAY_ENDPOINT } from './constants'\n\n// The Conversions API token never leaves the Medusa side, so no\n// secret-bearing config type is exported. Storefront code consumes\n// `SnapchatPublicConfig` only.\nexport type {\n SnapchatPublicConfig,\n SnapchatCapiEventPayload,\n SnapchatEventName,\n} from './types'\n","/**\n * Every fixed string the package puts on the wire or in browser storage.\n * Wire names are a cross-repo contract: Medusa's strict Zod schema and the\n * orchestrator's storefront relay template expect exactly these values.\n */\n\n/**\n * Snap standard event names. The pixel (`snaptr('track', <name>)`) and\n * Conversions API v3 (`event_name`) use the SAME uppercase strings.\n * https://developers.snap.com/api/marketing-api/Conversions-API/Parameters\n */\nexport const SNAP_EVENTS = {\n PAGE_VIEW: 'PAGE_VIEW',\n VIEW_CONTENT: 'VIEW_CONTENT',\n ADD_CART: 'ADD_CART',\n START_CHECKOUT: 'START_CHECKOUT',\n PURCHASE: 'PURCHASE',\n} as const\n\n/**\n * Merchant setting (from `/store/snapchat-ads/config`) that switches each\n * event off. Medusa enforces the same map server-side before enqueueing.\n */\nexport const EVENT_TOGGLES = {\n PAGE_VIEW: 'track_page_view',\n VIEW_CONTENT: 'track_view_content',\n ADD_CART: 'track_add_cart',\n START_CHECKOUT: 'track_start_checkout',\n PURCHASE: 'track_purchase',\n} as const satisfies Record<keyof typeof SNAP_EVENTS, string>\n\n/** How long the browser reuses the merchant config before re-reading it. */\nexport const CONFIG_REFRESH_MS = 60_000\n\n/** Storefront relay route written by the orchestrator's template. */\nexport const CAPI_RELAY_ENDPOINT = '/api/snapchat/events'\n\n/** Snap's official pixel script. */\nexport const SNAP_SCRIPT_SRC = 'https://sc-static.net/scevent.min.js'\n\n/** URL param Snap appends to ad landing URLs (forwarded as `sc_click_id`). */\nexport const CLICK_ID_QUERY_PARAM = 'ScCid'\n\nexport const COOKIE_NAMES = {\n /** Plugin-owned raw PII for the relay; Medusa hashes it. */\n PII: 'amboras_snap_pii',\n /** Plugin-owned copy of `ScCid`. */\n CLICK_ID: 'amboras_snap_clid',\n /** Snap's own first-party cookie (forwarded as `sc_cookie1`). */\n SCID: '_scid',\n} as const\n\nexport const COOKIE_MAX_AGE_SECONDS = {\n PII: 30 * 24 * 60 * 60,\n // Matches Snap's default 28-day click attribution window.\n CLICK_ID: 28 * 24 * 60 * 60,\n} as const\n\n/** sessionStorage / globalThis keys the slot components use for dedup. */\nexport const STORAGE_KEYS = {\n CART_STATE: 'amboras_snap_cart_state_v1',\n CART_RECENT_FIRES: '__amboras_snap_recent_fires__',\n CHECKOUT_SEEN: 'amboras_snap_checkout_seen',\n PURCHASE_PREFIX: 'amboras_snap_purchase_',\n} as const\n\n/** Storefront window events the provider listens to. */\nexport const WINDOW_EVENTS = {\n CONSENT_CHANGED: 'amboras-consent-changed',\n AUTH_CHANGED: 'amboras-auth-changed',\n} as const\n","/**\n * Typed wrapper around Snap's global `snaptr`. The shim is installed by\n * `SnapPixelProvider` behind consent - every other slot component calls\n * `safeSnaptr()`, which is a no-op when snaptr is absent (consent denied,\n * plugin uninstalled, or SSR).\n */\n\nimport { SNAP_SCRIPT_SRC } from '../constants'\n\ndeclare global {\n interface Window {\n snaptr?: SnaptrFunction\n }\n}\n\nexport type SnaptrFunction = (...args: unknown[]) => void\n\n/** Best-effort synchronous call. Returns whether snaptr was present. */\nexport function safeSnaptr(...args: unknown[]): boolean {\n if (typeof window === 'undefined') return false\n const fn = window.snaptr\n if (typeof fn !== 'function') return false\n try {\n fn(...args)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Install the snaptr queue shim + inject `scevent.min.js`. Idempotent.\n * Mirrors the shape of Snap's official install snippet\n * (https://developers.snap.com/api/marketing-api/Ads-API/snap-pixel) so the\n * loaded script drains the queue via `handleRequest`.\n */\nexport function installSnaptrShim(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n if (window.snaptr) return\n\n const shim = function (...args: unknown[]): void {\n const self = shim as unknown as { handleRequest?: (...a: unknown[]) => void; queue: unknown[] }\n if (self.handleRequest) self.handleRequest(...args)\n else self.queue.push(args)\n } as SnaptrFunction\n ;(shim as unknown as { queue: unknown[] }).queue = []\n window.snaptr = shim\n\n const script = document.createElement('script')\n script.async = true\n script.src = SNAP_SCRIPT_SRC\n document.head.appendChild(script)\n}\n","import type { SnapchatEventName } from '../types'\n\n/**\n * Event-ID builders for browser <-> CAPI dedup. Snap matches the pixel's\n * `client_dedup_id` against the CAPI `event_id` within 48h, so the browser\n * call and the CAPI relay for one user action must carry the same ID. Both\n * are fired from the same function call, so the ID only has to be stable\n * within that call - not across calls.\n *\n * Two flavours:\n * - `makeEventId`: deterministic. Used where a repeat fire of the SAME\n * action must collapse into one (START_CHECKOUT per cart, PURCHASE per\n * order), e.g. a refresh on the thank-you page.\n * - `makeUniqueEventId`: deterministic prefix + random suffix. Used for\n * actions a shopper legitimately repeats (viewing a product again,\n * adding the same variant twice, revisiting a page). A pure\n * deterministic ID would make Snap drop every repeat within 48h.\n */\nexport function makeEventId(eventName: SnapchatEventName, primaryKey: string): string {\n const safeKey = primaryKey.replace(/[^a-zA-Z0-9_-]/g, '_')\n return `${eventName.toLowerCase()}_${safeKey}`\n}\n\nfunction randomSuffix(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto\n if (c?.randomUUID) return c.randomUUID().replace(/-/g, '').slice(0, 16)\n return Math.random().toString(36).slice(2, 12) + Date.now().toString(36)\n}\n\nexport function makeUniqueEventId(eventName: SnapchatEventName, primaryKey: string): string {\n return `${makeEventId(eventName, primaryKey)}_${randomSuffix()}`\n}\n","/**\n * Snap-specific cookies, on top of `@amboras-dev/pixel-core`'s cookie helpers:\n * - `_scid`: Snap's first-party cookie set by the pixel. Forwarded raw to\n * CAPI as `sc_cookie1`.\n * - `amboras_snap_clid`: plugin-owned copy of the `ScCid` URL param Snap\n * appends to ad landing URLs. Forwarded raw to CAPI as `sc_click_id`.\n * Captured on landing because a later SPA navigation drops the param.\n * - `amboras_snap_pii`: plugin-owned raw (unhashed) customer PII for the\n * storefront's `/api/snapchat/events` relay; Medusa normalizes + hashes\n * it before shipping to Snap. Separate from the meta/pinterest PII\n * cookies so the plugins stay decoupled.\n */\n\nimport { deleteCookie, readCookie, writeCookie, writePiiCookie, type PiiPayload } from '@amboras-dev/pixel-core'\nimport { CLICK_ID_QUERY_PARAM, COOKIE_MAX_AGE_SECONDS, COOKIE_NAMES } from '../constants'\n\nexport function readScid(): string | undefined {\n return readCookie(COOKIE_NAMES.SCID)\n}\n\nexport function readClickId(): string | undefined {\n return readCookie(COOKIE_NAMES.CLICK_ID)\n}\n\n/**\n * Store `?ScCid=` from the landing URL. A newer click id replaces an older\n * one so the most recent ad click wins attribution.\n */\nexport function captureClickIdFromQuery(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n let clickId: string | null = null\n try {\n clickId = new URL(window.location.href).searchParams.get(CLICK_ID_QUERY_PARAM)\n } catch {\n return\n }\n if (!clickId || clickId === readClickId()) return\n writeCookie(COOKIE_NAMES.CLICK_ID, clickId, COOKIE_MAX_AGE_SECONDS.CLICK_ID)\n}\n\nexport function writeSnapchatPiiCookie(payload: PiiPayload): void {\n writePiiCookie(COOKIE_NAMES.PII, payload, COOKIE_MAX_AGE_SECONDS.PII)\n}\n\nexport function clearSnapchatPiiCookie(): void {\n deleteCookie(COOKIE_NAMES.PII)\n}\n","import { resolveBackendUrl, resolvePublishableKey, resolveStoreEnvironmentId } from '@amboras-dev/pixel-core'\nimport type { SnapchatPublicConfig } from './types'\n\ninterface GetSnapchatConfigOptions {\n baseUrl?: string\n publishableApiKey?: string\n storeId?: string\n /** Injected `fetch` for testing. Defaults to global `fetch`. */\n fetchImpl?: typeof fetch\n}\n\n/**\n * Fetches the non-secret Snapchat config from Medusa. Returns `null` when\n * the backend URL is unknown or the endpoint is missing, unauthorized, or\n * errors. The Conversions API token is never returned by the server route,\n * so it never surfaces here.\n */\nexport async function getSnapchatConfig(\n options: GetSnapchatConfigOptions = {},\n): Promise<SnapchatPublicConfig | null> {\n const baseUrl = options.baseUrl ?? resolveBackendUrl()\n if (!baseUrl) return null\n const publishableKey = options.publishableApiKey ?? resolvePublishableKey()\n const storeId = options.storeId ?? resolveStoreEnvironmentId()\n const doFetch = options.fetchImpl ?? fetch\n\n try {\n const res = await doFetch(`${baseUrl}/store/snapchat-ads/config`, {\n headers: {\n ...(storeId ? { 'X-Store-Environment-ID': storeId } : {}),\n ...(publishableKey ? { 'x-publishable-api-key': publishableKey } : {}),\n },\n next: { revalidate: 60 },\n } as RequestInit)\n if (!res.ok) return null\n return (await res.json()) as SnapchatPublicConfig\n } catch {\n return null\n }\n}\n","/**\n * The merchant's live Snapchat settings, read once per page (refreshed\n * every CONFIG_REFRESH_MS) and shared by the provider and every event.\n *\n * Fail-closed: no config (network error, plugin disconnected, Medusa answers\n * `{ enabled: false }`) means no pixel and no events. The browser then\n * honours \"Plugin enabled\" and each \"Track ...\" toggle the same way Medusa's\n * events route does, and a Pixel ID change applies without a redeploy.\n */\n\nimport { getSnapchatConfig } from '../get-config'\nimport { CONFIG_REFRESH_MS, EVENT_TOGGLES } from '../constants'\nimport type { SnapchatEventName, SnapchatPublicConfig } from '../types'\n\nlet cached: { at: number; read: Promise<SnapchatPublicConfig | null> } | null = null\n\nexport function loadSnapchatConfig(): Promise<SnapchatPublicConfig | null> {\n const now = Date.now()\n if (cached && now - cached.at < CONFIG_REFRESH_MS) return cached.read\n const read = getSnapchatConfig().then((config) => {\n // A failed read is not reused, so the next event tries again.\n if (!config) cached = null\n return config\n })\n cached = { at: now, read }\n return read\n}\n\n/** The pixel ID to load, or null when the plugin is off / not configured. */\nexport function activePixelId(config: SnapchatPublicConfig | null): string | null {\n if (!config || config.enabled !== true) return null\n return config.pixel_id || null\n}\n\nexport function isEventEnabled(config: SnapchatPublicConfig | null, eventName: SnapchatEventName): boolean {\n if (!activePixelId(config)) return false\n // A toggle the config omits counts as on, matching Medusa's default.\n return config![EVENT_TOGGLES[eventName]] !== false\n}\n\n/** Test-only: forget the cached config. */\nexport function resetSnapchatConfigCache(): void {\n cached = null\n}\n","import { safeSnaptr } from '../lib/snaptr'\nimport { makeEventId, makeUniqueEventId } from '../lib/event-id'\nimport { readClickId, readScid } from '../lib/cookies'\nimport { CAPI_RELAY_ENDPOINT, SNAP_EVENTS } from '../constants'\nimport { isEventEnabled, loadSnapchatConfig } from '../lib/runtime-config'\nimport type { SnapchatCapiEventPayload, SnapchatEventName } from '../types'\n\nfunction currentUrl(): string {\n return typeof window !== 'undefined' ? window.location.href : ''\n}\n\n/**\n * Match keys shared by every CAPI event: the `ScCid` click id captured on\n * ad landing and Snap's `_scid` cookie. Both are sent RAW - Snap matches\n * them against its own tables, they must never be hashed.\n */\nfunction baseUserData(): SnapchatCapiEventPayload['userData'] {\n const out: SnapchatCapiEventPayload['userData'] = {}\n const clickId = readClickId()\n const cookie1 = readScid()\n if (clickId) out.clickId = clickId\n if (cookie1) out.cookie1 = cookie1\n return out\n}\n\n/** Drop undefined keys so the pixel never receives `price: undefined`. */\nfunction compact(input: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(input)) {\n if (v !== undefined) out[k] = v\n }\n return out\n}\n\n/**\n * POST the event to the storefront's `/api/snapchat/events` proxy. The\n * proxy enriches with the PII cookie + IP + User-Agent and forwards to\n * Medusa's `/store/snapchat-ads/events` route, which queues it on the shared\n * outbox. Intentionally UNGATED on analytics consent - CAPI operates on a\n * different legal basis than the browser pixel. Merchant owns that call.\n */\nexport async function sendSnapchatCapiEvent(payload: SnapchatCapiEventPayload): Promise<void> {\n try {\n await fetch(CAPI_RELAY_ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true,\n })\n } catch {\n // non-fatal - CAPI is best-effort from the browser's perspective\n }\n}\n\n/**\n * Fire one event on both channels with a shared ID. The pixel gets\n * `client_dedup_id` (Snap's browser-side dedup key); the relay gets the\n * same value as `eventId`, which Medusa sends as CAPI `event_id`.\n *\n * Gated on the merchant's live settings first: with the plugin off or this\n * event's \"Track ...\" toggle off, nothing leaves the browser on either\n * channel. URL and match keys are read before the (cached) config lookup,\n * so an SPA navigation during it cannot attribute the event to the next page.\n */\nfunction fire(\n eventName: SnapchatEventName,\n eventId: string,\n pixelParams: Record<string, unknown>,\n customData?: SnapchatCapiEventPayload['customData'],\n): void {\n const payload: SnapchatCapiEventPayload = {\n eventName,\n eventId,\n eventSourceUrl: currentUrl(),\n userData: baseUserData(),\n ...(customData ? { customData } : {}),\n }\n const params = compact({ ...pixelParams, client_dedup_id: eventId })\n void loadSnapchatConfig().then((config) => {\n if (!isEventEnabled(config, eventName)) return\n safeSnaptr('track', eventName, params)\n void sendSnapchatCapiEvent(payload)\n })\n}\n\nexport function trackPageView(): void {\n fire(SNAP_EVENTS.PAGE_VIEW, makeUniqueEventId(SNAP_EVENTS.PAGE_VIEW, 'page'), {})\n}\n\nexport function trackViewContent(\n productId: string,\n value?: number,\n currency?: string,\n): void {\n fire(\n SNAP_EVENTS.VIEW_CONTENT,\n makeUniqueEventId(SNAP_EVENTS.VIEW_CONTENT, productId),\n { price: value, currency, item_ids: [productId] },\n { value, currency, contentIds: [productId] },\n )\n}\n\n/**\n * Fire `ADD_CART` for a single variant's quantity delta. `contentIds` is\n * typically `[productId, variantId]`.\n */\nexport function trackAddCart(\n variantId: string,\n quantity: number,\n value?: number,\n currency?: string,\n contentIds?: string[],\n): void {\n const ids = contentIds?.length ? contentIds : [variantId]\n fire(\n SNAP_EVENTS.ADD_CART,\n makeUniqueEventId(SNAP_EVENTS.ADD_CART, variantId),\n { price: value, currency, item_ids: ids, number_items: quantity },\n { value, currency, contentIds: ids, numItems: quantity },\n )\n}\n\n/** Deterministic per cart: a checkout refresh collapses into one event. */\nexport function trackStartCheckout(\n cartId: string,\n itemCount: number,\n value?: number,\n currency?: string,\n contentIds?: string[],\n): void {\n fire(\n SNAP_EVENTS.START_CHECKOUT,\n makeEventId(SNAP_EVENTS.START_CHECKOUT, cartId),\n { price: value, currency, item_ids: contentIds?.length ? contentIds : undefined, number_items: itemCount },\n { value, currency, contentIds, numItems: itemCount },\n )\n}\n\n/**\n * Order-complete tracker. Both channels use the SAME event name\n * (`PURCHASE`) - unlike Pinterest there is no browser/server name split.\n * Dedup is doubly anchored: `client_dedup_id` == `event_id` (48h) and pixel\n * `transaction_id` == CAPI `order_id` (30d).\n */\nexport function trackPurchase(\n orderId: string,\n value: number,\n currency: string,\n itemCount?: number,\n contentIds?: string[],\n): void {\n fire(\n SNAP_EVENTS.PURCHASE,\n makeEventId(SNAP_EVENTS.PURCHASE, orderId),\n {\n price: value,\n currency,\n transaction_id: orderId,\n item_ids: contentIds?.length ? contentIds : undefined,\n number_items: itemCount,\n },\n { value, currency, orderId, contentIds, numItems: itemCount },\n )\n}\n"],"mappings":"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","names":["import_pixel_core"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSnapchatConfig
|
|
3
|
+
} from "./chunk-IP2VTHD6.mjs";
|
|
4
|
+
|
|
5
|
+
// src/constants/index.ts
|
|
6
|
+
var SNAP_EVENTS = {
|
|
7
|
+
PAGE_VIEW: "PAGE_VIEW",
|
|
8
|
+
VIEW_CONTENT: "VIEW_CONTENT",
|
|
9
|
+
ADD_CART: "ADD_CART",
|
|
10
|
+
START_CHECKOUT: "START_CHECKOUT",
|
|
11
|
+
PURCHASE: "PURCHASE"
|
|
12
|
+
};
|
|
13
|
+
var EVENT_TOGGLES = {
|
|
14
|
+
PAGE_VIEW: "track_page_view",
|
|
15
|
+
VIEW_CONTENT: "track_view_content",
|
|
16
|
+
ADD_CART: "track_add_cart",
|
|
17
|
+
START_CHECKOUT: "track_start_checkout",
|
|
18
|
+
PURCHASE: "track_purchase"
|
|
19
|
+
};
|
|
20
|
+
var CONFIG_REFRESH_MS = 6e4;
|
|
21
|
+
var CAPI_RELAY_ENDPOINT = "/api/snapchat/events";
|
|
22
|
+
var COOKIE_NAMES = {
|
|
23
|
+
/** Plugin-owned raw PII for the relay; Medusa hashes it. */
|
|
24
|
+
PII: "amboras_snap_pii",
|
|
25
|
+
/** Plugin-owned copy of `ScCid`. */
|
|
26
|
+
CLICK_ID: "amboras_snap_clid",
|
|
27
|
+
/** Snap's own first-party cookie (forwarded as `sc_cookie1`). */
|
|
28
|
+
SCID: "_scid"
|
|
29
|
+
};
|
|
30
|
+
var COOKIE_MAX_AGE_SECONDS = {
|
|
31
|
+
PII: 30 * 24 * 60 * 60,
|
|
32
|
+
// Matches Snap's default 28-day click attribution window.
|
|
33
|
+
CLICK_ID: 28 * 24 * 60 * 60
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/lib/snaptr.ts
|
|
37
|
+
function safeSnaptr(...args) {
|
|
38
|
+
if (typeof window === "undefined") return false;
|
|
39
|
+
const fn = window.snaptr;
|
|
40
|
+
if (typeof fn !== "function") return false;
|
|
41
|
+
try {
|
|
42
|
+
fn(...args);
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/lib/event-id.ts
|
|
50
|
+
function makeEventId(eventName, primaryKey) {
|
|
51
|
+
const safeKey = primaryKey.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
52
|
+
return `${eventName.toLowerCase()}_${safeKey}`;
|
|
53
|
+
}
|
|
54
|
+
function randomSuffix() {
|
|
55
|
+
const c = globalThis.crypto;
|
|
56
|
+
if (c?.randomUUID) return c.randomUUID().replace(/-/g, "").slice(0, 16);
|
|
57
|
+
return Math.random().toString(36).slice(2, 12) + Date.now().toString(36);
|
|
58
|
+
}
|
|
59
|
+
function makeUniqueEventId(eventName, primaryKey) {
|
|
60
|
+
return `${makeEventId(eventName, primaryKey)}_${randomSuffix()}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/lib/cookies.ts
|
|
64
|
+
import { deleteCookie, readCookie, writeCookie, writePiiCookie } from "@amboras-dev/pixel-core";
|
|
65
|
+
function readScid() {
|
|
66
|
+
return readCookie(COOKIE_NAMES.SCID);
|
|
67
|
+
}
|
|
68
|
+
function readClickId() {
|
|
69
|
+
return readCookie(COOKIE_NAMES.CLICK_ID);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/lib/runtime-config.ts
|
|
73
|
+
var cached = null;
|
|
74
|
+
function loadSnapchatConfig() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
if (cached && now - cached.at < CONFIG_REFRESH_MS) return cached.read;
|
|
77
|
+
const read = getSnapchatConfig().then((config) => {
|
|
78
|
+
if (!config) cached = null;
|
|
79
|
+
return config;
|
|
80
|
+
});
|
|
81
|
+
cached = { at: now, read };
|
|
82
|
+
return read;
|
|
83
|
+
}
|
|
84
|
+
function activePixelId(config) {
|
|
85
|
+
if (!config || config.enabled !== true) return null;
|
|
86
|
+
return config.pixel_id || null;
|
|
87
|
+
}
|
|
88
|
+
function isEventEnabled(config, eventName) {
|
|
89
|
+
if (!activePixelId(config)) return false;
|
|
90
|
+
return config[EVENT_TOGGLES[eventName]] !== false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/hooks/useSnapchatEvents.ts
|
|
94
|
+
function currentUrl() {
|
|
95
|
+
return typeof window !== "undefined" ? window.location.href : "";
|
|
96
|
+
}
|
|
97
|
+
function baseUserData() {
|
|
98
|
+
const out = {};
|
|
99
|
+
const clickId = readClickId();
|
|
100
|
+
const cookie1 = readScid();
|
|
101
|
+
if (clickId) out.clickId = clickId;
|
|
102
|
+
if (cookie1) out.cookie1 = cookie1;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function compact(input) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [k, v] of Object.entries(input)) {
|
|
108
|
+
if (v !== void 0) out[k] = v;
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
async function sendSnapchatCapiEvent(payload) {
|
|
113
|
+
try {
|
|
114
|
+
await fetch(CAPI_RELAY_ENDPOINT, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify(payload),
|
|
118
|
+
keepalive: true
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function fire(eventName, eventId, pixelParams, customData) {
|
|
124
|
+
const payload = {
|
|
125
|
+
eventName,
|
|
126
|
+
eventId,
|
|
127
|
+
eventSourceUrl: currentUrl(),
|
|
128
|
+
userData: baseUserData(),
|
|
129
|
+
...customData ? { customData } : {}
|
|
130
|
+
};
|
|
131
|
+
const params = compact({ ...pixelParams, client_dedup_id: eventId });
|
|
132
|
+
void loadSnapchatConfig().then((config) => {
|
|
133
|
+
if (!isEventEnabled(config, eventName)) return;
|
|
134
|
+
safeSnaptr("track", eventName, params);
|
|
135
|
+
void sendSnapchatCapiEvent(payload);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function trackPageView() {
|
|
139
|
+
fire(SNAP_EVENTS.PAGE_VIEW, makeUniqueEventId(SNAP_EVENTS.PAGE_VIEW, "page"), {});
|
|
140
|
+
}
|
|
141
|
+
function trackViewContent(productId, value, currency) {
|
|
142
|
+
fire(
|
|
143
|
+
SNAP_EVENTS.VIEW_CONTENT,
|
|
144
|
+
makeUniqueEventId(SNAP_EVENTS.VIEW_CONTENT, productId),
|
|
145
|
+
{ price: value, currency, item_ids: [productId] },
|
|
146
|
+
{ value, currency, contentIds: [productId] }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
function trackAddCart(variantId, quantity, value, currency, contentIds) {
|
|
150
|
+
const ids = contentIds?.length ? contentIds : [variantId];
|
|
151
|
+
fire(
|
|
152
|
+
SNAP_EVENTS.ADD_CART,
|
|
153
|
+
makeUniqueEventId(SNAP_EVENTS.ADD_CART, variantId),
|
|
154
|
+
{ price: value, currency, item_ids: ids, number_items: quantity },
|
|
155
|
+
{ value, currency, contentIds: ids, numItems: quantity }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
function trackStartCheckout(cartId, itemCount, value, currency, contentIds) {
|
|
159
|
+
fire(
|
|
160
|
+
SNAP_EVENTS.START_CHECKOUT,
|
|
161
|
+
makeEventId(SNAP_EVENTS.START_CHECKOUT, cartId),
|
|
162
|
+
{ price: value, currency, item_ids: contentIds?.length ? contentIds : void 0, number_items: itemCount },
|
|
163
|
+
{ value, currency, contentIds, numItems: itemCount }
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function trackPurchase(orderId, value, currency, itemCount, contentIds) {
|
|
167
|
+
fire(
|
|
168
|
+
SNAP_EVENTS.PURCHASE,
|
|
169
|
+
makeEventId(SNAP_EVENTS.PURCHASE, orderId),
|
|
170
|
+
{
|
|
171
|
+
price: value,
|
|
172
|
+
currency,
|
|
173
|
+
transaction_id: orderId,
|
|
174
|
+
item_ids: contentIds?.length ? contentIds : void 0,
|
|
175
|
+
number_items: itemCount
|
|
176
|
+
},
|
|
177
|
+
{ value, currency, orderId, contentIds, numItems: itemCount }
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
export {
|
|
181
|
+
CAPI_RELAY_ENDPOINT,
|
|
182
|
+
COOKIE_NAMES,
|
|
183
|
+
SNAP_EVENTS,
|
|
184
|
+
getSnapchatConfig,
|
|
185
|
+
makeEventId,
|
|
186
|
+
makeUniqueEventId,
|
|
187
|
+
sendSnapchatCapiEvent,
|
|
188
|
+
trackAddCart,
|
|
189
|
+
trackPageView,
|
|
190
|
+
trackPurchase,
|
|
191
|
+
trackStartCheckout,
|
|
192
|
+
trackViewContent
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants/index.ts","../src/lib/snaptr.ts","../src/lib/event-id.ts","../src/lib/cookies.ts","../src/lib/runtime-config.ts","../src/hooks/useSnapchatEvents.ts"],"sourcesContent":["/**\n * Every fixed string the package puts on the wire or in browser storage.\n * Wire names are a cross-repo contract: Medusa's strict Zod schema and the\n * orchestrator's storefront relay template expect exactly these values.\n */\n\n/**\n * Snap standard event names. The pixel (`snaptr('track', <name>)`) and\n * Conversions API v3 (`event_name`) use the SAME uppercase strings.\n * https://developers.snap.com/api/marketing-api/Conversions-API/Parameters\n */\nexport const SNAP_EVENTS = {\n PAGE_VIEW: 'PAGE_VIEW',\n VIEW_CONTENT: 'VIEW_CONTENT',\n ADD_CART: 'ADD_CART',\n START_CHECKOUT: 'START_CHECKOUT',\n PURCHASE: 'PURCHASE',\n} as const\n\n/**\n * Merchant setting (from `/store/snapchat-ads/config`) that switches each\n * event off. Medusa enforces the same map server-side before enqueueing.\n */\nexport const EVENT_TOGGLES = {\n PAGE_VIEW: 'track_page_view',\n VIEW_CONTENT: 'track_view_content',\n ADD_CART: 'track_add_cart',\n START_CHECKOUT: 'track_start_checkout',\n PURCHASE: 'track_purchase',\n} as const satisfies Record<keyof typeof SNAP_EVENTS, string>\n\n/** How long the browser reuses the merchant config before re-reading it. */\nexport const CONFIG_REFRESH_MS = 60_000\n\n/** Storefront relay route written by the orchestrator's template. */\nexport const CAPI_RELAY_ENDPOINT = '/api/snapchat/events'\n\n/** Snap's official pixel script. */\nexport const SNAP_SCRIPT_SRC = 'https://sc-static.net/scevent.min.js'\n\n/** URL param Snap appends to ad landing URLs (forwarded as `sc_click_id`). */\nexport const CLICK_ID_QUERY_PARAM = 'ScCid'\n\nexport const COOKIE_NAMES = {\n /** Plugin-owned raw PII for the relay; Medusa hashes it. */\n PII: 'amboras_snap_pii',\n /** Plugin-owned copy of `ScCid`. */\n CLICK_ID: 'amboras_snap_clid',\n /** Snap's own first-party cookie (forwarded as `sc_cookie1`). */\n SCID: '_scid',\n} as const\n\nexport const COOKIE_MAX_AGE_SECONDS = {\n PII: 30 * 24 * 60 * 60,\n // Matches Snap's default 28-day click attribution window.\n CLICK_ID: 28 * 24 * 60 * 60,\n} as const\n\n/** sessionStorage / globalThis keys the slot components use for dedup. */\nexport const STORAGE_KEYS = {\n CART_STATE: 'amboras_snap_cart_state_v1',\n CART_RECENT_FIRES: '__amboras_snap_recent_fires__',\n CHECKOUT_SEEN: 'amboras_snap_checkout_seen',\n PURCHASE_PREFIX: 'amboras_snap_purchase_',\n} as const\n\n/** Storefront window events the provider listens to. */\nexport const WINDOW_EVENTS = {\n CONSENT_CHANGED: 'amboras-consent-changed',\n AUTH_CHANGED: 'amboras-auth-changed',\n} as const\n","/**\n * Typed wrapper around Snap's global `snaptr`. The shim is installed by\n * `SnapPixelProvider` behind consent - every other slot component calls\n * `safeSnaptr()`, which is a no-op when snaptr is absent (consent denied,\n * plugin uninstalled, or SSR).\n */\n\nimport { SNAP_SCRIPT_SRC } from '../constants'\n\ndeclare global {\n interface Window {\n snaptr?: SnaptrFunction\n }\n}\n\nexport type SnaptrFunction = (...args: unknown[]) => void\n\n/** Best-effort synchronous call. Returns whether snaptr was present. */\nexport function safeSnaptr(...args: unknown[]): boolean {\n if (typeof window === 'undefined') return false\n const fn = window.snaptr\n if (typeof fn !== 'function') return false\n try {\n fn(...args)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Install the snaptr queue shim + inject `scevent.min.js`. Idempotent.\n * Mirrors the shape of Snap's official install snippet\n * (https://developers.snap.com/api/marketing-api/Ads-API/snap-pixel) so the\n * loaded script drains the queue via `handleRequest`.\n */\nexport function installSnaptrShim(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n if (window.snaptr) return\n\n const shim = function (...args: unknown[]): void {\n const self = shim as unknown as { handleRequest?: (...a: unknown[]) => void; queue: unknown[] }\n if (self.handleRequest) self.handleRequest(...args)\n else self.queue.push(args)\n } as SnaptrFunction\n ;(shim as unknown as { queue: unknown[] }).queue = []\n window.snaptr = shim\n\n const script = document.createElement('script')\n script.async = true\n script.src = SNAP_SCRIPT_SRC\n document.head.appendChild(script)\n}\n","import type { SnapchatEventName } from '../types'\n\n/**\n * Event-ID builders for browser <-> CAPI dedup. Snap matches the pixel's\n * `client_dedup_id` against the CAPI `event_id` within 48h, so the browser\n * call and the CAPI relay for one user action must carry the same ID. Both\n * are fired from the same function call, so the ID only has to be stable\n * within that call - not across calls.\n *\n * Two flavours:\n * - `makeEventId`: deterministic. Used where a repeat fire of the SAME\n * action must collapse into one (START_CHECKOUT per cart, PURCHASE per\n * order), e.g. a refresh on the thank-you page.\n * - `makeUniqueEventId`: deterministic prefix + random suffix. Used for\n * actions a shopper legitimately repeats (viewing a product again,\n * adding the same variant twice, revisiting a page). A pure\n * deterministic ID would make Snap drop every repeat within 48h.\n */\nexport function makeEventId(eventName: SnapchatEventName, primaryKey: string): string {\n const safeKey = primaryKey.replace(/[^a-zA-Z0-9_-]/g, '_')\n return `${eventName.toLowerCase()}_${safeKey}`\n}\n\nfunction randomSuffix(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto\n if (c?.randomUUID) return c.randomUUID().replace(/-/g, '').slice(0, 16)\n return Math.random().toString(36).slice(2, 12) + Date.now().toString(36)\n}\n\nexport function makeUniqueEventId(eventName: SnapchatEventName, primaryKey: string): string {\n return `${makeEventId(eventName, primaryKey)}_${randomSuffix()}`\n}\n","/**\n * Snap-specific cookies, on top of `@amboras-dev/pixel-core`'s cookie helpers:\n * - `_scid`: Snap's first-party cookie set by the pixel. Forwarded raw to\n * CAPI as `sc_cookie1`.\n * - `amboras_snap_clid`: plugin-owned copy of the `ScCid` URL param Snap\n * appends to ad landing URLs. Forwarded raw to CAPI as `sc_click_id`.\n * Captured on landing because a later SPA navigation drops the param.\n * - `amboras_snap_pii`: plugin-owned raw (unhashed) customer PII for the\n * storefront's `/api/snapchat/events` relay; Medusa normalizes + hashes\n * it before shipping to Snap. Separate from the meta/pinterest PII\n * cookies so the plugins stay decoupled.\n */\n\nimport { deleteCookie, readCookie, writeCookie, writePiiCookie, type PiiPayload } from '@amboras-dev/pixel-core'\nimport { CLICK_ID_QUERY_PARAM, COOKIE_MAX_AGE_SECONDS, COOKIE_NAMES } from '../constants'\n\nexport function readScid(): string | undefined {\n return readCookie(COOKIE_NAMES.SCID)\n}\n\nexport function readClickId(): string | undefined {\n return readCookie(COOKIE_NAMES.CLICK_ID)\n}\n\n/**\n * Store `?ScCid=` from the landing URL. A newer click id replaces an older\n * one so the most recent ad click wins attribution.\n */\nexport function captureClickIdFromQuery(): void {\n if (typeof window === 'undefined' || typeof document === 'undefined') return\n let clickId: string | null = null\n try {\n clickId = new URL(window.location.href).searchParams.get(CLICK_ID_QUERY_PARAM)\n } catch {\n return\n }\n if (!clickId || clickId === readClickId()) return\n writeCookie(COOKIE_NAMES.CLICK_ID, clickId, COOKIE_MAX_AGE_SECONDS.CLICK_ID)\n}\n\nexport function writeSnapchatPiiCookie(payload: PiiPayload): void {\n writePiiCookie(COOKIE_NAMES.PII, payload, COOKIE_MAX_AGE_SECONDS.PII)\n}\n\nexport function clearSnapchatPiiCookie(): void {\n deleteCookie(COOKIE_NAMES.PII)\n}\n","/**\n * The merchant's live Snapchat settings, read once per page (refreshed\n * every CONFIG_REFRESH_MS) and shared by the provider and every event.\n *\n * Fail-closed: no config (network error, plugin disconnected, Medusa answers\n * `{ enabled: false }`) means no pixel and no events. The browser then\n * honours \"Plugin enabled\" and each \"Track ...\" toggle the same way Medusa's\n * events route does, and a Pixel ID change applies without a redeploy.\n */\n\nimport { getSnapchatConfig } from '../get-config'\nimport { CONFIG_REFRESH_MS, EVENT_TOGGLES } from '../constants'\nimport type { SnapchatEventName, SnapchatPublicConfig } from '../types'\n\nlet cached: { at: number; read: Promise<SnapchatPublicConfig | null> } | null = null\n\nexport function loadSnapchatConfig(): Promise<SnapchatPublicConfig | null> {\n const now = Date.now()\n if (cached && now - cached.at < CONFIG_REFRESH_MS) return cached.read\n const read = getSnapchatConfig().then((config) => {\n // A failed read is not reused, so the next event tries again.\n if (!config) cached = null\n return config\n })\n cached = { at: now, read }\n return read\n}\n\n/** The pixel ID to load, or null when the plugin is off / not configured. */\nexport function activePixelId(config: SnapchatPublicConfig | null): string | null {\n if (!config || config.enabled !== true) return null\n return config.pixel_id || null\n}\n\nexport function isEventEnabled(config: SnapchatPublicConfig | null, eventName: SnapchatEventName): boolean {\n if (!activePixelId(config)) return false\n // A toggle the config omits counts as on, matching Medusa's default.\n return config![EVENT_TOGGLES[eventName]] !== false\n}\n\n/** Test-only: forget the cached config. */\nexport function resetSnapchatConfigCache(): void {\n cached = null\n}\n","import { safeSnaptr } from '../lib/snaptr'\nimport { makeEventId, makeUniqueEventId } from '../lib/event-id'\nimport { readClickId, readScid } from '../lib/cookies'\nimport { CAPI_RELAY_ENDPOINT, SNAP_EVENTS } from '../constants'\nimport { isEventEnabled, loadSnapchatConfig } from '../lib/runtime-config'\nimport type { SnapchatCapiEventPayload, SnapchatEventName } from '../types'\n\nfunction currentUrl(): string {\n return typeof window !== 'undefined' ? window.location.href : ''\n}\n\n/**\n * Match keys shared by every CAPI event: the `ScCid` click id captured on\n * ad landing and Snap's `_scid` cookie. Both are sent RAW - Snap matches\n * them against its own tables, they must never be hashed.\n */\nfunction baseUserData(): SnapchatCapiEventPayload['userData'] {\n const out: SnapchatCapiEventPayload['userData'] = {}\n const clickId = readClickId()\n const cookie1 = readScid()\n if (clickId) out.clickId = clickId\n if (cookie1) out.cookie1 = cookie1\n return out\n}\n\n/** Drop undefined keys so the pixel never receives `price: undefined`. */\nfunction compact(input: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(input)) {\n if (v !== undefined) out[k] = v\n }\n return out\n}\n\n/**\n * POST the event to the storefront's `/api/snapchat/events` proxy. The\n * proxy enriches with the PII cookie + IP + User-Agent and forwards to\n * Medusa's `/store/snapchat-ads/events` route, which queues it on the shared\n * outbox. Intentionally UNGATED on analytics consent - CAPI operates on a\n * different legal basis than the browser pixel. Merchant owns that call.\n */\nexport async function sendSnapchatCapiEvent(payload: SnapchatCapiEventPayload): Promise<void> {\n try {\n await fetch(CAPI_RELAY_ENDPOINT, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(payload),\n keepalive: true,\n })\n } catch {\n // non-fatal - CAPI is best-effort from the browser's perspective\n }\n}\n\n/**\n * Fire one event on both channels with a shared ID. The pixel gets\n * `client_dedup_id` (Snap's browser-side dedup key); the relay gets the\n * same value as `eventId`, which Medusa sends as CAPI `event_id`.\n *\n * Gated on the merchant's live settings first: with the plugin off or this\n * event's \"Track ...\" toggle off, nothing leaves the browser on either\n * channel. URL and match keys are read before the (cached) config lookup,\n * so an SPA navigation during it cannot attribute the event to the next page.\n */\nfunction fire(\n eventName: SnapchatEventName,\n eventId: string,\n pixelParams: Record<string, unknown>,\n customData?: SnapchatCapiEventPayload['customData'],\n): void {\n const payload: SnapchatCapiEventPayload = {\n eventName,\n eventId,\n eventSourceUrl: currentUrl(),\n userData: baseUserData(),\n ...(customData ? { customData } : {}),\n }\n const params = compact({ ...pixelParams, client_dedup_id: eventId })\n void loadSnapchatConfig().then((config) => {\n if (!isEventEnabled(config, eventName)) return\n safeSnaptr('track', eventName, params)\n void sendSnapchatCapiEvent(payload)\n })\n}\n\nexport function trackPageView(): void {\n fire(SNAP_EVENTS.PAGE_VIEW, makeUniqueEventId(SNAP_EVENTS.PAGE_VIEW, 'page'), {})\n}\n\nexport function trackViewContent(\n productId: string,\n value?: number,\n currency?: string,\n): void {\n fire(\n SNAP_EVENTS.VIEW_CONTENT,\n makeUniqueEventId(SNAP_EVENTS.VIEW_CONTENT, productId),\n { price: value, currency, item_ids: [productId] },\n { value, currency, contentIds: [productId] },\n )\n}\n\n/**\n * Fire `ADD_CART` for a single variant's quantity delta. `contentIds` is\n * typically `[productId, variantId]`.\n */\nexport function trackAddCart(\n variantId: string,\n quantity: number,\n value?: number,\n currency?: string,\n contentIds?: string[],\n): void {\n const ids = contentIds?.length ? contentIds : [variantId]\n fire(\n SNAP_EVENTS.ADD_CART,\n makeUniqueEventId(SNAP_EVENTS.ADD_CART, variantId),\n { price: value, currency, item_ids: ids, number_items: quantity },\n { value, currency, contentIds: ids, numItems: quantity },\n )\n}\n\n/** Deterministic per cart: a checkout refresh collapses into one event. */\nexport function trackStartCheckout(\n cartId: string,\n itemCount: number,\n value?: number,\n currency?: string,\n contentIds?: string[],\n): void {\n fire(\n SNAP_EVENTS.START_CHECKOUT,\n makeEventId(SNAP_EVENTS.START_CHECKOUT, cartId),\n { price: value, currency, item_ids: contentIds?.length ? contentIds : undefined, number_items: itemCount },\n { value, currency, contentIds, numItems: itemCount },\n )\n}\n\n/**\n * Order-complete tracker. Both channels use the SAME event name\n * (`PURCHASE`) - unlike Pinterest there is no browser/server name split.\n * Dedup is doubly anchored: `client_dedup_id` == `event_id` (48h) and pixel\n * `transaction_id` == CAPI `order_id` (30d).\n */\nexport function trackPurchase(\n orderId: string,\n value: number,\n currency: string,\n itemCount?: number,\n contentIds?: string[],\n): void {\n fire(\n SNAP_EVENTS.PURCHASE,\n makeEventId(SNAP_EVENTS.PURCHASE, orderId),\n {\n price: value,\n currency,\n transaction_id: orderId,\n item_ids: contentIds?.length ? contentIds : undefined,\n number_items: itemCount,\n },\n { value, currency, orderId, contentIds, numItems: itemCount },\n )\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
interface SnapPixelProviderProps {
|
|
2
|
+
/**
|
|
3
|
+
* Bound by the manifest's `propsFromConfig` (migration 475) and kept for
|
|
4
|
+
* that contract. The pixel actually loaded is the live config's
|
|
5
|
+
* `pixel_id`, so a Pixel ID change in the admin applies without a
|
|
6
|
+
* storefront redeploy.
|
|
7
|
+
*/
|
|
8
|
+
pixelId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Root providers slot component for Snapchat. Three effects, one job each:
|
|
12
|
+
* 1. Consent: read `amboras_consent` and follow `amboras-consent-changed`
|
|
13
|
+
* so a banner can enable the pixel mid-session. Nothing below runs
|
|
14
|
+
* without consent.
|
|
15
|
+
* 2. Identity: fetch the logged-in customer on mount and on
|
|
16
|
+
* `amboras-auth-changed` (not per route - identity almost never changes
|
|
17
|
+
* mid-session) and write or clear the `amboras_snap_pii` cookie the CAPI
|
|
18
|
+
* relay reads.
|
|
19
|
+
* 3. Navigation: on every route change capture `?ScCid=`, read the
|
|
20
|
+
* merchant's live config, install the pixel the first time
|
|
21
|
+
* (`snaptr('init', pixelId)`), then fire the single `PAGE_VIEW`
|
|
22
|
+
* (browser + CAPI, shared event id). The click id is captured first so
|
|
23
|
+
* the landing page's PAGE_VIEW already carries it. With the plugin
|
|
24
|
+
* switched off (or no config) the pixel is never loaded.
|
|
25
|
+
*/
|
|
26
|
+
declare function SnapPixelProvider(_props: SnapPixelProviderProps): null;
|
|
27
|
+
|
|
28
|
+
interface SnapViewContentProps {
|
|
29
|
+
productId?: string;
|
|
30
|
+
productName?: string;
|
|
31
|
+
sku?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Product price in Medusa's storage unit (×100 for every currency, see
|
|
34
|
+
* `toMajorUnits` in @amboras-dev/pixel-core). Converted to major units
|
|
35
|
+
* before it reaches Snap.
|
|
36
|
+
*/
|
|
37
|
+
price?: number;
|
|
38
|
+
currency?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Null-rendering component for the `pdpAnalytics` plugin slot. Fires Snap
|
|
42
|
+
* `VIEW_CONTENT` (pixel + CAPI relay) on PDP mount and once per productId
|
|
43
|
+
* change during SPA navigation.
|
|
44
|
+
*/
|
|
45
|
+
declare function SnapViewContent({ productId, price, currency }: SnapViewContentProps): null;
|
|
46
|
+
|
|
47
|
+
interface SnapAddCartProps {
|
|
48
|
+
cartId?: string;
|
|
49
|
+
itemCount?: number;
|
|
50
|
+
total?: number;
|
|
51
|
+
currency?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Null-rendering component for the `cartUpdate` plugin slot. Fires Snap
|
|
55
|
+
* `ADD_CART` once per newly added variant, with the positive quantity delta
|
|
56
|
+
* and its value in major units.
|
|
57
|
+
*
|
|
58
|
+
* The diff (first-hydration seed, per-cart sessionStorage snapshot, removals
|
|
59
|
+
* ignored, per-variant dedup window, failed-read handling) lives in
|
|
60
|
+
* `@amboras-dev/pixel-core`'s cart-add tracker; this component only maps an
|
|
61
|
+
* add to the Snap event. The cart is self-fetched there, so the event never
|
|
62
|
+
* depends on merchant-passed `total` / `currency`.
|
|
63
|
+
*/
|
|
64
|
+
declare function SnapAddCart({ cartId, itemCount }: SnapAddCartProps): null;
|
|
65
|
+
|
|
66
|
+
interface SnapStartCheckoutProps {
|
|
67
|
+
cartId?: string;
|
|
68
|
+
itemCount?: number;
|
|
69
|
+
total?: number;
|
|
70
|
+
currency?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Null-rendering component for the `checkoutStart` plugin slot. Fires
|
|
74
|
+
* Snap `START_CHECKOUT` exactly once per cartId. Two-layer dedup:
|
|
75
|
+
* - `useRef` guards StrictMode double-mount.
|
|
76
|
+
* - `sessionStorage` guards page-refresh + back-navigation within the
|
|
77
|
+
* same session.
|
|
78
|
+
*
|
|
79
|
+
* Self-fetches the cart from Medusa so `value` and `currency` are correct
|
|
80
|
+
* even when the merchant slot context is thin or wrong. Snap treats
|
|
81
|
+
* `value` as major units, so `toMajorUnits` runs at this boundary (Medusa
|
|
82
|
+
* stores everything in minor units).
|
|
83
|
+
*
|
|
84
|
+
* The dedup ref is set only AFTER a successful dispatch - a StrictMode
|
|
85
|
+
* cleanup between effect-run and fetch-complete does NOT poison the ref,
|
|
86
|
+
* so the event still fires on the second mount.
|
|
87
|
+
*/
|
|
88
|
+
declare function SnapStartCheckout({ cartId, itemCount, total, currency, }: SnapStartCheckoutProps): null;
|
|
89
|
+
|
|
90
|
+
interface SnapPurchaseProps {
|
|
91
|
+
orderId?: string;
|
|
92
|
+
total?: number;
|
|
93
|
+
currency?: string;
|
|
94
|
+
itemCount?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Null-rendering component for the `checkoutComplete` plugin slot.
|
|
98
|
+
* Fires Snap `PURCHASE` (with `transaction_id` = order id) exactly once per
|
|
99
|
+
* orderId.
|
|
100
|
+
*
|
|
101
|
+
* Primary path: self-fetch from `/store/orders/:id` and derive value +
|
|
102
|
+
* currency + contentIds from the authoritative Medusa response, then convert
|
|
103
|
+
* to major units via currency-aware `toMajorUnits` (correct for JPY/KWD).
|
|
104
|
+
*
|
|
105
|
+
* Fallback path (fetch failed - guest order with expired session, network
|
|
106
|
+
* error): fire using the incoming `total` + `currency` props with the same
|
|
107
|
+
* conversion. Snap dedups against the CAPI relay by `client_dedup_id` /
|
|
108
|
+
* `event_id` (48h) and `transaction_id` / `order_id` (30d).
|
|
109
|
+
*
|
|
110
|
+
* Two-layer dedup (useRef + sessionStorage) so a StrictMode double-mount,
|
|
111
|
+
* a page refresh on the thank-you page, or a back-nav never double-fires.
|
|
112
|
+
* The ref is set AFTER dispatch so a StrictMode cleanup between mount and
|
|
113
|
+
* effect body doesn't poison it.
|
|
114
|
+
*/
|
|
115
|
+
declare function SnapPurchase({ orderId, total, currency, itemCount, }: SnapPurchaseProps): null;
|
|
116
|
+
|
|
117
|
+
export { SnapAddCart, type SnapAddCartProps, SnapPixelProvider, type SnapPixelProviderProps, SnapPurchase, type SnapPurchaseProps, SnapStartCheckout, type SnapStartCheckoutProps, SnapViewContent, type SnapViewContentProps };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
interface SnapPixelProviderProps {
|
|
2
|
+
/**
|
|
3
|
+
* Bound by the manifest's `propsFromConfig` (migration 475) and kept for
|
|
4
|
+
* that contract. The pixel actually loaded is the live config's
|
|
5
|
+
* `pixel_id`, so a Pixel ID change in the admin applies without a
|
|
6
|
+
* storefront redeploy.
|
|
7
|
+
*/
|
|
8
|
+
pixelId?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Root providers slot component for Snapchat. Three effects, one job each:
|
|
12
|
+
* 1. Consent: read `amboras_consent` and follow `amboras-consent-changed`
|
|
13
|
+
* so a banner can enable the pixel mid-session. Nothing below runs
|
|
14
|
+
* without consent.
|
|
15
|
+
* 2. Identity: fetch the logged-in customer on mount and on
|
|
16
|
+
* `amboras-auth-changed` (not per route - identity almost never changes
|
|
17
|
+
* mid-session) and write or clear the `amboras_snap_pii` cookie the CAPI
|
|
18
|
+
* relay reads.
|
|
19
|
+
* 3. Navigation: on every route change capture `?ScCid=`, read the
|
|
20
|
+
* merchant's live config, install the pixel the first time
|
|
21
|
+
* (`snaptr('init', pixelId)`), then fire the single `PAGE_VIEW`
|
|
22
|
+
* (browser + CAPI, shared event id). The click id is captured first so
|
|
23
|
+
* the landing page's PAGE_VIEW already carries it. With the plugin
|
|
24
|
+
* switched off (or no config) the pixel is never loaded.
|
|
25
|
+
*/
|
|
26
|
+
declare function SnapPixelProvider(_props: SnapPixelProviderProps): null;
|
|
27
|
+
|
|
28
|
+
interface SnapViewContentProps {
|
|
29
|
+
productId?: string;
|
|
30
|
+
productName?: string;
|
|
31
|
+
sku?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Product price in Medusa's storage unit (×100 for every currency, see
|
|
34
|
+
* `toMajorUnits` in @amboras-dev/pixel-core). Converted to major units
|
|
35
|
+
* before it reaches Snap.
|
|
36
|
+
*/
|
|
37
|
+
price?: number;
|
|
38
|
+
currency?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Null-rendering component for the `pdpAnalytics` plugin slot. Fires Snap
|
|
42
|
+
* `VIEW_CONTENT` (pixel + CAPI relay) on PDP mount and once per productId
|
|
43
|
+
* change during SPA navigation.
|
|
44
|
+
*/
|
|
45
|
+
declare function SnapViewContent({ productId, price, currency }: SnapViewContentProps): null;
|
|
46
|
+
|
|
47
|
+
interface SnapAddCartProps {
|
|
48
|
+
cartId?: string;
|
|
49
|
+
itemCount?: number;
|
|
50
|
+
total?: number;
|
|
51
|
+
currency?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Null-rendering component for the `cartUpdate` plugin slot. Fires Snap
|
|
55
|
+
* `ADD_CART` once per newly added variant, with the positive quantity delta
|
|
56
|
+
* and its value in major units.
|
|
57
|
+
*
|
|
58
|
+
* The diff (first-hydration seed, per-cart sessionStorage snapshot, removals
|
|
59
|
+
* ignored, per-variant dedup window, failed-read handling) lives in
|
|
60
|
+
* `@amboras-dev/pixel-core`'s cart-add tracker; this component only maps an
|
|
61
|
+
* add to the Snap event. The cart is self-fetched there, so the event never
|
|
62
|
+
* depends on merchant-passed `total` / `currency`.
|
|
63
|
+
*/
|
|
64
|
+
declare function SnapAddCart({ cartId, itemCount }: SnapAddCartProps): null;
|
|
65
|
+
|
|
66
|
+
interface SnapStartCheckoutProps {
|
|
67
|
+
cartId?: string;
|
|
68
|
+
itemCount?: number;
|
|
69
|
+
total?: number;
|
|
70
|
+
currency?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Null-rendering component for the `checkoutStart` plugin slot. Fires
|
|
74
|
+
* Snap `START_CHECKOUT` exactly once per cartId. Two-layer dedup:
|
|
75
|
+
* - `useRef` guards StrictMode double-mount.
|
|
76
|
+
* - `sessionStorage` guards page-refresh + back-navigation within the
|
|
77
|
+
* same session.
|
|
78
|
+
*
|
|
79
|
+
* Self-fetches the cart from Medusa so `value` and `currency` are correct
|
|
80
|
+
* even when the merchant slot context is thin or wrong. Snap treats
|
|
81
|
+
* `value` as major units, so `toMajorUnits` runs at this boundary (Medusa
|
|
82
|
+
* stores everything in minor units).
|
|
83
|
+
*
|
|
84
|
+
* The dedup ref is set only AFTER a successful dispatch - a StrictMode
|
|
85
|
+
* cleanup between effect-run and fetch-complete does NOT poison the ref,
|
|
86
|
+
* so the event still fires on the second mount.
|
|
87
|
+
*/
|
|
88
|
+
declare function SnapStartCheckout({ cartId, itemCount, total, currency, }: SnapStartCheckoutProps): null;
|
|
89
|
+
|
|
90
|
+
interface SnapPurchaseProps {
|
|
91
|
+
orderId?: string;
|
|
92
|
+
total?: number;
|
|
93
|
+
currency?: string;
|
|
94
|
+
itemCount?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Null-rendering component for the `checkoutComplete` plugin slot.
|
|
98
|
+
* Fires Snap `PURCHASE` (with `transaction_id` = order id) exactly once per
|
|
99
|
+
* orderId.
|
|
100
|
+
*
|
|
101
|
+
* Primary path: self-fetch from `/store/orders/:id` and derive value +
|
|
102
|
+
* currency + contentIds from the authoritative Medusa response, then convert
|
|
103
|
+
* to major units via currency-aware `toMajorUnits` (correct for JPY/KWD).
|
|
104
|
+
*
|
|
105
|
+
* Fallback path (fetch failed - guest order with expired session, network
|
|
106
|
+
* error): fire using the incoming `total` + `currency` props with the same
|
|
107
|
+
* conversion. Snap dedups against the CAPI relay by `client_dedup_id` /
|
|
108
|
+
* `event_id` (48h) and `transaction_id` / `order_id` (30d).
|
|
109
|
+
*
|
|
110
|
+
* Two-layer dedup (useRef + sessionStorage) so a StrictMode double-mount,
|
|
111
|
+
* a page refresh on the thank-you page, or a back-nav never double-fires.
|
|
112
|
+
* The ref is set AFTER dispatch so a StrictMode cleanup between mount and
|
|
113
|
+
* effect body doesn't poison it.
|
|
114
|
+
*/
|
|
115
|
+
declare function SnapPurchase({ orderId, total, currency, itemCount, }: SnapPurchaseProps): null;
|
|
116
|
+
|
|
117
|
+
export { SnapAddCart, type SnapAddCartProps, SnapPixelProvider, type SnapPixelProviderProps, SnapPurchase, type SnapPurchaseProps, SnapStartCheckout, type SnapStartCheckoutProps, SnapViewContent, type SnapViewContentProps };
|