@cimplify/sdk 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs ADDED
@@ -0,0 +1,436 @@
1
+ import { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
2
+ import { jsx, Fragment } from 'react/jsx-runtime';
3
+
4
+ // src/react/index.tsx
5
+
6
+ // src/types/elements.ts
7
+ var ELEMENT_TYPES = {
8
+ AUTH: "auth",
9
+ ADDRESS: "address",
10
+ PAYMENT: "payment"
11
+ };
12
+ var EVENT_TYPES = {
13
+ READY: "ready",
14
+ AUTHENTICATED: "authenticated",
15
+ REQUIRES_OTP: "requires_otp",
16
+ ERROR: "error",
17
+ CHANGE: "change"};
18
+
19
+ // src/ads/identity.ts
20
+ var COOKIE_NAME = "_cimplify_uid";
21
+ var COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
22
+ function getOrCreateUserId() {
23
+ if (typeof document === "undefined") return generateId();
24
+ const existing = getCookie(COOKIE_NAME);
25
+ if (existing) return existing;
26
+ const newId = generateId();
27
+ setCookie(COOKIE_NAME, newId, COOKIE_MAX_AGE);
28
+ return newId;
29
+ }
30
+ function collectDeviceSignals() {
31
+ if (typeof window === "undefined") {
32
+ return getEmptySignals();
33
+ }
34
+ return {
35
+ userAgent: navigator.userAgent,
36
+ language: navigator.language,
37
+ languages: Array.from(navigator.languages || []),
38
+ platform: navigator.platform,
39
+ screenWidth: screen.width,
40
+ screenHeight: screen.height,
41
+ colorDepth: screen.colorDepth,
42
+ pixelRatio: window.devicePixelRatio || 1,
43
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
44
+ timezoneOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
45
+ touchSupport: "ontouchstart" in window || navigator.maxTouchPoints > 0,
46
+ cookiesEnabled: navigator.cookieEnabled,
47
+ doNotTrack: navigator.doNotTrack === "1"
48
+ };
49
+ }
50
+ function generateFingerprint(signals) {
51
+ const components = [
52
+ signals.userAgent,
53
+ signals.language,
54
+ signals.platform,
55
+ `${signals.screenWidth}x${signals.screenHeight}`,
56
+ signals.colorDepth.toString(),
57
+ signals.pixelRatio.toString(),
58
+ signals.timezone,
59
+ signals.touchSupport.toString()
60
+ ];
61
+ return hashString(components.join("|"));
62
+ }
63
+ function getUserIdentity(authenticatedAccountId) {
64
+ const signals = collectDeviceSignals();
65
+ const fingerprint = generateFingerprint(signals);
66
+ const cookieId = getOrCreateUserId();
67
+ return {
68
+ uid: authenticatedAccountId || cookieId,
69
+ isAuthenticated: !!authenticatedAccountId,
70
+ fingerprint,
71
+ signals
72
+ };
73
+ }
74
+ function getDeviceType(signals) {
75
+ const ua = signals.userAgent.toLowerCase();
76
+ const width = signals.screenWidth;
77
+ if (/mobile|android|iphone|ipod/.test(ua) || width < 768) {
78
+ return "mobile";
79
+ }
80
+ if (/tablet|ipad/.test(ua) || width >= 768 && width < 1024) {
81
+ return "tablet";
82
+ }
83
+ return "desktop";
84
+ }
85
+ function getBrowserInfo(signals) {
86
+ const ua = signals.userAgent;
87
+ const browsers = [
88
+ { name: "Chrome", regex: /Chrome\/(\d+)/ },
89
+ { name: "Firefox", regex: /Firefox\/(\d+)/ },
90
+ { name: "Safari", regex: /Version\/(\d+).*Safari/ },
91
+ { name: "Edge", regex: /Edg\/(\d+)/ },
92
+ { name: "Opera", regex: /OPR\/(\d+)/ }
93
+ ];
94
+ for (const browser of browsers) {
95
+ const match = ua.match(browser.regex);
96
+ if (match) {
97
+ return { name: browser.name, version: match[1] };
98
+ }
99
+ }
100
+ return { name: "Unknown", version: "0" };
101
+ }
102
+ function getOSInfo(signals) {
103
+ const ua = signals.userAgent;
104
+ if (/Windows NT 10/.test(ua)) return { name: "Windows", version: "10" };
105
+ if (/Windows NT 6.3/.test(ua)) return { name: "Windows", version: "8.1" };
106
+ if (/Mac OS X (\d+[._]\d+)/.test(ua)) {
107
+ const match = ua.match(/Mac OS X (\d+[._]\d+)/);
108
+ return { name: "macOS", version: match?.[1].replace("_", ".") || "" };
109
+ }
110
+ if (/Android (\d+)/.test(ua)) {
111
+ const match = ua.match(/Android (\d+)/);
112
+ return { name: "Android", version: match?.[1] || "" };
113
+ }
114
+ if (/iPhone OS (\d+)/.test(ua)) {
115
+ const match = ua.match(/iPhone OS (\d+)/);
116
+ return { name: "iOS", version: match?.[1] || "" };
117
+ }
118
+ if (/Linux/.test(ua)) return { name: "Linux", version: "" };
119
+ return { name: "Unknown", version: "" };
120
+ }
121
+ function generateId() {
122
+ const timestamp = Date.now().toString(36);
123
+ const random = Math.random().toString(36).substring(2, 10);
124
+ return `${timestamp}-${random}`;
125
+ }
126
+ function hashString(str) {
127
+ let hash = 0;
128
+ for (let i = 0; i < str.length; i++) {
129
+ const char = str.charCodeAt(i);
130
+ hash = (hash << 5) - hash + char;
131
+ hash = hash & hash;
132
+ }
133
+ return Math.abs(hash).toString(36);
134
+ }
135
+ function getCookie(name) {
136
+ if (typeof document === "undefined") return null;
137
+ const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
138
+ return match ? match[2] : null;
139
+ }
140
+ function setCookie(name, value, maxAge) {
141
+ if (typeof document === "undefined") return;
142
+ document.cookie = `${name}=${value};max-age=${maxAge};path=/;SameSite=Lax`;
143
+ }
144
+ function getEmptySignals() {
145
+ return {
146
+ userAgent: "",
147
+ language: "",
148
+ languages: [],
149
+ platform: "",
150
+ screenWidth: 0,
151
+ screenHeight: 0,
152
+ colorDepth: 0,
153
+ pixelRatio: 1,
154
+ timezone: "",
155
+ timezoneOffset: 0,
156
+ touchSupport: false,
157
+ cookiesEnabled: false,
158
+ doNotTrack: false
159
+ };
160
+ }
161
+ function deriveAdsApiUrl() {
162
+ if (typeof window === "undefined") return "https://api.cimplify.io";
163
+ const hostname = window.location.hostname;
164
+ if (hostname === "localhost" || hostname === "127.0.0.1") {
165
+ return `${window.location.protocol}//${hostname}:8080`;
166
+ }
167
+ return "https://api.cimplify.io";
168
+ }
169
+ var AdContext = createContext({
170
+ siteId: null,
171
+ config: null,
172
+ isLoading: true,
173
+ identity: null,
174
+ apiBase: "https://api.cimplify.io"
175
+ });
176
+ function useAds() {
177
+ return useContext(AdContext);
178
+ }
179
+ function AdProvider({
180
+ siteId,
181
+ apiBase,
182
+ authenticatedAccountId,
183
+ children
184
+ }) {
185
+ const resolvedApiBase = apiBase || deriveAdsApiUrl();
186
+ const [config, setConfig] = useState(null);
187
+ const [isLoading, setIsLoading] = useState(true);
188
+ const [identity, setIdentity] = useState(null);
189
+ useEffect(() => {
190
+ const userIdentity = getUserIdentity(authenticatedAccountId);
191
+ setIdentity(userIdentity);
192
+ fetch(`${resolvedApiBase}/ads/config/${siteId}`).then((r) => r.json()).then((data) => {
193
+ setConfig(data);
194
+ setIsLoading(false);
195
+ }).catch(() => {
196
+ setConfig({ enabled: false, theme: "auto" });
197
+ setIsLoading(false);
198
+ });
199
+ }, [siteId, resolvedApiBase, authenticatedAccountId]);
200
+ return /* @__PURE__ */ jsx(AdContext.Provider, { value: { siteId, config, isLoading, identity, apiBase: resolvedApiBase }, children });
201
+ }
202
+ function Ad({
203
+ slot,
204
+ position = "static",
205
+ className,
206
+ style,
207
+ fallback,
208
+ onImpression,
209
+ onClick
210
+ }) {
211
+ const { siteId, config, isLoading, identity, apiBase } = useAds();
212
+ const [ad, setAd] = useState(null);
213
+ const [error, setError] = useState(false);
214
+ const impressionTracked = useRef(false);
215
+ const containerRef = useRef(null);
216
+ useEffect(() => {
217
+ if (isLoading || !config?.enabled || !siteId || !identity) return;
218
+ const path = typeof window !== "undefined" ? window.location.pathname : "/";
219
+ const referrer = typeof document !== "undefined" ? document.referrer : "";
220
+ if (config.excludePaths?.some((p) => new RegExp(p.replace("*", ".*")).test(path))) {
221
+ return;
222
+ }
223
+ const deviceType = getDeviceType(identity.signals);
224
+ const browser = getBrowserInfo(identity.signals);
225
+ const os = getOSInfo(identity.signals);
226
+ fetch(`${apiBase}/ads/get`, {
227
+ method: "POST",
228
+ headers: { "Content-Type": "application/json" },
229
+ body: JSON.stringify({
230
+ siteId,
231
+ slot,
232
+ path,
233
+ referrer,
234
+ user: {
235
+ uid: identity.uid,
236
+ fingerprint: identity.fingerprint,
237
+ isAuthenticated: identity.isAuthenticated
238
+ },
239
+ device: {
240
+ type: deviceType,
241
+ browser: browser.name,
242
+ browserVersion: browser.version,
243
+ os: os.name,
244
+ osVersion: os.version,
245
+ screenWidth: identity.signals.screenWidth,
246
+ screenHeight: identity.signals.screenHeight,
247
+ language: identity.signals.language,
248
+ timezone: identity.signals.timezone
249
+ }
250
+ })
251
+ }).then((r) => r.json()).then((data) => {
252
+ if (data?.html) {
253
+ setAd(data);
254
+ } else {
255
+ setError(true);
256
+ }
257
+ }).catch(() => setError(true));
258
+ }, [siteId, config, isLoading, slot, identity, apiBase]);
259
+ useEffect(() => {
260
+ if (!ad || impressionTracked.current || typeof window === "undefined" || !identity) return;
261
+ const observer = new IntersectionObserver(
262
+ ([entry]) => {
263
+ if (entry.isIntersecting && !impressionTracked.current) {
264
+ impressionTracked.current = true;
265
+ const impressionData = {
266
+ adId: ad.id,
267
+ siteId,
268
+ slot,
269
+ uid: identity.uid,
270
+ fingerprint: identity.fingerprint,
271
+ timestamp: Date.now()
272
+ };
273
+ if (navigator.sendBeacon) {
274
+ navigator.sendBeacon(
275
+ `${apiBase}/ads/impression`,
276
+ JSON.stringify(impressionData)
277
+ );
278
+ }
279
+ onImpression?.(ad.id);
280
+ }
281
+ },
282
+ { threshold: 0.5 }
283
+ );
284
+ if (containerRef.current) {
285
+ observer.observe(containerRef.current);
286
+ }
287
+ return () => observer.disconnect();
288
+ }, [ad, siteId, slot, onImpression, identity, apiBase]);
289
+ if (isLoading) {
290
+ return /* @__PURE__ */ jsx(
291
+ "div",
292
+ {
293
+ className: `cimplify-ad cimplify-ad--loading ${className || ""}`,
294
+ style,
295
+ "aria-hidden": "true"
296
+ }
297
+ );
298
+ }
299
+ if (!config?.enabled) return null;
300
+ if (error) return fallback ? /* @__PURE__ */ jsx(Fragment, { children: fallback }) : null;
301
+ if (!ad) return null;
302
+ const handleClick = () => {
303
+ onClick?.(ad.id);
304
+ };
305
+ const positionStyles = position === "fixed" ? { position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 9999 } : position === "sticky" ? { position: "sticky", top: 20 } : {};
306
+ return /* @__PURE__ */ jsx(
307
+ "div",
308
+ {
309
+ ref: containerRef,
310
+ id: `cimplify-ad-${ad.id}`,
311
+ className: `cimplify-ad cimplify-ad--${slot} cimplify-ad--${position} cimplify-ad--${config.theme} ${className || ""}`,
312
+ style: { ...positionStyles, ...style },
313
+ onClick: handleClick,
314
+ dangerouslySetInnerHTML: { __html: ad.html }
315
+ }
316
+ );
317
+ }
318
+ var ElementsContext = createContext({
319
+ elements: null,
320
+ isReady: false
321
+ });
322
+ function useElements() {
323
+ return useContext(ElementsContext).elements;
324
+ }
325
+ function useElementsReady() {
326
+ return useContext(ElementsContext).isReady;
327
+ }
328
+ function ElementsProvider({
329
+ client,
330
+ businessId,
331
+ options,
332
+ children
333
+ }) {
334
+ const [elements, setElements] = useState(null);
335
+ const [isReady, setIsReady] = useState(false);
336
+ useEffect(() => {
337
+ const instance = client.elements(businessId, options);
338
+ setElements(instance);
339
+ setIsReady(true);
340
+ return () => instance.destroy();
341
+ }, [client, businessId, options]);
342
+ return /* @__PURE__ */ jsx(ElementsContext.Provider, { value: { elements, isReady }, children });
343
+ }
344
+ function AuthElement({
345
+ className,
346
+ style,
347
+ prefillEmail,
348
+ onReady,
349
+ onAuthenticated,
350
+ onRequiresOtp,
351
+ onError
352
+ }) {
353
+ const containerRef = useRef(null);
354
+ const elementRef = useRef(null);
355
+ const elements = useElements();
356
+ useEffect(() => {
357
+ if (!elements || !containerRef.current) return;
358
+ const element = elements.create(ELEMENT_TYPES.AUTH, { prefillEmail });
359
+ elementRef.current = element;
360
+ element.on(EVENT_TYPES.READY, () => onReady?.());
361
+ element.on(EVENT_TYPES.AUTHENTICATED, (data) => onAuthenticated?.(data));
362
+ element.on(EVENT_TYPES.REQUIRES_OTP, (data) => onRequiresOtp?.(data));
363
+ element.on(EVENT_TYPES.ERROR, (data) => onError?.(data));
364
+ element.mount(containerRef.current);
365
+ return () => element.destroy();
366
+ }, [elements, prefillEmail, onReady, onAuthenticated, onRequiresOtp, onError]);
367
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style });
368
+ }
369
+ function AddressElement({
370
+ className,
371
+ style,
372
+ mode = "shipping",
373
+ onReady,
374
+ onChange,
375
+ onError
376
+ }) {
377
+ const containerRef = useRef(null);
378
+ const elementRef = useRef(null);
379
+ const elements = useElements();
380
+ useEffect(() => {
381
+ if (!elements || !containerRef.current) return;
382
+ const element = elements.create(ELEMENT_TYPES.ADDRESS, { mode });
383
+ elementRef.current = element;
384
+ element.on(EVENT_TYPES.READY, () => onReady?.());
385
+ element.on(EVENT_TYPES.CHANGE, (data) => onChange?.(data));
386
+ element.on(EVENT_TYPES.ERROR, (data) => onError?.(data));
387
+ element.mount(containerRef.current);
388
+ return () => element.destroy();
389
+ }, [elements, mode, onReady, onChange, onError]);
390
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style });
391
+ }
392
+ function PaymentElement({
393
+ className,
394
+ style,
395
+ amount,
396
+ currency,
397
+ onReady,
398
+ onChange,
399
+ onError
400
+ }) {
401
+ const containerRef = useRef(null);
402
+ const elementRef = useRef(null);
403
+ const elements = useElements();
404
+ useEffect(() => {
405
+ if (!elements || !containerRef.current) return;
406
+ const element = elements.create(ELEMENT_TYPES.PAYMENT, { amount, currency });
407
+ elementRef.current = element;
408
+ element.on(EVENT_TYPES.READY, () => onReady?.());
409
+ element.on(EVENT_TYPES.CHANGE, (data) => onChange?.(data));
410
+ element.on(EVENT_TYPES.ERROR, (data) => onError?.(data));
411
+ element.mount(containerRef.current);
412
+ return () => element.destroy();
413
+ }, [elements, amount, currency, onReady, onChange, onError]);
414
+ return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style });
415
+ }
416
+ function useCheckout() {
417
+ const elements = useElements();
418
+ const [isLoading, setIsLoading] = useState(false);
419
+ const submit = useCallback(
420
+ async (data) => {
421
+ if (!elements) {
422
+ return { success: false, error: { code: "NO_ELEMENTS", message: "Elements not initialized" } };
423
+ }
424
+ setIsLoading(true);
425
+ try {
426
+ return await elements.submitCheckout(data);
427
+ } finally {
428
+ setIsLoading(false);
429
+ }
430
+ },
431
+ [elements]
432
+ );
433
+ return { submit, isLoading };
434
+ }
435
+
436
+ export { Ad, AdProvider, AddressElement, AuthElement, ElementsProvider, PaymentElement, useAds, useCheckout, useElements, useElementsReady };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cimplify/sdk",
3
- "version": "0.3.6",
3
+ "version": "0.5.0",
4
4
  "description": "Cimplify Commerce SDK for storefronts",
