personalize-connect-sdk 1.0.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,82 @@
1
+ # Personalize Connect SDK
2
+
3
+ A lightweight Next.js package that bridges Sitecore Personalize Interactive Experiences with XM Cloud component datasources at runtime. The SDK reads configuration authored by the Marketplace app, calls Personalize for a decision, and swaps component content accordingly — with zero per-component code.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install personalize-connect-sdk
9
+ ```
10
+
11
+ Peer dependency: `react` >= 18.
12
+
13
+ ## Usage
14
+
15
+ ### 1. Wrap your app with `PersonalizeProvider`
16
+
17
+ ```tsx
18
+ import { PersonalizeProvider } from "personalize-connect-sdk";
19
+
20
+ export default function RootLayout({ children }) {
21
+ return (
22
+ <PersonalizeProvider
23
+ clientKey={process.env.NEXT_PUBLIC_PERSONALIZE_CLIENT_KEY!}
24
+ pointOfSale={process.env.NEXT_PUBLIC_PERSONALIZE_POINT_OF_SALE!}
25
+ resolveDatasource={async (datasourceId) => {
26
+ // Fetch datasource fields via Experience Edge GraphQL or Layout Service
27
+ const res = await fetch(`/api/datasource/${datasourceId}`);
28
+ return res.json();
29
+ }}
30
+ >
31
+ {children}
32
+ </PersonalizeProvider>
33
+ );
34
+ }
35
+ ```
36
+
37
+ ### 2. Wrap components with `withPersonalizeConnect`
38
+
39
+ ```tsx
40
+ import { withPersonalizeConnect } from "personalize-connect-sdk";
41
+ import MyComponent from "./MyComponent";
42
+
43
+ // Config is read from props.rendering.personalizeConnect (or customize via getConfig)
44
+ export default withPersonalizeConnect(MyComponent);
45
+ ```
46
+
47
+ The HOC renders immediately with the default datasource, calls Personalize asynchronously, and re-renders with personalized content when resolved. No changes needed inside the component.
48
+
49
+ ### 3. Or use the `usePersonalizeExperience` hook
50
+
51
+ ```tsx
52
+ import { usePersonalizeExperience } from "personalize-connect-sdk";
53
+
54
+ function MyComponent({ personalizeConnect }) {
55
+ const { contentKey, resolvedFields, isLoading, error } = usePersonalizeExperience(personalizeConnect);
56
+
57
+ if (isLoading) return <Skeleton />;
58
+ return <div>{resolvedFields?.heading}</div>;
59
+ }
60
+ ```
61
+
62
+ ## Config shape
63
+
64
+ Config is attached to each rendering in XMC layout data (authored by the Marketplace app):
65
+
66
+ ```ts
67
+ interface PersonalizeConnectConfig {
68
+ friendlyId: string; // Personalize Interactive Experience ID
69
+ contentMap: Record<string, string>; // contentKey -> datasource GUID
70
+ defaultKey: string; // Fallback key if experience fails
71
+ }
72
+ ```
73
+
74
+ ## Exports
75
+
76
+ - `PersonalizeProvider`, `usePersonalizeContext` — Provider and context
77
+ - `withPersonalizeConnect` — HOC for zero-code personalization
78
+ - `usePersonalizeExperience` — Hook for manual control
79
+ - `callPersonalize` — Low-level API client
80
+ - `resolveContent` — Map contentKey to datasource fields
81
+ - `getBrowserId` — Browser ID cookie helper
82
+ - Types: `PersonalizeConnectConfig`, `PersonalizeConnectProviderProps`, etc.
@@ -0,0 +1,144 @@
1
+ import { ReactNode, ComponentType } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Personalize Connect SDK types.
6
+ * Stored on the rendering in XMC layout data.
7
+ */
8
+ interface PersonalizeConnectConfig {
9
+ /** The Personalize Interactive Experience friendly ID */
10
+ friendlyId: string;
11
+ /**
12
+ * Maps contentKey values (returned by the experience)
13
+ * to XMC datasource item GUIDs
14
+ */
15
+ contentMap: Record<string, string>;
16
+ /**
17
+ * Which contentMap key to render on initial load
18
+ * and use as fallback if the experience fails
19
+ * or returns an unrecognized key
20
+ */
21
+ defaultKey: string;
22
+ }
23
+ /** The standardized API response contract from Personalize */
24
+ interface PersonalizeConnectResponse {
25
+ contentKey: string;
26
+ }
27
+ /** What the SDK sends to Personalize /v2/callFlows */
28
+ interface CallFlowsRequest {
29
+ clientKey: string;
30
+ channel: string;
31
+ language: string;
32
+ currencyCode: string;
33
+ pointOfSale: string;
34
+ browserId: string;
35
+ friendlyId: string;
36
+ params?: {
37
+ componentRef?: string;
38
+ pageRoute?: string;
39
+ [key: string]: unknown;
40
+ };
41
+ }
42
+ /** Provider configuration */
43
+ interface PersonalizeConnectProviderProps {
44
+ clientKey: string;
45
+ pointOfSale: string;
46
+ channel?: string;
47
+ language?: string;
48
+ currencyCode?: string;
49
+ timeout?: number;
50
+ /** Custom function to fetch datasource fields by item ID (e.g. via GraphQL or Layout Service) */
51
+ resolveDatasource?: (datasourceId: string) => Promise<ComponentFields>;
52
+ children: ReactNode;
53
+ }
54
+ /** Resolved datasource fields passed as component props */
55
+ type ComponentFields = Record<string, unknown>;
56
+ /** Value exposed by PersonalizeContext */
57
+ interface PersonalizeContextValue {
58
+ clientKey: string;
59
+ pointOfSale: string;
60
+ channel: string;
61
+ language: string;
62
+ currencyCode: string;
63
+ timeout: number;
64
+ browserId: string;
65
+ resolveDatasource: (datasourceId: string) => Promise<ComponentFields>;
66
+ }
67
+
68
+ declare function PersonalizeProvider({ children, clientKey, pointOfSale, channel, language, currencyCode, timeout, resolveDatasource, }: PersonalizeConnectProviderProps): react_jsx_runtime.JSX.Element;
69
+ declare function usePersonalizeContext(): PersonalizeContextValue | null;
70
+
71
+ /**
72
+ * Thin async wrapper around POST /v2/callFlows.
73
+ * Enforces timeout, parses response, validates contentKey.
74
+ */
75
+
76
+ interface CallPersonalizeOptions {
77
+ config: PersonalizeConnectConfig;
78
+ context: PersonalizeContextValue;
79
+ /** Optional: override API base URL (e.g. region-specific) */
80
+ apiBase?: string;
81
+ /** Optional: component ref for params */
82
+ componentRef?: string;
83
+ /** Optional: page route for params */
84
+ pageRoute?: string;
85
+ }
86
+ /**
87
+ * Call Personalize /v2/callFlows to get the active content key.
88
+ * Returns the contentKey on success, null on any failure.
89
+ */
90
+ declare function callPersonalize(options: CallPersonalizeOptions): Promise<string | null>;
91
+
92
+ /**
93
+ * Maps contentKey to datasource GUID via contentMap,
94
+ * fetches datasource fields, and falls back to defaultKey on failure.
95
+ */
96
+
97
+ interface ResolveContentOptions {
98
+ contentKey: string | null;
99
+ config: PersonalizeConnectConfig;
100
+ resolveDatasource: (datasourceId: string) => Promise<ComponentFields>;
101
+ }
102
+ interface ResolvedContent {
103
+ datasourceId: string;
104
+ fields: ComponentFields;
105
+ }
106
+ /**
107
+ * Resolve content key to datasource fields.
108
+ * Falls back to defaultKey if contentKey is null or not in contentMap.
109
+ * Returns null on any fetch failure.
110
+ */
111
+ declare function resolveContent(options: ResolveContentOptions): Promise<ResolvedContent | null>;
112
+
113
+ type GetConfigFromProps<P> = (props: P) => PersonalizeConnectConfig | undefined;
114
+ /**
115
+ * HOC that wraps any JSS/Content SDK component.
116
+ * If the rendering has a personalizeConnect config, it renders with defaultKey first,
117
+ * then asynchronously fetches the personalized content and re-renders.
118
+ * If no config, passes through unchanged.
119
+ */
120
+ declare function withPersonalizeConnect<P extends object>(WrappedComponent: ComponentType<P>, getConfig?: GetConfigFromProps<P>): ComponentType<P>;
121
+
122
+ interface UsePersonalizeExperienceResult {
123
+ contentKey: string | null;
124
+ resolvedFields: ComponentFields | null;
125
+ isLoading: boolean;
126
+ error: Error | null;
127
+ }
128
+ /**
129
+ * Hook alternative for cases where the HOC pattern doesn't fit.
130
+ * Returns contentKey, resolvedFields, isLoading, and error.
131
+ */
132
+ declare function usePersonalizeExperience(config: PersonalizeConnectConfig | undefined): UsePersonalizeExperienceResult;
133
+
134
+ /**
135
+ * Manages the Sitecore CDP browser ID cookie (bid_<clientKey>).
136
+ * Used for session continuity across Personalize decisions.
137
+ */
138
+ /**
139
+ * Get or create the browser ID for the given client key.
140
+ * Reads from cookie bid_<clientKey>, writes if missing, and returns the value.
141
+ */
142
+ declare function getBrowserId(clientKey: string): string;
143
+
144
+ export { type CallFlowsRequest, type CallPersonalizeOptions, type ComponentFields, type GetConfigFromProps, type PersonalizeConnectConfig, type PersonalizeConnectProviderProps, type PersonalizeConnectResponse, type PersonalizeContextValue, PersonalizeProvider, type ResolveContentOptions, type ResolvedContent, type UsePersonalizeExperienceResult, callPersonalize, getBrowserId, resolveContent, usePersonalizeContext, usePersonalizeExperience, withPersonalizeConnect };
@@ -0,0 +1,144 @@
1
+ import { ReactNode, ComponentType } from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /**
5
+ * Personalize Connect SDK types.
6
+ * Stored on the rendering in XMC layout data.
7
+ */
8
+ interface PersonalizeConnectConfig {
9
+ /** The Personalize Interactive Experience friendly ID */
10
+ friendlyId: string;
11
+ /**
12
+ * Maps contentKey values (returned by the experience)
13
+ * to XMC datasource item GUIDs
14
+ */
15
+ contentMap: Record<string, string>;
16
+ /**
17
+ * Which contentMap key to render on initial load
18
+ * and use as fallback if the experience fails
19
+ * or returns an unrecognized key
20
+ */
21
+ defaultKey: string;
22
+ }
23
+ /** The standardized API response contract from Personalize */
24
+ interface PersonalizeConnectResponse {
25
+ contentKey: string;
26
+ }
27
+ /** What the SDK sends to Personalize /v2/callFlows */
28
+ interface CallFlowsRequest {
29
+ clientKey: string;
30
+ channel: string;
31
+ language: string;
32
+ currencyCode: string;
33
+ pointOfSale: string;
34
+ browserId: string;
35
+ friendlyId: string;
36
+ params?: {
37
+ componentRef?: string;
38
+ pageRoute?: string;
39
+ [key: string]: unknown;
40
+ };
41
+ }
42
+ /** Provider configuration */
43
+ interface PersonalizeConnectProviderProps {
44
+ clientKey: string;
45
+ pointOfSale: string;
46
+ channel?: string;
47
+ language?: string;
48
+ currencyCode?: string;
49
+ timeout?: number;
50
+ /** Custom function to fetch datasource fields by item ID (e.g. via GraphQL or Layout Service) */
51
+ resolveDatasource?: (datasourceId: string) => Promise<ComponentFields>;
52
+ children: ReactNode;
53
+ }
54
+ /** Resolved datasource fields passed as component props */
55
+ type ComponentFields = Record<string, unknown>;
56
+ /** Value exposed by PersonalizeContext */
57
+ interface PersonalizeContextValue {
58
+ clientKey: string;
59
+ pointOfSale: string;
60
+ channel: string;
61
+ language: string;
62
+ currencyCode: string;
63
+ timeout: number;
64
+ browserId: string;
65
+ resolveDatasource: (datasourceId: string) => Promise<ComponentFields>;
66
+ }
67
+
68
+ declare function PersonalizeProvider({ children, clientKey, pointOfSale, channel, language, currencyCode, timeout, resolveDatasource, }: PersonalizeConnectProviderProps): react_jsx_runtime.JSX.Element;
69
+ declare function usePersonalizeContext(): PersonalizeContextValue | null;
70
+
71
+ /**
72
+ * Thin async wrapper around POST /v2/callFlows.
73
+ * Enforces timeout, parses response, validates contentKey.
74
+ */
75
+
76
+ interface CallPersonalizeOptions {
77
+ config: PersonalizeConnectConfig;
78
+ context: PersonalizeContextValue;
79
+ /** Optional: override API base URL (e.g. region-specific) */
80
+ apiBase?: string;
81
+ /** Optional: component ref for params */
82
+ componentRef?: string;
83
+ /** Optional: page route for params */
84
+ pageRoute?: string;
85
+ }
86
+ /**
87
+ * Call Personalize /v2/callFlows to get the active content key.
88
+ * Returns the contentKey on success, null on any failure.
89
+ */
90
+ declare function callPersonalize(options: CallPersonalizeOptions): Promise<string | null>;
91
+
92
+ /**
93
+ * Maps contentKey to datasource GUID via contentMap,
94
+ * fetches datasource fields, and falls back to defaultKey on failure.
95
+ */
96
+
97
+ interface ResolveContentOptions {
98
+ contentKey: string | null;
99
+ config: PersonalizeConnectConfig;
100
+ resolveDatasource: (datasourceId: string) => Promise<ComponentFields>;
101
+ }
102
+ interface ResolvedContent {
103
+ datasourceId: string;
104
+ fields: ComponentFields;
105
+ }
106
+ /**
107
+ * Resolve content key to datasource fields.
108
+ * Falls back to defaultKey if contentKey is null or not in contentMap.
109
+ * Returns null on any fetch failure.
110
+ */
111
+ declare function resolveContent(options: ResolveContentOptions): Promise<ResolvedContent | null>;
112
+
113
+ type GetConfigFromProps<P> = (props: P) => PersonalizeConnectConfig | undefined;
114
+ /**
115
+ * HOC that wraps any JSS/Content SDK component.
116
+ * If the rendering has a personalizeConnect config, it renders with defaultKey first,
117
+ * then asynchronously fetches the personalized content and re-renders.
118
+ * If no config, passes through unchanged.
119
+ */
120
+ declare function withPersonalizeConnect<P extends object>(WrappedComponent: ComponentType<P>, getConfig?: GetConfigFromProps<P>): ComponentType<P>;
121
+
122
+ interface UsePersonalizeExperienceResult {
123
+ contentKey: string | null;
124
+ resolvedFields: ComponentFields | null;
125
+ isLoading: boolean;
126
+ error: Error | null;
127
+ }
128
+ /**
129
+ * Hook alternative for cases where the HOC pattern doesn't fit.
130
+ * Returns contentKey, resolvedFields, isLoading, and error.
131
+ */
132
+ declare function usePersonalizeExperience(config: PersonalizeConnectConfig | undefined): UsePersonalizeExperienceResult;
133
+
134
+ /**
135
+ * Manages the Sitecore CDP browser ID cookie (bid_<clientKey>).
136
+ * Used for session continuity across Personalize decisions.
137
+ */
138
+ /**
139
+ * Get or create the browser ID for the given client key.
140
+ * Reads from cookie bid_<clientKey>, writes if missing, and returns the value.
141
+ */
142
+ declare function getBrowserId(clientKey: string): string;
143
+
144
+ export { type CallFlowsRequest, type CallPersonalizeOptions, type ComponentFields, type GetConfigFromProps, type PersonalizeConnectConfig, type PersonalizeConnectProviderProps, type PersonalizeConnectResponse, type PersonalizeContextValue, PersonalizeProvider, type ResolveContentOptions, type ResolvedContent, type UsePersonalizeExperienceResult, callPersonalize, getBrowserId, resolveContent, usePersonalizeContext, usePersonalizeExperience, withPersonalizeConnect };
package/dist/index.js ADDED
@@ -0,0 +1,283 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PersonalizeProvider: () => PersonalizeProvider,
24
+ callPersonalize: () => callPersonalize,
25
+ getBrowserId: () => getBrowserId,
26
+ resolveContent: () => resolveContent,
27
+ usePersonalizeContext: () => usePersonalizeContext,
28
+ usePersonalizeExperience: () => usePersonalizeExperience,
29
+ withPersonalizeConnect: () => withPersonalizeConnect
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/PersonalizeProvider.tsx
34
+ var import_react = require("react");
35
+
36
+ // src/browserId.ts
37
+ var COOKIE_PREFIX = "bid_";
38
+ var COOKIE_MAX_AGE = 63072e3;
39
+ var COOKIE_PATH = "/";
40
+ var COOKIE_SAME_SITE = "Lax";
41
+ function generateBrowserId() {
42
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
43
+ return crypto.randomUUID();
44
+ }
45
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
46
+ const r = Math.random() * 16 | 0;
47
+ const v = c === "x" ? r : r & 3 | 8;
48
+ return v.toString(16);
49
+ });
50
+ }
51
+ function getCookie(name) {
52
+ if (typeof document === "undefined") return null;
53
+ const match = document.cookie.match(new RegExp("(?:^|; )" + encodeURIComponent(name) + "=([^;]*)"));
54
+ return match ? decodeURIComponent(match[1]) : null;
55
+ }
56
+ function setCookie(name, value) {
57
+ if (typeof document === "undefined") return;
58
+ document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + "; path=" + COOKIE_PATH + "; max-age=" + COOKIE_MAX_AGE + "; SameSite=" + COOKIE_SAME_SITE;
59
+ }
60
+ function getBrowserId(clientKey) {
61
+ const cookieName = COOKIE_PREFIX + clientKey;
62
+ let bid = getCookie(cookieName);
63
+ if (!bid || typeof bid !== "string" || bid.length < 10) {
64
+ bid = generateBrowserId();
65
+ setCookie(cookieName, bid);
66
+ }
67
+ return bid;
68
+ }
69
+
70
+ // src/PersonalizeProvider.tsx
71
+ var import_jsx_runtime = require("react/jsx-runtime");
72
+ var PersonalizeContext = (0, import_react.createContext)(null);
73
+ var DEFAULT_CHANNEL = "WEB";
74
+ var DEFAULT_LANGUAGE = "EN";
75
+ var DEFAULT_CURRENCY = "USD";
76
+ var DEFAULT_TIMEOUT = 600;
77
+ var noopResolver = async () => ({});
78
+ function PersonalizeProvider({
79
+ children,
80
+ clientKey,
81
+ pointOfSale,
82
+ channel = DEFAULT_CHANNEL,
83
+ language = DEFAULT_LANGUAGE,
84
+ currencyCode = DEFAULT_CURRENCY,
85
+ timeout = DEFAULT_TIMEOUT,
86
+ resolveDatasource = noopResolver
87
+ }) {
88
+ const [browserId, setBrowserId] = (0, import_react.useState)("");
89
+ (0, import_react.useEffect)(() => {
90
+ setBrowserId(getBrowserId(clientKey));
91
+ }, [clientKey]);
92
+ const stableResolveDatasource = (0, import_react.useCallback)(resolveDatasource, [resolveDatasource]);
93
+ const effectiveBrowserId = browserId || (typeof window !== "undefined" ? getBrowserId(clientKey) : "");
94
+ const value = (0, import_react.useMemo)(
95
+ () => ({
96
+ clientKey,
97
+ pointOfSale,
98
+ channel,
99
+ language,
100
+ currencyCode,
101
+ timeout,
102
+ browserId: effectiveBrowserId,
103
+ resolveDatasource: stableResolveDatasource
104
+ }),
105
+ [
106
+ clientKey,
107
+ pointOfSale,
108
+ channel,
109
+ language,
110
+ currencyCode,
111
+ timeout,
112
+ effectiveBrowserId,
113
+ stableResolveDatasource
114
+ ]
115
+ );
116
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PersonalizeContext.Provider, { value, children });
117
+ }
118
+ function usePersonalizeContext() {
119
+ return (0, import_react.useContext)(PersonalizeContext);
120
+ }
121
+
122
+ // src/personalizeClient.ts
123
+ var DEFAULT_API_BASE = "https://api.boxever.com";
124
+ async function callPersonalize(options) {
125
+ const { config, context, apiBase = DEFAULT_API_BASE, componentRef, pageRoute } = options;
126
+ const body = {
127
+ clientKey: context.clientKey,
128
+ channel: context.channel,
129
+ language: context.language,
130
+ currencyCode: context.currencyCode,
131
+ pointOfSale: context.pointOfSale,
132
+ browserId: context.browserId,
133
+ friendlyId: config.friendlyId
134
+ };
135
+ if (componentRef || pageRoute) {
136
+ body.params = {};
137
+ if (componentRef) body.params.componentRef = componentRef;
138
+ if (pageRoute) body.params.pageRoute = pageRoute;
139
+ }
140
+ const controller = new AbortController();
141
+ const timeoutId = setTimeout(() => controller.abort(), context.timeout);
142
+ try {
143
+ const res = await fetch(`${apiBase.replace(/\/$/, "")}/v2/callFlows`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body: JSON.stringify(body),
147
+ signal: controller.signal
148
+ });
149
+ clearTimeout(timeoutId);
150
+ if (!res.ok) return null;
151
+ const data = await res.json();
152
+ if (!data || typeof data !== "object") return null;
153
+ const contentKey = data.contentKey;
154
+ if (typeof contentKey !== "string" || contentKey.trim() === "") return null;
155
+ return contentKey;
156
+ } catch {
157
+ clearTimeout(timeoutId);
158
+ return null;
159
+ }
160
+ }
161
+
162
+ // src/contentResolver.ts
163
+ async function resolveContent(options) {
164
+ const { contentKey, config, resolveDatasource } = options;
165
+ const effectiveKey = contentKey && contentKey in config.contentMap ? contentKey : config.defaultKey;
166
+ const datasourceId = config.contentMap[effectiveKey];
167
+ if (!datasourceId || typeof datasourceId !== "string") return null;
168
+ try {
169
+ const fields = await resolveDatasource(datasourceId);
170
+ if (!fields || typeof fields !== "object") return null;
171
+ return { datasourceId, fields };
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+
177
+ // src/withPersonalizeConnect.tsx
178
+ var import_react2 = require("react");
179
+ var import_jsx_runtime2 = require("react/jsx-runtime");
180
+ var DEFAULT_GET_CONFIG = (props) => props.rendering?.personalizeConnect;
181
+ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG) {
182
+ function PersonalizeConnectWrapper(props) {
183
+ const context = usePersonalizeContext();
184
+ const config = getConfig(props);
185
+ const [resolvedFields, setResolvedFields] = (0, import_react2.useState)(null);
186
+ const mountedRef = (0, import_react2.useRef)(true);
187
+ const runPersonalization = (0, import_react2.useCallback)(async () => {
188
+ if (!config || !context) return;
189
+ const contentKey = await callPersonalize({ config, context });
190
+ if (!mountedRef.current) return;
191
+ const resolved = await resolveContent({
192
+ contentKey,
193
+ config,
194
+ resolveDatasource: context.resolveDatasource
195
+ });
196
+ if (!mountedRef.current) return;
197
+ if (resolved) {
198
+ setResolvedFields(resolved.fields);
199
+ }
200
+ }, [config, context]);
201
+ (0, import_react2.useEffect)(() => {
202
+ mountedRef.current = true;
203
+ if (config && context) {
204
+ runPersonalization();
205
+ }
206
+ return () => {
207
+ mountedRef.current = false;
208
+ };
209
+ }, [config?.friendlyId, context?.clientKey, runPersonalization]);
210
+ if (!config || !context) {
211
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WrappedComponent, { ...props });
212
+ }
213
+ const mergedProps = {
214
+ ...props,
215
+ ...resolvedFields ?? {}
216
+ };
217
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WrappedComponent, { ...mergedProps });
218
+ }
219
+ PersonalizeConnectWrapper.displayName = `WithPersonalizeConnect(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`;
220
+ return PersonalizeConnectWrapper;
221
+ }
222
+
223
+ // src/usePersonalizeExperience.ts
224
+ var import_react3 = require("react");
225
+ function usePersonalizeExperience(config) {
226
+ const context = usePersonalizeContext();
227
+ const [contentKey, setContentKey] = (0, import_react3.useState)(null);
228
+ const [resolvedFields, setResolvedFields] = (0, import_react3.useState)(null);
229
+ const [isLoading, setIsLoading] = (0, import_react3.useState)(true);
230
+ const [error, setError] = (0, import_react3.useState)(null);
231
+ const mountedRef = (0, import_react3.useRef)(true);
232
+ const runPersonalization = (0, import_react3.useCallback)(async () => {
233
+ if (!config || !context) {
234
+ setIsLoading(false);
235
+ return;
236
+ }
237
+ setError(null);
238
+ setContentKey(null);
239
+ setResolvedFields(null);
240
+ setIsLoading(true);
241
+ try {
242
+ const key = await callPersonalize({ config, context });
243
+ if (!mountedRef.current) return;
244
+ setContentKey(key);
245
+ const resolved = await resolveContent({
246
+ contentKey: key,
247
+ config,
248
+ resolveDatasource: context.resolveDatasource
249
+ });
250
+ if (!mountedRef.current) return;
251
+ if (resolved) setResolvedFields(resolved.fields);
252
+ } catch (e) {
253
+ if (mountedRef.current) {
254
+ setError(e instanceof Error ? e : new Error(String(e)));
255
+ }
256
+ } finally {
257
+ if (mountedRef.current) setIsLoading(false);
258
+ }
259
+ }, [config, context]);
260
+ (0, import_react3.useEffect)(() => {
261
+ mountedRef.current = true;
262
+ runPersonalization();
263
+ return () => {
264
+ mountedRef.current = false;
265
+ };
266
+ }, [runPersonalization]);
267
+ return {
268
+ contentKey,
269
+ resolvedFields,
270
+ isLoading,
271
+ error
272
+ };
273
+ }
274
+ // Annotate the CommonJS export names for ESM import in node:
275
+ 0 && (module.exports = {
276
+ PersonalizeProvider,
277
+ callPersonalize,
278
+ getBrowserId,
279
+ resolveContent,
280
+ usePersonalizeContext,
281
+ usePersonalizeExperience,
282
+ withPersonalizeConnect
283
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,250 @@
1
+ // src/PersonalizeProvider.tsx
2
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
3
+
4
+ // src/browserId.ts
5
+ var COOKIE_PREFIX = "bid_";
6
+ var COOKIE_MAX_AGE = 63072e3;
7
+ var COOKIE_PATH = "/";
8
+ var COOKIE_SAME_SITE = "Lax";
9
+ function generateBrowserId() {
10
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
11
+ return crypto.randomUUID();
12
+ }
13
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
14
+ const r = Math.random() * 16 | 0;
15
+ const v = c === "x" ? r : r & 3 | 8;
16
+ return v.toString(16);
17
+ });
18
+ }
19
+ function getCookie(name) {
20
+ if (typeof document === "undefined") return null;
21
+ const match = document.cookie.match(new RegExp("(?:^|; )" + encodeURIComponent(name) + "=([^;]*)"));
22
+ return match ? decodeURIComponent(match[1]) : null;
23
+ }
24
+ function setCookie(name, value) {
25
+ if (typeof document === "undefined") return;
26
+ document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + "; path=" + COOKIE_PATH + "; max-age=" + COOKIE_MAX_AGE + "; SameSite=" + COOKIE_SAME_SITE;
27
+ }
28
+ function getBrowserId(clientKey) {
29
+ const cookieName = COOKIE_PREFIX + clientKey;
30
+ let bid = getCookie(cookieName);
31
+ if (!bid || typeof bid !== "string" || bid.length < 10) {
32
+ bid = generateBrowserId();
33
+ setCookie(cookieName, bid);
34
+ }
35
+ return bid;
36
+ }
37
+
38
+ // src/PersonalizeProvider.tsx
39
+ import { jsx } from "react/jsx-runtime";
40
+ var PersonalizeContext = createContext(null);
41
+ var DEFAULT_CHANNEL = "WEB";
42
+ var DEFAULT_LANGUAGE = "EN";
43
+ var DEFAULT_CURRENCY = "USD";
44
+ var DEFAULT_TIMEOUT = 600;
45
+ var noopResolver = async () => ({});
46
+ function PersonalizeProvider({
47
+ children,
48
+ clientKey,
49
+ pointOfSale,
50
+ channel = DEFAULT_CHANNEL,
51
+ language = DEFAULT_LANGUAGE,
52
+ currencyCode = DEFAULT_CURRENCY,
53
+ timeout = DEFAULT_TIMEOUT,
54
+ resolveDatasource = noopResolver
55
+ }) {
56
+ const [browserId, setBrowserId] = useState("");
57
+ useEffect(() => {
58
+ setBrowserId(getBrowserId(clientKey));
59
+ }, [clientKey]);
60
+ const stableResolveDatasource = useCallback(resolveDatasource, [resolveDatasource]);
61
+ const effectiveBrowserId = browserId || (typeof window !== "undefined" ? getBrowserId(clientKey) : "");
62
+ const value = useMemo(
63
+ () => ({
64
+ clientKey,
65
+ pointOfSale,
66
+ channel,
67
+ language,
68
+ currencyCode,
69
+ timeout,
70
+ browserId: effectiveBrowserId,
71
+ resolveDatasource: stableResolveDatasource
72
+ }),
73
+ [
74
+ clientKey,
75
+ pointOfSale,
76
+ channel,
77
+ language,
78
+ currencyCode,
79
+ timeout,
80
+ effectiveBrowserId,
81
+ stableResolveDatasource
82
+ ]
83
+ );
84
+ return /* @__PURE__ */ jsx(PersonalizeContext.Provider, { value, children });
85
+ }
86
+ function usePersonalizeContext() {
87
+ return useContext(PersonalizeContext);
88
+ }
89
+
90
+ // src/personalizeClient.ts
91
+ var DEFAULT_API_BASE = "https://api.boxever.com";
92
+ async function callPersonalize(options) {
93
+ const { config, context, apiBase = DEFAULT_API_BASE, componentRef, pageRoute } = options;
94
+ const body = {
95
+ clientKey: context.clientKey,
96
+ channel: context.channel,
97
+ language: context.language,
98
+ currencyCode: context.currencyCode,
99
+ pointOfSale: context.pointOfSale,
100
+ browserId: context.browserId,
101
+ friendlyId: config.friendlyId
102
+ };
103
+ if (componentRef || pageRoute) {
104
+ body.params = {};
105
+ if (componentRef) body.params.componentRef = componentRef;
106
+ if (pageRoute) body.params.pageRoute = pageRoute;
107
+ }
108
+ const controller = new AbortController();
109
+ const timeoutId = setTimeout(() => controller.abort(), context.timeout);
110
+ try {
111
+ const res = await fetch(`${apiBase.replace(/\/$/, "")}/v2/callFlows`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify(body),
115
+ signal: controller.signal
116
+ });
117
+ clearTimeout(timeoutId);
118
+ if (!res.ok) return null;
119
+ const data = await res.json();
120
+ if (!data || typeof data !== "object") return null;
121
+ const contentKey = data.contentKey;
122
+ if (typeof contentKey !== "string" || contentKey.trim() === "") return null;
123
+ return contentKey;
124
+ } catch {
125
+ clearTimeout(timeoutId);
126
+ return null;
127
+ }
128
+ }
129
+
130
+ // src/contentResolver.ts
131
+ async function resolveContent(options) {
132
+ const { contentKey, config, resolveDatasource } = options;
133
+ const effectiveKey = contentKey && contentKey in config.contentMap ? contentKey : config.defaultKey;
134
+ const datasourceId = config.contentMap[effectiveKey];
135
+ if (!datasourceId || typeof datasourceId !== "string") return null;
136
+ try {
137
+ const fields = await resolveDatasource(datasourceId);
138
+ if (!fields || typeof fields !== "object") return null;
139
+ return { datasourceId, fields };
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ // src/withPersonalizeConnect.tsx
146
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef, useState as useState2 } from "react";
147
+ import { jsx as jsx2 } from "react/jsx-runtime";
148
+ var DEFAULT_GET_CONFIG = (props) => props.rendering?.personalizeConnect;
149
+ function withPersonalizeConnect(WrappedComponent, getConfig = DEFAULT_GET_CONFIG) {
150
+ function PersonalizeConnectWrapper(props) {
151
+ const context = usePersonalizeContext();
152
+ const config = getConfig(props);
153
+ const [resolvedFields, setResolvedFields] = useState2(null);
154
+ const mountedRef = useRef(true);
155
+ const runPersonalization = useCallback2(async () => {
156
+ if (!config || !context) return;
157
+ const contentKey = await callPersonalize({ config, context });
158
+ if (!mountedRef.current) return;
159
+ const resolved = await resolveContent({
160
+ contentKey,
161
+ config,
162
+ resolveDatasource: context.resolveDatasource
163
+ });
164
+ if (!mountedRef.current) return;
165
+ if (resolved) {
166
+ setResolvedFields(resolved.fields);
167
+ }
168
+ }, [config, context]);
169
+ useEffect2(() => {
170
+ mountedRef.current = true;
171
+ if (config && context) {
172
+ runPersonalization();
173
+ }
174
+ return () => {
175
+ mountedRef.current = false;
176
+ };
177
+ }, [config?.friendlyId, context?.clientKey, runPersonalization]);
178
+ if (!config || !context) {
179
+ return /* @__PURE__ */ jsx2(WrappedComponent, { ...props });
180
+ }
181
+ const mergedProps = {
182
+ ...props,
183
+ ...resolvedFields ?? {}
184
+ };
185
+ return /* @__PURE__ */ jsx2(WrappedComponent, { ...mergedProps });
186
+ }
187
+ PersonalizeConnectWrapper.displayName = `WithPersonalizeConnect(${WrappedComponent.displayName ?? WrappedComponent.name ?? "Component"})`;
188
+ return PersonalizeConnectWrapper;
189
+ }
190
+
191
+ // src/usePersonalizeExperience.ts
192
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
193
+ function usePersonalizeExperience(config) {
194
+ const context = usePersonalizeContext();
195
+ const [contentKey, setContentKey] = useState3(null);
196
+ const [resolvedFields, setResolvedFields] = useState3(null);
197
+ const [isLoading, setIsLoading] = useState3(true);
198
+ const [error, setError] = useState3(null);
199
+ const mountedRef = useRef2(true);
200
+ const runPersonalization = useCallback3(async () => {
201
+ if (!config || !context) {
202
+ setIsLoading(false);
203
+ return;
204
+ }
205
+ setError(null);
206
+ setContentKey(null);
207
+ setResolvedFields(null);
208
+ setIsLoading(true);
209
+ try {
210
+ const key = await callPersonalize({ config, context });
211
+ if (!mountedRef.current) return;
212
+ setContentKey(key);
213
+ const resolved = await resolveContent({
214
+ contentKey: key,
215
+ config,
216
+ resolveDatasource: context.resolveDatasource
217
+ });
218
+ if (!mountedRef.current) return;
219
+ if (resolved) setResolvedFields(resolved.fields);
220
+ } catch (e) {
221
+ if (mountedRef.current) {
222
+ setError(e instanceof Error ? e : new Error(String(e)));
223
+ }
224
+ } finally {
225
+ if (mountedRef.current) setIsLoading(false);
226
+ }
227
+ }, [config, context]);
228
+ useEffect3(() => {
229
+ mountedRef.current = true;
230
+ runPersonalization();
231
+ return () => {
232
+ mountedRef.current = false;
233
+ };
234
+ }, [runPersonalization]);
235
+ return {
236
+ contentKey,
237
+ resolvedFields,
238
+ isLoading,
239
+ error
240
+ };
241
+ }
242
+ export {
243
+ PersonalizeProvider,
244
+ callPersonalize,
245
+ getBrowserId,
246
+ resolveContent,
247
+ usePersonalizeContext,
248
+ usePersonalizeExperience,
249
+ withPersonalizeConnect
250
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "personalize-connect-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Runtime SDK for Personalize Connect - resolves active experience outcomes and datasources in your rendering host",
5
+ "keywords": [
6
+ "sitecore",
7
+ "personalize",
8
+ "personalization",
9
+ "sitecore-personalize",
10
+ "rendering",
11
+ "marketplace"
12
+ ],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/your-org/2026-Team-Solo.git",
17
+ "directory": "packages/sdk"
18
+ },
19
+ "main": "dist/index.js",
20
+ "module": "dist/index.mjs",
21
+ "types": "dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.mjs",
26
+ "require": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "peerDependencies": {
34
+ "react": ">=18.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.2.0",
38
+ "@types/react": "^18.0.0",
39
+ "react": "^18.0.0",
40
+ "tsup": "^8.3.5",
41
+ "typescript": "^5.9.2"
42
+ },
43
+ "scripts": {
44
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
45
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch"
46
+ }
47
+ }