@engagex/react 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/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @engagex/react
2
+
3
+ React / Next.js wrapper for **EngageX** tracking. It loads `engage.v1.js` from
4
+ your tracking host and gives you a typed `<EngageProvider>`, `sendEngageEvent()`
5
+ and `engageIdentify()` — with **automatic page-view tracking across SPA route
6
+ changes**.
7
+
8
+ > Like `@next/third-parties`, this package does **not** bundle the tracker — it
9
+ > injects the script from your host, so it auto-updates, keeps endpoint
10
+ > auto-detection, and shares the anonymous `_cdp_tid` identity with your landing
11
+ > pages / embedded forms.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm add @engagex/react
17
+ # or: pnpm add @engagex/react
18
+ ```
19
+
20
+ ## Next.js (App Router)
21
+
22
+ ```tsx
23
+ // app/layout.tsx
24
+ import { EngageProvider } from '@engagex/react'
25
+
26
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
27
+ return (
28
+ <html>
29
+ <body>
30
+ <EngageProvider
31
+ publicKey="pk_live_xxx"
32
+ host="https://engage.data360.asia"
33
+ />
34
+ {children}
35
+ </body>
36
+ </html>
37
+ )
38
+ }
39
+ ```
40
+
41
+ `page.viewed` fires automatically on every route change — no `useEffect` needed.
42
+
43
+ ## Send events / identify (any client component)
44
+
45
+ ```tsx
46
+ 'use client'
47
+ import { sendEngageEvent, engageIdentify } from '@engagex/react'
48
+
49
+ // business / behavioural event
50
+ sendEngageEvent('order.placed', { order_id: 'DH1', total: 1990000, currency: 'VND' })
51
+
52
+ // identify — resolving key (phone/email/zalo_id, ≥1) + optional traits + consent
53
+ engageIdentify(
54
+ { phone: '0912345678', name: 'Nguyễn Văn An', gender: 'male', birthday: '1990-12-31' },
55
+ { consent: true, purpose: 'marketing' },
56
+ )
57
+ ```
58
+
59
+ - Traits are applied **fill-missing** (never overwrite existing profile data).
60
+ - Re-calling `identify` with the same phone **updates**, it does not create a
61
+ duplicate. Identifying a different person rotates the anonymous id.
62
+
63
+ ## Plain React (Vite / CRA)
64
+
65
+ Same API. `<EngageProvider>` tracks route changes via the History API, so it
66
+ works with React Router too — no extra wiring.
67
+
68
+ ## API
69
+
70
+ | Export | Description |
71
+ | --- | --- |
72
+ | `<EngageProvider publicKey host autoPageview? branchId? />` | Loads the SDK + auto page-view. Mount once at the root. |
73
+ | `sendEngageEvent(event, properties?)` | Forward a `track` call (SSR-safe). |
74
+ | `engageIdentify(identity, consent?)` | Forward an `identify` call (SSR-safe). |
75
+
76
+ ### `<EngageProvider>` props
77
+
78
+ | Prop | Type | Default | |
79
+ | --- | --- | --- | --- |
80
+ | `publicKey` | `string` | — | Source public key (`pk_...`). |
81
+ | `host` | `string` | — | Tracking host serving `engage.v1.js`. |
82
+ | `autoPageview` | `boolean` | `true` | Auto-fire `page.viewed` on route change. |
83
+ | `branchId` | `number \| string` | — | Attached to every event. |
84
+
85
+ ## License
86
+
87
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,131 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ EngageProvider: () => EngageProvider,
25
+ engageIdentify: () => engageIdentify,
26
+ sendEngageEvent: () => sendEngageEvent
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/provider.tsx
31
+ var import_react = require("react");
32
+ var import_jsx_runtime = require("react/jsx-runtime");
33
+ var STUB_METHODS = ["init", "track", "identify", "flush"];
34
+ var NAV_EVENT = "engagex:navigation";
35
+ function EngageProvider({
36
+ publicKey,
37
+ host,
38
+ autoPageview = true,
39
+ branchId,
40
+ children
41
+ }) {
42
+ (0, import_react.useEffect)(() => {
43
+ if (typeof window === "undefined" || !publicKey || !host) return;
44
+ const w = window;
45
+ const base = host.replace(/\/+$/, "");
46
+ if (!w.__engagex_loaded) {
47
+ w.__engagex_loaded = true;
48
+ const name = "engage";
49
+ if (!w[name]) {
50
+ w[name] = { _q: [] };
51
+ STUB_METHODS.forEach((m) => {
52
+ w[name][m] = function() {
53
+ w[name]._q.push([m, arguments]);
54
+ };
55
+ });
56
+ }
57
+ const el = document.createElement("script");
58
+ el.async = true;
59
+ el.src = base + "/engage.v1.js";
60
+ const first = document.getElementsByTagName("script")[0];
61
+ if (first && first.parentNode) first.parentNode.insertBefore(el, first);
62
+ else document.head.appendChild(el);
63
+ w[name].init({ key: publicKey, endpoint: base, autoPageview: false, branch_id: branchId });
64
+ }
65
+ if (!autoPageview) return;
66
+ const fire = () => {
67
+ try {
68
+ w.engage?.track("page.viewed", { url: window.location.href });
69
+ } catch {
70
+ }
71
+ };
72
+ fire();
73
+ const origPush = window.history.pushState;
74
+ const origReplace = window.history.replaceState;
75
+ let patchedHere = false;
76
+ if (!w.__engagex_history_patched) {
77
+ w.__engagex_history_patched = true;
78
+ patchedHere = true;
79
+ window.history.pushState = function(...args) {
80
+ const r = origPush.apply(this, args);
81
+ window.dispatchEvent(new Event(NAV_EVENT));
82
+ return r;
83
+ };
84
+ window.history.replaceState = function(...args) {
85
+ const r = origReplace.apply(this, args);
86
+ window.dispatchEvent(new Event(NAV_EVENT));
87
+ return r;
88
+ };
89
+ }
90
+ window.addEventListener(NAV_EVENT, fire);
91
+ window.addEventListener("popstate", fire);
92
+ return () => {
93
+ window.removeEventListener(NAV_EVENT, fire);
94
+ window.removeEventListener("popstate", fire);
95
+ if (patchedHere) {
96
+ window.history.pushState = origPush;
97
+ window.history.replaceState = origReplace;
98
+ w.__engagex_history_patched = false;
99
+ }
100
+ };
101
+ }, [publicKey, host, autoPageview, branchId]);
102
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
103
+ }
104
+
105
+ // src/engage.ts
106
+ function resolve() {
107
+ if (typeof window === "undefined") return void 0;
108
+ return window.engage;
109
+ }
110
+ function warnNotReady() {
111
+ if (typeof console !== "undefined") {
112
+ console.warn("[engagex] window.engage is not ready \u2014 did you mount <EngageProvider>?");
113
+ }
114
+ }
115
+ function sendEngageEvent(event, properties) {
116
+ const e = resolve();
117
+ if (e && typeof e.track === "function") e.track(event, properties);
118
+ else warnNotReady();
119
+ }
120
+ function engageIdentify(identity, consent) {
121
+ const e = resolve();
122
+ if (e && typeof e.identify === "function") e.identify(identity, consent);
123
+ else warnNotReady();
124
+ }
125
+ // Annotate the CommonJS export names for ESM import in node:
126
+ 0 && (module.exports = {
127
+ EngageProvider,
128
+ engageIdentify,
129
+ sendEngageEvent
130
+ });
131
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/provider.tsx","../src/engage.ts"],"sourcesContent":["export { EngageProvider } from './provider'\nexport type { EngageProviderProps } from './provider'\n\nexport { sendEngageEvent, engageIdentify } from './engage'\nexport type { EngageIdentity, EngageConsent, EngageGlobal } from './engage'\n","import { useEffect } from 'react'\n\nexport interface EngageProviderProps {\n /** Your source public key, e.g. \"pk_live_...\". */\n publicKey: string\n /** Tracking host origin that serves engage.v1.js, e.g. \"https://engage.data360.asia\". */\n host: string\n /**\n * Auto-fire `page.viewed` on SPA route changes (default true). Works in Next\n * App/Pages Router and React Router without importing them — it hooks the\n * History API. Disable to fire page views yourself.\n */\n autoPageview?: boolean\n /** Optional branch id attached to every event. */\n branchId?: number | string\n children?: React.ReactNode\n}\n\nconst STUB_METHODS = ['init', 'track', 'identify', 'flush'] as const\nconst NAV_EVENT = 'engagex:navigation'\n\n/**\n * Mount once near the root of your app.\n *\n * // app/layout.tsx (Next.js) — it's a client component\n * <EngageProvider publicKey=\"pk_...\" host=\"https://engage.data360.asia\" />\n *\n * It injects engage.v1.js from `host` (auto-updates, keeps endpoint auto-detect\n * and shared _cdp_tid identity) and tracks page views across client navigation.\n */\nexport function EngageProvider({\n publicKey,\n host,\n autoPageview = true,\n branchId,\n children,\n}: EngageProviderProps) {\n useEffect(() => {\n if (typeof window === 'undefined' || !publicKey || !host) return\n const w = window as unknown as Record<string, any>\n const base = host.replace(/\\/+$/, '')\n\n // 1) Install the async-loader stub + inject engage.v1.js exactly once.\n if (!w.__engagex_loaded) {\n w.__engagex_loaded = true\n const name = 'engage'\n if (!w[name]) {\n w[name] = { _q: [] as unknown[] }\n STUB_METHODS.forEach((m) => {\n w[name][m] = function () {\n // eslint-disable-next-line prefer-rest-params\n w[name]._q.push([m, arguments])\n }\n })\n }\n const el = document.createElement('script')\n el.async = true\n el.src = base + '/engage.v1.js'\n const first = document.getElementsByTagName('script')[0]\n if (first && first.parentNode) first.parentNode.insertBefore(el, first)\n else document.head.appendChild(el)\n // autoPageview:false at SDK level — page views are handled here so SPA\n // route changes are captured (SDK only auto-fires once on init).\n w[name].init({ key: publicKey, endpoint: base, autoPageview: false, branch_id: branchId })\n }\n\n if (!autoPageview) return\n\n const fire = () => {\n try {\n w.engage?.track('page.viewed', { url: window.location.href })\n } catch {\n /* no-op */\n }\n }\n fire() // initial view\n\n // 2) Framework-agnostic SPA route tracking: patch History API once, then\n // listen for our synthetic nav event + popstate.\n const origPush = window.history.pushState\n const origReplace = window.history.replaceState\n let patchedHere = false\n if (!w.__engagex_history_patched) {\n w.__engagex_history_patched = true\n patchedHere = true\n window.history.pushState = function (this: History, ...args: unknown[]) {\n const r = origPush.apply(this, args as [unknown, string, (string | URL | null)?])\n window.dispatchEvent(new Event(NAV_EVENT))\n return r\n }\n window.history.replaceState = function (this: History, ...args: unknown[]) {\n const r = origReplace.apply(this, args as [unknown, string, (string | URL | null)?])\n window.dispatchEvent(new Event(NAV_EVENT))\n return r\n }\n }\n window.addEventListener(NAV_EVENT, fire)\n window.addEventListener('popstate', fire)\n\n return () => {\n window.removeEventListener(NAV_EVENT, fire)\n window.removeEventListener('popstate', fire)\n if (patchedHere) {\n window.history.pushState = origPush\n window.history.replaceState = origReplace\n w.__engagex_history_patched = false\n }\n }\n }, [publicKey, host, autoPageview, branchId])\n\n return <>{children}</>\n}\n","/**\n * Typed helpers to call the EngageX browser SDK (window.engage) safely from\n * React / Next.js. All calls are SSR-guarded — a no-op on the server.\n *\n * The actual tracking logic lives in engage.v1.js served from your tracking\n * host; <EngageProvider> injects it. These helpers just forward to it.\n */\n\nexport interface EngageIdentity {\n /** At least one resolving key is required: phone / email / zalo_id. */\n phone?: string\n email?: string\n zalo_id?: string\n /** Optional traits — applied fill-missing to the customer profile. */\n name?: string\n /** male | female | other (normalized server-side from \"Nam\"/\"Nữ\"...). */\n gender?: string\n /** e.g. 1990-12-31 or 31/12/1990. */\n birthday?: string\n address?: string\n [key: string]: unknown\n}\n\nexport interface EngageConsent {\n consent: boolean\n purpose?: string\n policy_version?: string\n}\n\nexport interface EngageGlobal {\n init: (opts: Record<string, unknown>) => void\n track: (event: string, properties?: Record<string, unknown>) => void\n identify: (identity: EngageIdentity, consent?: EngageConsent) => void\n flush?: (useBeacon?: boolean) => void\n anonId?: () => string\n _q?: unknown[]\n}\n\ndeclare global {\n interface Window {\n engage?: EngageGlobal\n }\n}\n\nfunction resolve(): EngageGlobal | undefined {\n if (typeof window === 'undefined') return undefined\n return window.engage\n}\n\nfunction warnNotReady(): void {\n if (typeof console !== 'undefined') {\n console.warn('[engagex] window.engage is not ready — did you mount <EngageProvider>?')\n }\n}\n\n/**\n * Send a business/behavioural event, e.g.\n * sendEngageEvent('order.placed', { order_id: 'DH1', total: 1990000 })\n */\nexport function sendEngageEvent(event: string, properties?: Record<string, unknown>): void {\n const e = resolve()\n if (e && typeof e.track === 'function') e.track(event, properties)\n else warnNotReady()\n}\n\n/**\n * Identify the visitor. `identity` mixes resolving keys (phone/email/zalo_id,\n * at least one) with optional traits (name/gender/birthday/address). Re-calling\n * with the same phone updates (fill-missing) — it never creates a duplicate.\n * engageIdentify({ phone: '09...', name: 'An' }, { consent: true })\n */\nexport function engageIdentify(identity: EngageIdentity, consent?: EngageConsent): void {\n const e = resolve()\n if (e && typeof e.identify === 'function') e.identify(identity, consent)\n else warnNotReady()\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,81 @@
1
+ import * as react from 'react';
2
+
3
+ interface EngageProviderProps {
4
+ /** Your source public key, e.g. "pk_live_...". */
5
+ publicKey: string;
6
+ /** Tracking host origin that serves engage.v1.js, e.g. "https://engage.data360.asia". */
7
+ host: string;
8
+ /**
9
+ * Auto-fire `page.viewed` on SPA route changes (default true). Works in Next
10
+ * App/Pages Router and React Router without importing them — it hooks the
11
+ * History API. Disable to fire page views yourself.
12
+ */
13
+ autoPageview?: boolean;
14
+ /** Optional branch id attached to every event. */
15
+ branchId?: number | string;
16
+ children?: React.ReactNode;
17
+ }
18
+ /**
19
+ * Mount once near the root of your app.
20
+ *
21
+ * // app/layout.tsx (Next.js) — it's a client component
22
+ * <EngageProvider publicKey="pk_..." host="https://engage.data360.asia" />
23
+ *
24
+ * It injects engage.v1.js from `host` (auto-updates, keeps endpoint auto-detect
25
+ * and shared _cdp_tid identity) and tracks page views across client navigation.
26
+ */
27
+ declare function EngageProvider({ publicKey, host, autoPageview, branchId, children, }: EngageProviderProps): react.JSX.Element;
28
+
29
+ /**
30
+ * Typed helpers to call the EngageX browser SDK (window.engage) safely from
31
+ * React / Next.js. All calls are SSR-guarded — a no-op on the server.
32
+ *
33
+ * The actual tracking logic lives in engage.v1.js served from your tracking
34
+ * host; <EngageProvider> injects it. These helpers just forward to it.
35
+ */
36
+ interface EngageIdentity {
37
+ /** At least one resolving key is required: phone / email / zalo_id. */
38
+ phone?: string;
39
+ email?: string;
40
+ zalo_id?: string;
41
+ /** Optional traits — applied fill-missing to the customer profile. */
42
+ name?: string;
43
+ /** male | female | other (normalized server-side from "Nam"/"Nữ"...). */
44
+ gender?: string;
45
+ /** e.g. 1990-12-31 or 31/12/1990. */
46
+ birthday?: string;
47
+ address?: string;
48
+ [key: string]: unknown;
49
+ }
50
+ interface EngageConsent {
51
+ consent: boolean;
52
+ purpose?: string;
53
+ policy_version?: string;
54
+ }
55
+ interface EngageGlobal {
56
+ init: (opts: Record<string, unknown>) => void;
57
+ track: (event: string, properties?: Record<string, unknown>) => void;
58
+ identify: (identity: EngageIdentity, consent?: EngageConsent) => void;
59
+ flush?: (useBeacon?: boolean) => void;
60
+ anonId?: () => string;
61
+ _q?: unknown[];
62
+ }
63
+ declare global {
64
+ interface Window {
65
+ engage?: EngageGlobal;
66
+ }
67
+ }
68
+ /**
69
+ * Send a business/behavioural event, e.g.
70
+ * sendEngageEvent('order.placed', { order_id: 'DH1', total: 1990000 })
71
+ */
72
+ declare function sendEngageEvent(event: string, properties?: Record<string, unknown>): void;
73
+ /**
74
+ * Identify the visitor. `identity` mixes resolving keys (phone/email/zalo_id,
75
+ * at least one) with optional traits (name/gender/birthday/address). Re-calling
76
+ * with the same phone updates (fill-missing) — it never creates a duplicate.
77
+ * engageIdentify({ phone: '09...', name: 'An' }, { consent: true })
78
+ */
79
+ declare function engageIdentify(identity: EngageIdentity, consent?: EngageConsent): void;
80
+
81
+ export { type EngageConsent, type EngageGlobal, type EngageIdentity, EngageProvider, type EngageProviderProps, engageIdentify, sendEngageEvent };
@@ -0,0 +1,81 @@
1
+ import * as react from 'react';
2
+
3
+ interface EngageProviderProps {
4
+ /** Your source public key, e.g. "pk_live_...". */
5
+ publicKey: string;
6
+ /** Tracking host origin that serves engage.v1.js, e.g. "https://engage.data360.asia". */
7
+ host: string;
8
+ /**
9
+ * Auto-fire `page.viewed` on SPA route changes (default true). Works in Next
10
+ * App/Pages Router and React Router without importing them — it hooks the
11
+ * History API. Disable to fire page views yourself.
12
+ */
13
+ autoPageview?: boolean;
14
+ /** Optional branch id attached to every event. */
15
+ branchId?: number | string;
16
+ children?: React.ReactNode;
17
+ }
18
+ /**
19
+ * Mount once near the root of your app.
20
+ *
21
+ * // app/layout.tsx (Next.js) — it's a client component
22
+ * <EngageProvider publicKey="pk_..." host="https://engage.data360.asia" />
23
+ *
24
+ * It injects engage.v1.js from `host` (auto-updates, keeps endpoint auto-detect
25
+ * and shared _cdp_tid identity) and tracks page views across client navigation.
26
+ */
27
+ declare function EngageProvider({ publicKey, host, autoPageview, branchId, children, }: EngageProviderProps): react.JSX.Element;
28
+
29
+ /**
30
+ * Typed helpers to call the EngageX browser SDK (window.engage) safely from
31
+ * React / Next.js. All calls are SSR-guarded — a no-op on the server.
32
+ *
33
+ * The actual tracking logic lives in engage.v1.js served from your tracking
34
+ * host; <EngageProvider> injects it. These helpers just forward to it.
35
+ */
36
+ interface EngageIdentity {
37
+ /** At least one resolving key is required: phone / email / zalo_id. */
38
+ phone?: string;
39
+ email?: string;
40
+ zalo_id?: string;
41
+ /** Optional traits — applied fill-missing to the customer profile. */
42
+ name?: string;
43
+ /** male | female | other (normalized server-side from "Nam"/"Nữ"...). */
44
+ gender?: string;
45
+ /** e.g. 1990-12-31 or 31/12/1990. */
46
+ birthday?: string;
47
+ address?: string;
48
+ [key: string]: unknown;
49
+ }
50
+ interface EngageConsent {
51
+ consent: boolean;
52
+ purpose?: string;
53
+ policy_version?: string;
54
+ }
55
+ interface EngageGlobal {
56
+ init: (opts: Record<string, unknown>) => void;
57
+ track: (event: string, properties?: Record<string, unknown>) => void;
58
+ identify: (identity: EngageIdentity, consent?: EngageConsent) => void;
59
+ flush?: (useBeacon?: boolean) => void;
60
+ anonId?: () => string;
61
+ _q?: unknown[];
62
+ }
63
+ declare global {
64
+ interface Window {
65
+ engage?: EngageGlobal;
66
+ }
67
+ }
68
+ /**
69
+ * Send a business/behavioural event, e.g.
70
+ * sendEngageEvent('order.placed', { order_id: 'DH1', total: 1990000 })
71
+ */
72
+ declare function sendEngageEvent(event: string, properties?: Record<string, unknown>): void;
73
+ /**
74
+ * Identify the visitor. `identity` mixes resolving keys (phone/email/zalo_id,
75
+ * at least one) with optional traits (name/gender/birthday/address). Re-calling
76
+ * with the same phone updates (fill-missing) — it never creates a duplicate.
77
+ * engageIdentify({ phone: '09...', name: 'An' }, { consent: true })
78
+ */
79
+ declare function engageIdentify(identity: EngageIdentity, consent?: EngageConsent): void;
80
+
81
+ export { type EngageConsent, type EngageGlobal, type EngageIdentity, EngageProvider, type EngageProviderProps, engageIdentify, sendEngageEvent };
package/dist/index.js ADDED
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ // src/provider.tsx
4
+ import { useEffect } from "react";
5
+ import { Fragment, jsx } from "react/jsx-runtime";
6
+ var STUB_METHODS = ["init", "track", "identify", "flush"];
7
+ var NAV_EVENT = "engagex:navigation";
8
+ function EngageProvider({
9
+ publicKey,
10
+ host,
11
+ autoPageview = true,
12
+ branchId,
13
+ children
14
+ }) {
15
+ useEffect(() => {
16
+ if (typeof window === "undefined" || !publicKey || !host) return;
17
+ const w = window;
18
+ const base = host.replace(/\/+$/, "");
19
+ if (!w.__engagex_loaded) {
20
+ w.__engagex_loaded = true;
21
+ const name = "engage";
22
+ if (!w[name]) {
23
+ w[name] = { _q: [] };
24
+ STUB_METHODS.forEach((m) => {
25
+ w[name][m] = function() {
26
+ w[name]._q.push([m, arguments]);
27
+ };
28
+ });
29
+ }
30
+ const el = document.createElement("script");
31
+ el.async = true;
32
+ el.src = base + "/engage.v1.js";
33
+ const first = document.getElementsByTagName("script")[0];
34
+ if (first && first.parentNode) first.parentNode.insertBefore(el, first);
35
+ else document.head.appendChild(el);
36
+ w[name].init({ key: publicKey, endpoint: base, autoPageview: false, branch_id: branchId });
37
+ }
38
+ if (!autoPageview) return;
39
+ const fire = () => {
40
+ try {
41
+ w.engage?.track("page.viewed", { url: window.location.href });
42
+ } catch {
43
+ }
44
+ };
45
+ fire();
46
+ const origPush = window.history.pushState;
47
+ const origReplace = window.history.replaceState;
48
+ let patchedHere = false;
49
+ if (!w.__engagex_history_patched) {
50
+ w.__engagex_history_patched = true;
51
+ patchedHere = true;
52
+ window.history.pushState = function(...args) {
53
+ const r = origPush.apply(this, args);
54
+ window.dispatchEvent(new Event(NAV_EVENT));
55
+ return r;
56
+ };
57
+ window.history.replaceState = function(...args) {
58
+ const r = origReplace.apply(this, args);
59
+ window.dispatchEvent(new Event(NAV_EVENT));
60
+ return r;
61
+ };
62
+ }
63
+ window.addEventListener(NAV_EVENT, fire);
64
+ window.addEventListener("popstate", fire);
65
+ return () => {
66
+ window.removeEventListener(NAV_EVENT, fire);
67
+ window.removeEventListener("popstate", fire);
68
+ if (patchedHere) {
69
+ window.history.pushState = origPush;
70
+ window.history.replaceState = origReplace;
71
+ w.__engagex_history_patched = false;
72
+ }
73
+ };
74
+ }, [publicKey, host, autoPageview, branchId]);
75
+ return /* @__PURE__ */ jsx(Fragment, { children });
76
+ }
77
+
78
+ // src/engage.ts
79
+ function resolve() {
80
+ if (typeof window === "undefined") return void 0;
81
+ return window.engage;
82
+ }
83
+ function warnNotReady() {
84
+ if (typeof console !== "undefined") {
85
+ console.warn("[engagex] window.engage is not ready \u2014 did you mount <EngageProvider>?");
86
+ }
87
+ }
88
+ function sendEngageEvent(event, properties) {
89
+ const e = resolve();
90
+ if (e && typeof e.track === "function") e.track(event, properties);
91
+ else warnNotReady();
92
+ }
93
+ function engageIdentify(identity, consent) {
94
+ const e = resolve();
95
+ if (e && typeof e.identify === "function") e.identify(identity, consent);
96
+ else warnNotReady();
97
+ }
98
+ export {
99
+ EngageProvider,
100
+ engageIdentify,
101
+ sendEngageEvent
102
+ };
103
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/provider.tsx","../src/engage.ts"],"sourcesContent":["import { useEffect } from 'react'\n\nexport interface EngageProviderProps {\n /** Your source public key, e.g. \"pk_live_...\". */\n publicKey: string\n /** Tracking host origin that serves engage.v1.js, e.g. \"https://engage.data360.asia\". */\n host: string\n /**\n * Auto-fire `page.viewed` on SPA route changes (default true). Works in Next\n * App/Pages Router and React Router without importing them — it hooks the\n * History API. Disable to fire page views yourself.\n */\n autoPageview?: boolean\n /** Optional branch id attached to every event. */\n branchId?: number | string\n children?: React.ReactNode\n}\n\nconst STUB_METHODS = ['init', 'track', 'identify', 'flush'] as const\nconst NAV_EVENT = 'engagex:navigation'\n\n/**\n * Mount once near the root of your app.\n *\n * // app/layout.tsx (Next.js) — it's a client component\n * <EngageProvider publicKey=\"pk_...\" host=\"https://engage.data360.asia\" />\n *\n * It injects engage.v1.js from `host` (auto-updates, keeps endpoint auto-detect\n * and shared _cdp_tid identity) and tracks page views across client navigation.\n */\nexport function EngageProvider({\n publicKey,\n host,\n autoPageview = true,\n branchId,\n children,\n}: EngageProviderProps) {\n useEffect(() => {\n if (typeof window === 'undefined' || !publicKey || !host) return\n const w = window as unknown as Record<string, any>\n const base = host.replace(/\\/+$/, '')\n\n // 1) Install the async-loader stub + inject engage.v1.js exactly once.\n if (!w.__engagex_loaded) {\n w.__engagex_loaded = true\n const name = 'engage'\n if (!w[name]) {\n w[name] = { _q: [] as unknown[] }\n STUB_METHODS.forEach((m) => {\n w[name][m] = function () {\n // eslint-disable-next-line prefer-rest-params\n w[name]._q.push([m, arguments])\n }\n })\n }\n const el = document.createElement('script')\n el.async = true\n el.src = base + '/engage.v1.js'\n const first = document.getElementsByTagName('script')[0]\n if (first && first.parentNode) first.parentNode.insertBefore(el, first)\n else document.head.appendChild(el)\n // autoPageview:false at SDK level — page views are handled here so SPA\n // route changes are captured (SDK only auto-fires once on init).\n w[name].init({ key: publicKey, endpoint: base, autoPageview: false, branch_id: branchId })\n }\n\n if (!autoPageview) return\n\n const fire = () => {\n try {\n w.engage?.track('page.viewed', { url: window.location.href })\n } catch {\n /* no-op */\n }\n }\n fire() // initial view\n\n // 2) Framework-agnostic SPA route tracking: patch History API once, then\n // listen for our synthetic nav event + popstate.\n const origPush = window.history.pushState\n const origReplace = window.history.replaceState\n let patchedHere = false\n if (!w.__engagex_history_patched) {\n w.__engagex_history_patched = true\n patchedHere = true\n window.history.pushState = function (this: History, ...args: unknown[]) {\n const r = origPush.apply(this, args as [unknown, string, (string | URL | null)?])\n window.dispatchEvent(new Event(NAV_EVENT))\n return r\n }\n window.history.replaceState = function (this: History, ...args: unknown[]) {\n const r = origReplace.apply(this, args as [unknown, string, (string | URL | null)?])\n window.dispatchEvent(new Event(NAV_EVENT))\n return r\n }\n }\n window.addEventListener(NAV_EVENT, fire)\n window.addEventListener('popstate', fire)\n\n return () => {\n window.removeEventListener(NAV_EVENT, fire)\n window.removeEventListener('popstate', fire)\n if (patchedHere) {\n window.history.pushState = origPush\n window.history.replaceState = origReplace\n w.__engagex_history_patched = false\n }\n }\n }, [publicKey, host, autoPageview, branchId])\n\n return <>{children}</>\n}\n","/**\n * Typed helpers to call the EngageX browser SDK (window.engage) safely from\n * React / Next.js. All calls are SSR-guarded — a no-op on the server.\n *\n * The actual tracking logic lives in engage.v1.js served from your tracking\n * host; <EngageProvider> injects it. These helpers just forward to it.\n */\n\nexport interface EngageIdentity {\n /** At least one resolving key is required: phone / email / zalo_id. */\n phone?: string\n email?: string\n zalo_id?: string\n /** Optional traits — applied fill-missing to the customer profile. */\n name?: string\n /** male | female | other (normalized server-side from \"Nam\"/\"Nữ\"...). */\n gender?: string\n /** e.g. 1990-12-31 or 31/12/1990. */\n birthday?: string\n address?: string\n [key: string]: unknown\n}\n\nexport interface EngageConsent {\n consent: boolean\n purpose?: string\n policy_version?: string\n}\n\nexport interface EngageGlobal {\n init: (opts: Record<string, unknown>) => void\n track: (event: string, properties?: Record<string, unknown>) => void\n identify: (identity: EngageIdentity, consent?: EngageConsent) => void\n flush?: (useBeacon?: boolean) => void\n anonId?: () => string\n _q?: unknown[]\n}\n\ndeclare global {\n interface Window {\n engage?: EngageGlobal\n }\n}\n\nfunction resolve(): EngageGlobal | undefined {\n if (typeof window === 'undefined') return undefined\n return window.engage\n}\n\nfunction warnNotReady(): void {\n if (typeof console !== 'undefined') {\n console.warn('[engagex] window.engage is not ready — did you mount <EngageProvider>?')\n }\n}\n\n/**\n * Send a business/behavioural event, e.g.\n * sendEngageEvent('order.placed', { order_id: 'DH1', total: 1990000 })\n */\nexport function sendEngageEvent(event: string, properties?: Record<string, unknown>): void {\n const e = resolve()\n if (e && typeof e.track === 'function') e.track(event, properties)\n else warnNotReady()\n}\n\n/**\n * Identify the visitor. `identity` mixes resolving keys (phone/email/zalo_id,\n * at least one) with optional traits (name/gender/birthday/address). Re-calling\n * with the same phone updates (fill-missing) — it never creates a duplicate.\n * engageIdentify({ phone: '09...', name: 'An' }, { consent: true })\n */\nexport function engageIdentify(identity: EngageIdentity, consent?: EngageConsent): void {\n const e = resolve()\n if (e && typeof e.identify === 'function') e.identify(identity, consent)\n else warnNotReady()\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@engagex/react",
3
+ "version": "0.1.0",
4
+ "description": "React / Next.js wrapper for EngageX tracking. Loads engage.v1.js from your tracking host and gives you a typed <EngageProvider>, sendEngageEvent() and engageIdentify() with automatic SPA page-view tracking.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "prepublishOnly": "npm run build"
25
+ },
26
+ "peerDependencies": {
27
+ "react": ">=18"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^18.3.12",
31
+ "react": "^18.3.1",
32
+ "tsup": "^8.3.5",
33
+ "typescript": "^5.6.3"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "keywords": [
39
+ "engagex",
40
+ "tracking",
41
+ "analytics",
42
+ "cdp",
43
+ "react",
44
+ "nextjs",
45
+ "next"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18"
49
+ }
50
+ }