5
5
  "keywords": [
6
6
  "cimplify",
@@ -20,6 +20,11 @@
20
20
  "types": "./dist/index.d.ts",
21
21
  "import": "./dist/index.mjs",
22
22
  "require": "./dist/index.js"
23
+ },
24
+ "./react": {
25
+ "types": "./dist/react.d.ts",
26
+ "import": "./dist/react.mjs",
27
+ "require": "./dist/react.js"
23
28
  }
24
29
  },
25
30
  "scripts": {
@@ -29,11 +34,28 @@
29
34
  "clean": "rm -rf dist",
30
35
  "lint:ox": "oxlint --fix .",
31
36
  "format": "oxfmt . --write",
32
- "format:check": "oxfmt . --check"
37
+ "format:check": "oxfmt . --check",
38
+ "test": "vitest",
39
+ "test:run": "vitest run",
40
+ "test:coverage": "vitest run --coverage"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=17.0.0"
44
+ },
45
+ "peerDependenciesMeta": {
46
+ "react": {
47
+ "optional": true
48
+ }
33
49
  },
34
50
  "devDependencies": {
51
+ "@testing-library/dom": "^10.0.0",
52
+ "@testing-library/react": "^16.0.0",
53
+ "@types/react": "^19.2.7",
35
54
  "@typescript/native-preview": "^7.0.0-dev.20260109.1",
55
+ "jsdom": "^25.0.0",
56
+ "react": "^19.2.3",
36
57
  "tsup": "^8.5.1",
37
- "typescript": "5.9.3"
58
+ "typescript": "5.9.3",
59
+ "vitest": "^2.1.0"
38
60
  }
39
61
  }