@apex-inc/react 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -0
- package/dist/ApexPreferenceCenter.d.ts +40 -0
- package/dist/ApexPreferenceCenter.d.ts.map +1 -0
- package/dist/ApexPreferenceCenter.js +129 -0
- package/dist/ApexPreferenceCenter.js.map +1 -0
- package/dist/esm/ApexPreferenceCenter.js +125 -0
- package/dist/esm/experiments.js +200 -0
- package/dist/esm/index.js +21 -0
- package/dist/esm/types.js +8 -0
- package/dist/esm/useApexPreferences.js +189 -0
- package/dist/experiments.d.ts +50 -0
- package/dist/experiments.d.ts.map +1 -0
- package/dist/experiments.js +207 -0
- package/dist/experiments.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/useApexPreferences.d.ts +48 -0
- package/dist/useApexPreferences.d.ts.map +1 -0
- package/dist/useApexPreferences.js +193 -0
- package/dist/useApexPreferences.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useApexPreferences` — headless data hook backing the
|
|
3
|
+
* `<ApexPreferenceCenter>` component.
|
|
4
|
+
*
|
|
5
|
+
* Customers with their own design system import this hook directly
|
|
6
|
+
* and render their own controls. The default `<ApexPreferenceCenter>`
|
|
7
|
+
* built on top of this hook ships as the "drop a tag, get a working
|
|
8
|
+
* surface" path; the hook is the "I'll style it myself" path.
|
|
9
|
+
*
|
|
10
|
+
* The hook never throws — all errors land on `state.error` so the
|
|
11
|
+
* caller can render an inline message instead of an error boundary
|
|
12
|
+
* fallback. This matches the dashboard's own preference page UX.
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useState } from "react";
|
|
15
|
+
const DEFAULT_API = "https://app.apex.inc";
|
|
16
|
+
function decodeHmacEuid(token) {
|
|
17
|
+
// The Apex token shape is `<base64url-payload>.<base64url-sig>`.
|
|
18
|
+
// We only read the payload; the server re-verifies the signature
|
|
19
|
+
// on every call, so we never trust the decoded values on the
|
|
20
|
+
// client beyond using them as fetch parameters.
|
|
21
|
+
const parts = token.split(".");
|
|
22
|
+
if (parts.length !== 2)
|
|
23
|
+
return {};
|
|
24
|
+
try {
|
|
25
|
+
const padded = parts[0]
|
|
26
|
+
.replace(/-/g, "+")
|
|
27
|
+
.replace(/_/g, "/")
|
|
28
|
+
.padEnd(parts[0].length + ((4 - (parts[0].length % 4)) % 4), "=");
|
|
29
|
+
const json = atob(padded);
|
|
30
|
+
const obj = JSON.parse(json);
|
|
31
|
+
return { euid: obj.euid, pk: obj.pk };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function useApexPreferences(options) {
|
|
38
|
+
const apiBaseUrl = (options.apiBaseUrl ?? DEFAULT_API).replace(/\/$/, "");
|
|
39
|
+
// Resolve identity once. With token auth we crack open the payload
|
|
40
|
+
// to read the workspaceKey + endUserId; with cookie auth we require
|
|
41
|
+
// both as explicit props.
|
|
42
|
+
const tokenAuth = "token" in options.endUserAuth ? options.endUserAuth : null;
|
|
43
|
+
const decoded = tokenAuth ? decodeHmacEuid(tokenAuth.token) : null;
|
|
44
|
+
const endUserId = options.endUserId ?? decoded?.euid;
|
|
45
|
+
const workspaceKey = options.workspaceKey ?? decoded?.pk;
|
|
46
|
+
const [state, setState] = useState({
|
|
47
|
+
prefs: null,
|
|
48
|
+
loading: true,
|
|
49
|
+
saving: false,
|
|
50
|
+
saved: false,
|
|
51
|
+
error: null,
|
|
52
|
+
});
|
|
53
|
+
const authHeaders = useCallback(() => {
|
|
54
|
+
const headers = {
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
};
|
|
57
|
+
if (tokenAuth) {
|
|
58
|
+
headers["Authorization"] = `Bearer ${tokenAuth.token}`;
|
|
59
|
+
}
|
|
60
|
+
if (workspaceKey) {
|
|
61
|
+
headers["x-workspace-key"] = workspaceKey;
|
|
62
|
+
}
|
|
63
|
+
return headers;
|
|
64
|
+
}, [tokenAuth, workspaceKey]);
|
|
65
|
+
const fetchOpts = useCallback(() => {
|
|
66
|
+
const init = { headers: authHeaders() };
|
|
67
|
+
if (!tokenAuth) {
|
|
68
|
+
// Cookie-auth callers need credentials forwarded to pick up the
|
|
69
|
+
// Apex dashboard's `apex_session` cookie.
|
|
70
|
+
init.credentials = "include";
|
|
71
|
+
}
|
|
72
|
+
return init;
|
|
73
|
+
}, [authHeaders, tokenAuth]);
|
|
74
|
+
const refresh = useCallback(async () => {
|
|
75
|
+
if (!endUserId) {
|
|
76
|
+
setState((s) => ({
|
|
77
|
+
...s,
|
|
78
|
+
loading: false,
|
|
79
|
+
error: "endUserId not available — pass it explicitly or use a token-based auth.",
|
|
80
|
+
}));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(`/api/communications/preferences/${encodeURIComponent(endUserId)}`, apiBaseUrl);
|
|
86
|
+
if (workspaceKey)
|
|
87
|
+
url.searchParams.set("workspaceKey", workspaceKey);
|
|
88
|
+
const res = await fetch(url.toString(), fetchOpts());
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
throw new Error(`HTTP ${res.status}`);
|
|
91
|
+
}
|
|
92
|
+
const prefs = (await res.json());
|
|
93
|
+
setState((s) => ({ ...s, prefs, loading: false }));
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
setState((s) => ({
|
|
97
|
+
...s,
|
|
98
|
+
loading: false,
|
|
99
|
+
error: err instanceof Error
|
|
100
|
+
? err.message
|
|
101
|
+
: "Failed to load preferences",
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}, [apiBaseUrl, endUserId, fetchOpts, workspaceKey]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
void refresh();
|
|
107
|
+
}, [refresh]);
|
|
108
|
+
const save = useCallback(async (patch) => {
|
|
109
|
+
if (!endUserId)
|
|
110
|
+
return;
|
|
111
|
+
setState((s) => ({ ...s, saving: true, saved: false, error: null }));
|
|
112
|
+
try {
|
|
113
|
+
const url = new URL(`/api/communications/preferences/${encodeURIComponent(endUserId)}`, apiBaseUrl);
|
|
114
|
+
if (workspaceKey)
|
|
115
|
+
url.searchParams.set("workspaceKey", workspaceKey);
|
|
116
|
+
const res = await fetch(url.toString(), {
|
|
117
|
+
method: "PATCH",
|
|
118
|
+
...fetchOpts(),
|
|
119
|
+
body: JSON.stringify(patch),
|
|
120
|
+
});
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
throw new Error(`HTTP ${res.status}`);
|
|
123
|
+
}
|
|
124
|
+
const saved = (await res.json());
|
|
125
|
+
setState((s) => ({ ...s, prefs: saved, saving: false, saved: true }));
|
|
126
|
+
// Clear the "saved!" badge after 2s — matches the dashboard.
|
|
127
|
+
setTimeout(() => {
|
|
128
|
+
setState((s) => ({ ...s, saved: false }));
|
|
129
|
+
}, 2000);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
setState((s) => ({
|
|
133
|
+
...s,
|
|
134
|
+
saving: false,
|
|
135
|
+
error: err instanceof Error
|
|
136
|
+
? err.message
|
|
137
|
+
: "Failed to save preferences",
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
}, [apiBaseUrl, endUserId, fetchOpts, workspaceKey]);
|
|
141
|
+
const setGlobalOptOut = useCallback(async (value) => {
|
|
142
|
+
if (!state.prefs)
|
|
143
|
+
return;
|
|
144
|
+
const next = { ...state.prefs, globalOptOut: value };
|
|
145
|
+
setState((s) => ({ ...s, prefs: next }));
|
|
146
|
+
await save({ globalOptOut: value });
|
|
147
|
+
}, [save, state.prefs]);
|
|
148
|
+
const setChannel = useCallback(async (channel, enabled) => {
|
|
149
|
+
if (!state.prefs)
|
|
150
|
+
return;
|
|
151
|
+
// Mirror the legacy `in_app_push` alias when toggling `inbox`.
|
|
152
|
+
const channelPatch = {
|
|
153
|
+
[channel]: enabled,
|
|
154
|
+
};
|
|
155
|
+
if (channel === "inbox")
|
|
156
|
+
channelPatch.in_app_push = enabled;
|
|
157
|
+
const next = {
|
|
158
|
+
...state.prefs,
|
|
159
|
+
channelPreferences: {
|
|
160
|
+
...state.prefs.channelPreferences,
|
|
161
|
+
...channelPatch,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
setState((s) => ({ ...s, prefs: next }));
|
|
165
|
+
await save({ channelPreferences: channelPatch });
|
|
166
|
+
}, [save, state.prefs]);
|
|
167
|
+
const setCommunicationOverride = useCallback(async (communicationId, optedOut) => {
|
|
168
|
+
if (!state.prefs)
|
|
169
|
+
return;
|
|
170
|
+
const next = {
|
|
171
|
+
...state.prefs,
|
|
172
|
+
communicationOverrides: {
|
|
173
|
+
...state.prefs.communicationOverrides,
|
|
174
|
+
[communicationId]: { optedOut },
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
setState((s) => ({ ...s, prefs: next }));
|
|
178
|
+
await save({
|
|
179
|
+
communicationOverrides: { [communicationId]: { optedOut } },
|
|
180
|
+
});
|
|
181
|
+
}, [save, state.prefs]);
|
|
182
|
+
return {
|
|
183
|
+
...state,
|
|
184
|
+
refresh,
|
|
185
|
+
setGlobalOptOut,
|
|
186
|
+
setChannel,
|
|
187
|
+
setCommunicationOverride,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type Variant = "control" | "variant_b";
|
|
3
|
+
/**
|
|
4
|
+
* Resolution config. `apiBase` defaults to "" (same-origin). When set
|
|
5
|
+
* (along with `workspaceKey` for cross-origin customer apps), requests
|
|
6
|
+
* carry the `x-apex-workspace` header and target the absolute base.
|
|
7
|
+
*/
|
|
8
|
+
export interface ApexExperimentConfig {
|
|
9
|
+
/** Apex API base URL, e.g. "https://app.apex.inc". Default "" (same-origin). */
|
|
10
|
+
apiBase?: string;
|
|
11
|
+
/** Workspace key — sent as the `x-apex-workspace` header when present. */
|
|
12
|
+
workspaceKey?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Provides experiment resolution config (apiBase / workspaceKey) to
|
|
16
|
+
* every `useApexVariant` call below it. Optional — per-call overrides
|
|
17
|
+
* and same-origin defaults work without it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ApexProvider(props: {
|
|
20
|
+
apiBase?: string;
|
|
21
|
+
workspaceKey?: string;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}): JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* React hook for code-level A/B experiments.
|
|
26
|
+
*
|
|
27
|
+
* Returns "control" or "variant_b" based on:
|
|
28
|
+
* 1. Preview mode (?_apex_preview=variant_b&_apex_exp=<id>)
|
|
29
|
+
* 2. Cached assignment in localStorage
|
|
30
|
+
* 3. Server assignment (GET /api/experiments/{id}/assign?source=sdk)
|
|
31
|
+
* 4. Deterministic hash of visitorId + experimentId (fallback)
|
|
32
|
+
*
|
|
33
|
+
* Falls back to "control" on server render and on any error.
|
|
34
|
+
*
|
|
35
|
+
* Config resolution: per-call `overrides` win over `<ApexProvider>`
|
|
36
|
+
* context, which wins over the same-origin default (`apiBase: ""`).
|
|
37
|
+
*/
|
|
38
|
+
export declare function useApexVariant(experimentId: string, overrides?: ApexExperimentConfig): Variant;
|
|
39
|
+
/**
|
|
40
|
+
* Clear a cached assignment (useful when an experiment is archived/completed).
|
|
41
|
+
*/
|
|
42
|
+
export declare function clearApexVariant(experimentId: string): void;
|
|
43
|
+
/**
|
|
44
|
+
* Check if currently in Apex preview mode for a specific experiment.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useApexPreview(experimentId?: string): {
|
|
47
|
+
isPreview: boolean;
|
|
48
|
+
previewVariant: Variant | null;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=experiments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments.d.ts","sourceRoot":"","sources":["../src/experiments.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;AAE9C;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;CACrB,GAAG,GAAG,CAAC,OAAO,CAMd;AAiFD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,YAAY,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,oBAAoB,GAC/B,OAAO,CA0DT;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAI3D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,OAAO,GAAG,IAAI,CAAC;CAChC,CAaA"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
"use client";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.ApexProvider = ApexProvider;
|
|
5
|
+
exports.useApexVariant = useApexVariant;
|
|
6
|
+
exports.clearApexVariant = clearApexVariant;
|
|
7
|
+
exports.useApexPreview = useApexPreview;
|
|
8
|
+
/**
|
|
9
|
+
* Code-level A/B experiment hooks for customer React apps.
|
|
10
|
+
*
|
|
11
|
+
* This is the publishable generalization of the app-internal
|
|
12
|
+
* `useApexVariant` hook. The behavior is identical to the first-party
|
|
13
|
+
* web hook — preview params, localStorage cache, visitor cookie,
|
|
14
|
+
* assignment fetch, murmur-hash fallback, and the canonical
|
|
15
|
+
* `experiment_exposure` event (deduped per arm) — but the API base
|
|
16
|
+
* and workspace are configurable so it works from a CUSTOMER's origin,
|
|
17
|
+
* not just same-origin first-party apps.
|
|
18
|
+
*
|
|
19
|
+
* Configure once with `<ApexProvider apiBase="https://app.apex.inc"
|
|
20
|
+
* workspaceKey="ws_…">`, or pass `{ apiBase, workspaceKey }` per call.
|
|
21
|
+
* With no config the hook defaults to same-origin (`apiBase: ""`), so
|
|
22
|
+
* first-party apps keep working unchanged.
|
|
23
|
+
*/
|
|
24
|
+
const react_1 = require("react");
|
|
25
|
+
const ApexExperimentContext = (0, react_1.createContext)({});
|
|
26
|
+
/**
|
|
27
|
+
* Provides experiment resolution config (apiBase / workspaceKey) to
|
|
28
|
+
* every `useApexVariant` call below it. Optional — per-call overrides
|
|
29
|
+
* and same-origin defaults work without it.
|
|
30
|
+
*/
|
|
31
|
+
function ApexProvider(props) {
|
|
32
|
+
const value = {
|
|
33
|
+
apiBase: props.apiBase,
|
|
34
|
+
workspaceKey: props.workspaceKey,
|
|
35
|
+
};
|
|
36
|
+
return (0, react_1.createElement)(ApexExperimentContext.Provider, { value }, props.children);
|
|
37
|
+
}
|
|
38
|
+
function murmurhash3(key) {
|
|
39
|
+
let h = 0x811c9dc5;
|
|
40
|
+
for (let i = 0; i < key.length; i++) {
|
|
41
|
+
h ^= key.charCodeAt(i);
|
|
42
|
+
h = Math.imul(h, 0x01000193);
|
|
43
|
+
}
|
|
44
|
+
return (h >>> 0) % 100;
|
|
45
|
+
}
|
|
46
|
+
function getVisitorId() {
|
|
47
|
+
if (typeof window === "undefined")
|
|
48
|
+
return "";
|
|
49
|
+
const key = "apex_vid";
|
|
50
|
+
const match = document.cookie.match(new RegExp(`(?:^|; )${key}=([^;]*)`));
|
|
51
|
+
if (match)
|
|
52
|
+
return decodeURIComponent(match[1]);
|
|
53
|
+
const id = crypto.randomUUID();
|
|
54
|
+
const d = new Date();
|
|
55
|
+
d.setTime(d.getTime() + 365 * 86400000);
|
|
56
|
+
document.cookie = `${key}=${encodeURIComponent(id)};expires=${d.toUTCString()};path=/;SameSite=Lax`;
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
function normalizeBase(apiBase) {
|
|
60
|
+
return (apiBase ?? "").replace(/\/$/, "");
|
|
61
|
+
}
|
|
62
|
+
function workspaceHeaders(workspaceKey) {
|
|
63
|
+
return workspaceKey ? { "x-apex-workspace": workspaceKey } : {};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fire-once-per-arm exposure tracking. The denominator every experiment
|
|
67
|
+
* surface counts against (canonical `experiment_exposure` event). Deduped
|
|
68
|
+
* per (experiment, variant) for the page session so re-renders don't spam.
|
|
69
|
+
*/
|
|
70
|
+
const firedExposures = new Set();
|
|
71
|
+
function fireExposure(experimentId, variant, config) {
|
|
72
|
+
if (typeof window === "undefined")
|
|
73
|
+
return;
|
|
74
|
+
const key = `${experimentId}:${variant}`;
|
|
75
|
+
if (firedExposures.has(key))
|
|
76
|
+
return;
|
|
77
|
+
firedExposures.add(key);
|
|
78
|
+
const visitorId = getVisitorId();
|
|
79
|
+
const base = normalizeBase(config.apiBase);
|
|
80
|
+
try {
|
|
81
|
+
void fetch(`${base}/api/events`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
...workspaceHeaders(config.workspaceKey),
|
|
86
|
+
},
|
|
87
|
+
credentials: "include",
|
|
88
|
+
keepalive: true,
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
type: "experiment_exposure",
|
|
91
|
+
visitorId,
|
|
92
|
+
url: window.location.href,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
experimentId,
|
|
95
|
+
variant,
|
|
96
|
+
data: { experiment_id: experimentId, variant_key: variant, surface: "web" },
|
|
97
|
+
}),
|
|
98
|
+
}).catch(() => { });
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* exposure tracking is best-effort */
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function getPreviewParams() {
|
|
105
|
+
if (typeof window === "undefined")
|
|
106
|
+
return null;
|
|
107
|
+
const params = new URLSearchParams(window.location.search);
|
|
108
|
+
const variant = params.get("_apex_preview");
|
|
109
|
+
const expId = params.get("_apex_exp");
|
|
110
|
+
if (variant)
|
|
111
|
+
return { variant, expId };
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* React hook for code-level A/B experiments.
|
|
116
|
+
*
|
|
117
|
+
* Returns "control" or "variant_b" based on:
|
|
118
|
+
* 1. Preview mode (?_apex_preview=variant_b&_apex_exp=<id>)
|
|
119
|
+
* 2. Cached assignment in localStorage
|
|
120
|
+
* 3. Server assignment (GET /api/experiments/{id}/assign?source=sdk)
|
|
121
|
+
* 4. Deterministic hash of visitorId + experimentId (fallback)
|
|
122
|
+
*
|
|
123
|
+
* Falls back to "control" on server render and on any error.
|
|
124
|
+
*
|
|
125
|
+
* Config resolution: per-call `overrides` win over `<ApexProvider>`
|
|
126
|
+
* context, which wins over the same-origin default (`apiBase: ""`).
|
|
127
|
+
*/
|
|
128
|
+
function useApexVariant(experimentId, overrides) {
|
|
129
|
+
const ctx = (0, react_1.useContext)(ApexExperimentContext);
|
|
130
|
+
const apiBase = overrides?.apiBase ?? ctx.apiBase ?? "";
|
|
131
|
+
const workspaceKey = overrides?.workspaceKey ?? ctx.workspaceKey;
|
|
132
|
+
const config = { apiBase, workspaceKey };
|
|
133
|
+
const [variant, setVariant] = (0, react_1.useState)(() => {
|
|
134
|
+
const preview = getPreviewParams();
|
|
135
|
+
if (preview && (!preview.expId || preview.expId === experimentId)) {
|
|
136
|
+
return preview.variant;
|
|
137
|
+
}
|
|
138
|
+
if (typeof window === "undefined")
|
|
139
|
+
return "control";
|
|
140
|
+
const cached = localStorage.getItem(`apex_var_${experimentId}`);
|
|
141
|
+
if (cached === "control" || cached === "variant_b")
|
|
142
|
+
return cached;
|
|
143
|
+
return "control";
|
|
144
|
+
});
|
|
145
|
+
(0, react_1.useEffect)(() => {
|
|
146
|
+
// Preview mode is the author previewing — not a real exposure.
|
|
147
|
+
const preview = getPreviewParams();
|
|
148
|
+
if (preview && (!preview.expId || preview.expId === experimentId))
|
|
149
|
+
return;
|
|
150
|
+
const cacheKey = `apex_var_${experimentId}`;
|
|
151
|
+
const cached = localStorage.getItem(cacheKey);
|
|
152
|
+
if (cached === "control" || cached === "variant_b") {
|
|
153
|
+
// Already assigned this session — still count the exposure (deduped).
|
|
154
|
+
fireExposure(experimentId, cached, config);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const visitorId = getVisitorId();
|
|
158
|
+
if (!visitorId)
|
|
159
|
+
return;
|
|
160
|
+
const base = normalizeBase(apiBase);
|
|
161
|
+
fetch(`${base}/api/experiments/${experimentId}/assign?source=sdk`, {
|
|
162
|
+
credentials: "include",
|
|
163
|
+
headers: workspaceHeaders(workspaceKey),
|
|
164
|
+
})
|
|
165
|
+
.then((r) => {
|
|
166
|
+
if (!r.ok)
|
|
167
|
+
throw new Error(`${r.status}`);
|
|
168
|
+
return r.json();
|
|
169
|
+
})
|
|
170
|
+
.then((data) => {
|
|
171
|
+
localStorage.setItem(cacheKey, data.variant);
|
|
172
|
+
setVariant(data.variant);
|
|
173
|
+
fireExposure(experimentId, data.variant, config);
|
|
174
|
+
})
|
|
175
|
+
.catch(() => {
|
|
176
|
+
const bucket = murmurhash3(visitorId + experimentId);
|
|
177
|
+
const fallback = bucket < 50 ? "control" : "variant_b";
|
|
178
|
+
localStorage.setItem(cacheKey, fallback);
|
|
179
|
+
setVariant(fallback);
|
|
180
|
+
fireExposure(experimentId, fallback, config);
|
|
181
|
+
});
|
|
182
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
+
}, [experimentId, apiBase, workspaceKey]);
|
|
184
|
+
return variant;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Clear a cached assignment (useful when an experiment is archived/completed).
|
|
188
|
+
*/
|
|
189
|
+
function clearApexVariant(experimentId) {
|
|
190
|
+
if (typeof window !== "undefined") {
|
|
191
|
+
localStorage.removeItem(`apex_var_${experimentId}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Check if currently in Apex preview mode for a specific experiment.
|
|
196
|
+
*/
|
|
197
|
+
function useApexPreview(experimentId) {
|
|
198
|
+
const [state] = (0, react_1.useState)(() => {
|
|
199
|
+
const preview = getPreviewParams();
|
|
200
|
+
if (preview && (!experimentId || !preview.expId || preview.expId === experimentId)) {
|
|
201
|
+
return { isPreview: true, previewVariant: preview.variant };
|
|
202
|
+
}
|
|
203
|
+
return { isPreview: false, previewVariant: null };
|
|
204
|
+
});
|
|
205
|
+
return state;
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=experiments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"experiments.js","sourceRoot":"","sources":["../src/experiments.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @apex-inc/react — embeddable React components for Apex
|
|
3
|
+
* communication surfaces.
|
|
4
|
+
*
|
|
5
|
+
* MVP scope (v0.1):
|
|
6
|
+
* - <ApexPreferenceCenter> — styled + headless variants.
|
|
7
|
+
* - useApexPreferences — headless hook for full-custom UIs.
|
|
8
|
+
*
|
|
9
|
+
* Deferred to v0.2 (gated on design-partner demand):
|
|
10
|
+
* - <ApexInbox> embeddable bell + drawer
|
|
11
|
+
* - useApexWebPushSubscription
|
|
12
|
+
* - Customer service-worker template
|
|
13
|
+
*
|
|
14
|
+
* Design rationale: every product on the planet needs a preferences
|
|
15
|
+
* UI; only some need an embedded inbox. Shipping the universal piece
|
|
16
|
+
* first lets us validate demand for the channel-specific pieces.
|
|
17
|
+
*/
|
|
18
|
+
export { ApexPreferenceCenter } from "./ApexPreferenceCenter";
|
|
19
|
+
export type { ApexPreferenceCenterProps } from "./ApexPreferenceCenter";
|
|
20
|
+
export { useApexPreferences, type UseApexPreferencesOptions, type UseApexPreferencesState, type UseApexPreferencesActions, type UseApexPreferencesResult, } from "./useApexPreferences";
|
|
21
|
+
export type { ApexChannel, ApexChannelRead, ApexCommPreferences, ApexEndUserAuth, ApexThemeTokens, } from "./types";
|
|
22
|
+
export { ApexProvider, useApexVariant, useApexPreview, clearApexVariant, type Variant, type ApexExperimentConfig, } from "./experiments";
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAC9D,YAAY,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,eAAe,GAChB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,YAAY,EACZ,cAAc,EACd,cAAc,EACd,gBAAgB,EAChB,KAAK,OAAO,EACZ,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @apex-inc/react — embeddable React components for Apex
|
|
4
|
+
* communication surfaces.
|
|
5
|
+
*
|
|
6
|
+
* MVP scope (v0.1):
|
|
7
|
+
* - <ApexPreferenceCenter> — styled + headless variants.
|
|
8
|
+
* - useApexPreferences — headless hook for full-custom UIs.
|
|
9
|
+
*
|
|
10
|
+
* Deferred to v0.2 (gated on design-partner demand):
|
|
11
|
+
* - <ApexInbox> embeddable bell + drawer
|
|
12
|
+
* - useApexWebPushSubscription
|
|
13
|
+
* - Customer service-worker template
|
|
14
|
+
*
|
|
15
|
+
* Design rationale: every product on the planet needs a preferences
|
|
16
|
+
* UI; only some need an embedded inbox. Shipping the universal piece
|
|
17
|
+
* first lets us validate demand for the channel-specific pieces.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.clearApexVariant = exports.useApexPreview = exports.useApexVariant = exports.ApexProvider = exports.useApexPreferences = exports.ApexPreferenceCenter = void 0;
|
|
21
|
+
var ApexPreferenceCenter_1 = require("./ApexPreferenceCenter");
|
|
22
|
+
Object.defineProperty(exports, "ApexPreferenceCenter", { enumerable: true, get: function () { return ApexPreferenceCenter_1.ApexPreferenceCenter; } });
|
|
23
|
+
var useApexPreferences_1 = require("./useApexPreferences");
|
|
24
|
+
Object.defineProperty(exports, "useApexPreferences", { enumerable: true, get: function () { return useApexPreferences_1.useApexPreferences; } });
|
|
25
|
+
// ─── Code-level A/B experiments ───────────────────────────────────────────
|
|
26
|
+
var experiments_1 = require("./experiments");
|
|
27
|
+
Object.defineProperty(exports, "ApexProvider", { enumerable: true, get: function () { return experiments_1.ApexProvider; } });
|
|
28
|
+
Object.defineProperty(exports, "useApexVariant", { enumerable: true, get: function () { return experiments_1.useApexVariant; } });
|
|
29
|
+
Object.defineProperty(exports, "useApexPreview", { enumerable: true, get: function () { return experiments_1.useApexPreview; } });
|
|
30
|
+
Object.defineProperty(exports, "clearApexVariant", { enumerable: true, get: function () { return experiments_1.clearApexVariant; } });
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;AAEH,+DAA8D;AAArD,4HAAA,oBAAoB,OAAA;AAE7B,2DAM8B;AAL5B,wHAAA,kBAAkB,OAAA;AAcpB,6EAA6E;AAC7E,6CAOuB;AANrB,2GAAA,YAAY,OAAA;AACZ,6GAAA,cAAc,OAAA;AACd,6GAAA,cAAc,OAAA;AACd,+GAAA,gBAAgB,OAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the @apex-inc/react package.
|
|
3
|
+
*
|
|
4
|
+
* Intentionally mirrors `EndUserCommPreferences` from the app source
|
|
5
|
+
* tree without importing it — the package has no dependency on the
|
|
6
|
+
* app workspace.
|
|
7
|
+
*/
|
|
8
|
+
export type ApexChannel = "email" | "inbox" | "web_push" | "mobile_push";
|
|
9
|
+
/**
|
|
10
|
+
* The legacy `in_app_push` channel still appears on records authored
|
|
11
|
+
* before DFC-007. Treated as an alias for `inbox` on read.
|
|
12
|
+
*/
|
|
13
|
+
export type ApexChannelRead = ApexChannel | "in_app_push";
|
|
14
|
+
export interface ApexCommPreferences {
|
|
15
|
+
endUserId: string;
|
|
16
|
+
workspaceKey: string;
|
|
17
|
+
globalOptOut: boolean;
|
|
18
|
+
channelPreferences: Partial<Record<ApexChannelRead, boolean>>;
|
|
19
|
+
communicationOverrides: Record<string, {
|
|
20
|
+
optedOut: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
journeyOverrides?: Record<string, {
|
|
23
|
+
optedOut: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
updatedAt: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Auth credentials for the embedded preference center.
|
|
29
|
+
*
|
|
30
|
+
* Production integrations always pass `{ token }` — an Apex-signed
|
|
31
|
+
* HMAC token minted server-side via `apex.signEndUserToken(...)` (or
|
|
32
|
+
* the matching helper in the customer's preferred Apex SDK). The
|
|
33
|
+
* token carries `pk` (workspaceKey) and `euid` (endUserId), so the
|
|
34
|
+
* component doesn't need either prop separately.
|
|
35
|
+
*
|
|
36
|
+
* `{ sessionCookie: true }` is reserved for the Apex dashboard's own
|
|
37
|
+
* dogfood path — same-origin callers (us.apex.inc) can rely on the
|
|
38
|
+
* dashboard's Cognito session cookie. Customer apps embedding this
|
|
39
|
+
* component on their own domain must use a token.
|
|
40
|
+
*/
|
|
41
|
+
export type ApexEndUserAuth = {
|
|
42
|
+
token: string;
|
|
43
|
+
} | {
|
|
44
|
+
sessionCookie: true;
|
|
45
|
+
};
|
|
46
|
+
export interface ApexThemeTokens {
|
|
47
|
+
/** Accent colour for the active checkbox. Defaults to Apex green (`#00BE7D`). */
|
|
48
|
+
accentColor?: string;
|
|
49
|
+
/** Background colour for the surrounding container. */
|
|
50
|
+
backgroundColor?: string;
|
|
51
|
+
/** Foreground text colour. */
|
|
52
|
+
textColor?: string;
|
|
53
|
+
/** Optional border radius in px. */
|
|
54
|
+
borderRadius?: number;
|
|
55
|
+
/** Font stack. */
|
|
56
|
+
fontFamily?: string;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;AAEzE;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG,aAAa,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC;IAC9D,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjB;IAAE,aAAa,EAAE,IAAI,CAAA;CAAE,CAAC;AAE5B,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8BAA8B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for the @apex-inc/react package.
|
|
4
|
+
*
|
|
5
|
+
* Intentionally mirrors `EndUserCommPreferences` from the app source
|
|
6
|
+
* tree without importing it — the package has no dependency on the
|
|
7
|
+
* app workspace.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `useApexPreferences` — headless data hook backing the
|
|
3
|
+
* `<ApexPreferenceCenter>` component.
|
|
4
|
+
*
|
|
5
|
+
* Customers with their own design system import this hook directly
|
|
6
|
+
* and render their own controls. The default `<ApexPreferenceCenter>`
|
|
7
|
+
* built on top of this hook ships as the "drop a tag, get a working
|
|
8
|
+
* surface" path; the hook is the "I'll style it myself" path.
|
|
9
|
+
*
|
|
10
|
+
* The hook never throws — all errors land on `state.error` so the
|
|
11
|
+
* caller can render an inline message instead of an error boundary
|
|
12
|
+
* fallback. This matches the dashboard's own preference page UX.
|
|
13
|
+
*/
|
|
14
|
+
import type { ApexChannel, ApexCommPreferences, ApexEndUserAuth } from "./types";
|
|
15
|
+
export interface UseApexPreferencesOptions {
|
|
16
|
+
/** Base URL of the Apex API. Defaults to `https://app.apex.inc`. */
|
|
17
|
+
apiBaseUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Workspace identifier. Required for `{ sessionCookie }` auth; for
|
|
20
|
+
* `{ token }` auth the value comes from the token's `pk` claim and
|
|
21
|
+
* passing it explicitly is optional.
|
|
22
|
+
*/
|
|
23
|
+
workspaceKey?: string;
|
|
24
|
+
/**
|
|
25
|
+
* End-user identifier (their id in your system). Required for
|
|
26
|
+
* `{ sessionCookie }` auth; with `{ token }` auth the helper uses
|
|
27
|
+
* the token's `euid` claim.
|
|
28
|
+
*/
|
|
29
|
+
endUserId?: string;
|
|
30
|
+
/** Auth credentials. See `ApexEndUserAuth` for the two shapes. */
|
|
31
|
+
endUserAuth: ApexEndUserAuth;
|
|
32
|
+
}
|
|
33
|
+
export interface UseApexPreferencesState {
|
|
34
|
+
prefs: ApexCommPreferences | null;
|
|
35
|
+
loading: boolean;
|
|
36
|
+
saving: boolean;
|
|
37
|
+
saved: boolean;
|
|
38
|
+
error: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface UseApexPreferencesActions {
|
|
41
|
+
refresh: () => Promise<void>;
|
|
42
|
+
setGlobalOptOut: (value: boolean) => Promise<void>;
|
|
43
|
+
setChannel: (channel: ApexChannel, enabled: boolean) => Promise<void>;
|
|
44
|
+
setCommunicationOverride: (communicationId: string, optedOut: boolean) => Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export type UseApexPreferencesResult = UseApexPreferencesState & UseApexPreferencesActions;
|
|
47
|
+
export declare function useApexPreferences(options: UseApexPreferencesOptions): UseApexPreferencesResult;
|
|
48
|
+
//# sourceMappingURL=useApexPreferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useApexPreferences.d.ts","sourceRoot":"","sources":["../src/useApexPreferences.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EAChB,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,yBAAyB;IACxC,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,WAAW,EAAE,eAAe,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,eAAe,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,wBAAwB,EAAE,CACxB,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,OAAO,KACd,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB;AAED,MAAM,MAAM,wBAAwB,GAAG,uBAAuB,GAC5D,yBAAyB,CAAC;AA2B5B,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,yBAAyB,GACjC,wBAAwB,CA8K1B"}
|