@encatch/web-sdk 0.0.13

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.
@@ -0,0 +1,125 @@
1
+ import { useState } from "preact/hooks";
2
+ import { SubmitFeedback, ViewFeedback } from "@encatch/schema";
3
+ import { EncatchApiSDK } from "@encatch/api-sdk";
4
+ import { moveFeedbackToSubmitted } from "../utils/feedback-storage";
5
+
6
+ export interface SubmitFeedbackResponse {
7
+ success?: boolean;
8
+ error?: string;
9
+ }
10
+
11
+ export const useSubmitFeedbackForm = ({
12
+ apiKey,
13
+ hostUrl,
14
+ }: {
15
+ apiKey: string;
16
+ hostUrl: string;
17
+ }) => {
18
+ const [loadingSubmitFeedback, setLoadingSubmitFeedback] = useState(false);
19
+ const [errorSubmitFeedback, setErrorSubmitFeedback] = useState<string | null>(
20
+ null
21
+ );
22
+ const [successSubmitFeedback, setSuccessSubmitFeedback] = useState(false);
23
+
24
+ // New state for viewFeedback
25
+ const [loadingViewFeedback, setLoadingViewFeedback] = useState(false);
26
+ const [errorViewFeedback, setErrorViewFeedback] = useState<string | null>(
27
+ null
28
+ );
29
+ const [successViewFeedback, setSuccessViewFeedback] = useState(false);
30
+
31
+ // Instantiate the SDK once
32
+ const sdk = new EncatchApiSDK({
33
+ apiKey,
34
+ hostUrl,
35
+ appPackageName: window.location.hostname,
36
+ enableLogging: true,
37
+ });
38
+
39
+ const submitFeedback = async (
40
+ params: SubmitFeedback
41
+ ): Promise<SubmitFeedbackResponse> => {
42
+ setLoadingSubmitFeedback(true);
43
+ setErrorSubmitFeedback(null);
44
+ setSuccessSubmitFeedback(false);
45
+
46
+ try {
47
+ // Ensure sessionInfo is always provided (required by SDK)
48
+ if (!params.sessionInfo) {
49
+ throw new Error("sessionInfo is required for submitting feedback");
50
+ }
51
+
52
+ // Use the SDK's submitFeedback method
53
+ // Type assertion needed because SDK requires sessionInfo to be non-optional
54
+ const result = await sdk.submitFeedback(params as any);
55
+
56
+ if (!result.success) {
57
+ throw new Error(result.error || "Unknown error from SDK");
58
+ }
59
+
60
+ // Move feedback from pending to submitted with timestamp
61
+ const feedbackConfigId = params.formConfig.feedbackConfigurationId;
62
+ if (feedbackConfigId) {
63
+ moveFeedbackToSubmitted(feedbackConfigId);
64
+ }
65
+
66
+ setSuccessSubmitFeedback(true);
67
+ return { success: true };
68
+ } catch (err) {
69
+ const errorMessage =
70
+ err instanceof Error
71
+ ? err.message
72
+ : "An error occurred while submitting feedback";
73
+ setErrorSubmitFeedback(errorMessage);
74
+ return { error: errorMessage };
75
+ } finally {
76
+ setLoadingSubmitFeedback(false);
77
+ }
78
+ };
79
+
80
+ const viewFeedback = async (
81
+ params: ViewFeedback
82
+ ): Promise<SubmitFeedbackResponse> => {
83
+ setLoadingViewFeedback(true);
84
+ setErrorViewFeedback(null);
85
+ setSuccessViewFeedback(false);
86
+
87
+ try {
88
+ // Ensure sessionInfo is always provided (required by SDK)
89
+ if (!params.sessionInfo) {
90
+ throw new Error("sessionInfo is required for viewing feedback");
91
+ }
92
+
93
+ // Use the SDK's viewFeedback method
94
+ // Type assertion needed because SDK requires sessionInfo to be non-optional
95
+ const result = await sdk.viewFeedback(params as any);
96
+
97
+ if (!result.success) {
98
+ throw new Error(result.error || "Unknown error from SDK");
99
+ }
100
+
101
+ setSuccessViewFeedback(true);
102
+ return { success: true };
103
+ } catch (err) {
104
+ const errorMessage =
105
+ err instanceof Error
106
+ ? err.message
107
+ : "An error occurred while viewing feedback";
108
+ setErrorViewFeedback(errorMessage);
109
+ return { error: errorMessage };
110
+ } finally {
111
+ setLoadingViewFeedback(false);
112
+ }
113
+ };
114
+
115
+ return {
116
+ loadingSubmitFeedback,
117
+ errorSubmitFeedback,
118
+ successSubmitFeedback,
119
+ submitFeedback,
120
+ loadingViewFeedback,
121
+ errorViewFeedback,
122
+ successViewFeedback,
123
+ viewFeedback,
124
+ };
125
+ };
@@ -0,0 +1,53 @@
1
+ import { UserInfo } from "@encatch/schema";
2
+ import { useState, useEffect } from "preact/hooks";
3
+
4
+ const USER_STORAGE_KEY = "encatch_app_user";
5
+ const SESSION_STORAGE_KEY = "app_session";
6
+
7
+ function generateSessionId() {
8
+ return crypto.randomUUID();
9
+ }
10
+
11
+ export const useUserSession = () => {
12
+ const [userInfoObject, setUserInfoObject] = useState<UserInfo>({
13
+ userName: "",
14
+ properties: {},
15
+ });
16
+ const [sessionId, setSessionId] = useState<string | null>(null);
17
+
18
+ const updateUserInfo = (userId: string, traits?: Record<string, any>) => {
19
+ const newUserInfo: UserInfo = {
20
+ userName: userId,
21
+ properties: traits || {},
22
+ };
23
+ setUserInfoObject(newUserInfo);
24
+ localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(newUserInfo));
25
+ };
26
+
27
+ useEffect(() => {
28
+ // Handle session - check if there's an existing session
29
+ const storedSessionId = localStorage.getItem(SESSION_STORAGE_KEY);
30
+ if (storedSessionId) {
31
+ setSessionId(storedSessionId);
32
+ } else {
33
+ // Create new session if none exists
34
+ const newSessionId = generateSessionId();
35
+ localStorage.setItem(SESSION_STORAGE_KEY, newSessionId);
36
+ setSessionId(newSessionId);
37
+ }
38
+
39
+ // Handle user info
40
+ const storedUser = localStorage.getItem(USER_STORAGE_KEY);
41
+ if (storedUser) {
42
+ setUserInfoObject(JSON.parse(storedUser));
43
+ }
44
+
45
+ }, []);
46
+
47
+ return {
48
+ userInfoObject,
49
+ updateUserInfo,
50
+ sessionId,
51
+ setUserInfoObject,
52
+ };
53
+ };
package/src/module.tsx ADDED
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Module entry point for @encatch/web-sdk
3
+ *
4
+ * Usage:
5
+ * ```typescript
6
+ * import { Encatch } from '@encatch/web-sdk';
7
+ *
8
+ * const encatch = new Encatch();
9
+ * encatch.init('YOUR_API_KEY', {
10
+ * host: 'https://your-host.com',
11
+ * autoStartEnabled: true
12
+ * });
13
+ * ```
14
+ */
15
+
16
+ import { createEncatchInstance } from "./encatch-instance";
17
+ import { EncatchConfig, FormEventType, FormEventPayload } from "./@types/encatch-type";
18
+
19
+ let coreLoaded = false;
20
+
21
+ /**
22
+ * Load the core wrapper for module usage
23
+ * SSR-safe: Only runs in browser environment
24
+ */
25
+ async function loadCore() {
26
+ // SSR safety check
27
+ if (coreLoaded || typeof window === "undefined" || typeof document === "undefined") return;
28
+ coreLoaded = true;
29
+
30
+ try {
31
+ // Dynamically import and render the core wrapper
32
+ const { render } = await import("preact");
33
+ const CoreWrapper = (await import("./core-wrapper")).default;
34
+
35
+ // Ensure root exists (SSR-safe)
36
+ if (typeof document === "undefined") return;
37
+
38
+ let root = document.getElementById("enisght-root");
39
+ if (!root && document.body) {
40
+ root = document.createElement("div");
41
+ root.id = "enisght-root";
42
+ document.body.appendChild(root);
43
+ }
44
+
45
+ // Render the core wrapper (only if we have a root)
46
+ if (root) {
47
+ render(<CoreWrapper />, root);
48
+ }
49
+ } catch (err) {
50
+ console.error("Failed to load Encatch core:", err);
51
+ }
52
+ }
53
+
54
+ // Singleton instance for static API
55
+ let defaultInstance: Encatch | null = null;
56
+
57
+ /**
58
+ * Encatch SDK class for module-based usage
59
+ */
60
+ export class Encatch {
61
+ private instance: EncatchGlobal;
62
+
63
+ constructor() {
64
+ this.instance = createEncatchInstance();
65
+
66
+ // Sync instance with window.encatch for core-wrapper compatibility
67
+ // This ensures core-wrapper can access the instance
68
+ if (typeof window !== "undefined") {
69
+ // Only set if not already set (to avoid overwriting CDN version)
70
+ if (!window.encatch) {
71
+ window.encatch = this.instance;
72
+ }
73
+ }
74
+
75
+ // Ensure the root div exists (SSR-safe)
76
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
77
+ if (!document.getElementById("enisght-root")) {
78
+ const rootDiv = document.createElement("div");
79
+ rootDiv.id = "enisght-root";
80
+ if (document.body) {
81
+ document.body.appendChild(rootDiv);
82
+ } else {
83
+ // Wait for DOM to be ready
84
+ if (document.readyState === "loading") {
85
+ document.addEventListener("DOMContentLoaded", () => {
86
+ if (!document.getElementById("enisght-root") && document.body) {
87
+ document.body.appendChild(rootDiv);
88
+ }
89
+ });
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Static method to initialize the default Encatch instance
98
+ * This provides a convenient API: Encatch.init(apiKey, options)
99
+ */
100
+ static init(apiKey: string, options?: EncatchConfig): Encatch {
101
+ if (!defaultInstance) {
102
+ defaultInstance = new Encatch();
103
+ }
104
+ defaultInstance.init(apiKey, options);
105
+ return defaultInstance;
106
+ }
107
+
108
+ /**
109
+ * Get the default singleton instance
110
+ */
111
+ static getInstance(): Encatch {
112
+ if (!defaultInstance) {
113
+ defaultInstance = new Encatch();
114
+ }
115
+ return defaultInstance;
116
+ }
117
+
118
+ /**
119
+ * Initialize the Encatch SDK
120
+ * SSR-safe: Can be called on server, but only initializes in browser
121
+ */
122
+ init(apiKey: string, options?: EncatchConfig): void {
123
+ // SSR safety check
124
+ if (typeof window === "undefined" || typeof document === "undefined") {
125
+ // In SSR environment, just store config for later initialization
126
+ this.instance.apiKey = apiKey;
127
+ if (options) {
128
+ Object.assign(this.instance.config, options);
129
+ }
130
+ return;
131
+ }
132
+
133
+ // Override the init to load core directly instead of via script tag
134
+ if (this.instance.initialized) return;
135
+
136
+ // Update all config first
137
+ this.instance.apiKey = apiKey;
138
+ // Set host with default if not provided
139
+ this.instance.config.host = options?.host || "https://app.encatch.com";
140
+ if (options?.autoStartSessionDisabled !== undefined) {
141
+ this.instance.config.autoStartSessionDisabled =
142
+ options.autoStartSessionDisabled;
143
+ }
144
+ if (options?.processAfterIdentitySet !== undefined) {
145
+ this.instance.config.processAfterIdentitySet =
146
+ options.processAfterIdentitySet;
147
+ } else {
148
+ this.instance.config.processAfterIdentitySet = false;
149
+ }
150
+
151
+ if (options?.autoStartEnabled !== undefined) {
152
+ this.instance.config.autoStartEnabled = options.autoStartEnabled;
153
+ }
154
+ if (options?.themeMode) {
155
+ this.instance.config.themeMode = options.themeMode;
156
+ } else {
157
+ this.instance.config.themeMode = "light";
158
+ }
159
+ if (options?.language) {
160
+ this.instance.config.language = options.language;
161
+ } else {
162
+ this.instance.config.language = "en";
163
+ }
164
+
165
+ if (options?.customCssLink) {
166
+ this.instance.config.customCssLink = options.customCssLink;
167
+ }
168
+
169
+ if (options?.setUser) {
170
+ this.instance.config.setUser = options.setUser;
171
+ }
172
+
173
+ if (options?.onFormEvent) {
174
+ this.instance.config.onFormEvent = options.onFormEvent;
175
+ }
176
+
177
+ if (options?.customProperties) {
178
+ this.instance.config.customProperties = options.customProperties;
179
+ }
180
+
181
+ if (options?.onSessionDisabled) {
182
+ this.instance.config.onSessionDisabled = options.onSessionDisabled;
183
+ }
184
+
185
+ // Mark as initialized
186
+ this.instance.initialized = true;
187
+
188
+ // Sync instance with window.encatch AFTER all config is set
189
+ // This ensures core-wrapper can access all properties
190
+ if (typeof window !== "undefined") {
191
+ window.encatch = this.instance;
192
+ }
193
+
194
+ // Load core directly for module usage (after window.encatch is set)
195
+ loadCore();
196
+ }
197
+
198
+ /**
199
+ * Track a custom event
200
+ */
201
+ trackEvent(eventName: string, properties?: Record<string, any>): void {
202
+ if (this.instance.trackEvent) {
203
+ this.instance.trackEvent(eventName, properties);
204
+ } else {
205
+ this.instance._i.push(["trackEvent", eventName, properties]);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Start the SDK session
211
+ */
212
+ start(
213
+ userId?: string,
214
+ traits?: {
215
+ $set?: Record<string, any>;
216
+ $set_once?: Record<string, any>;
217
+ $counter?: Record<string, any>;
218
+ $unset?: string[];
219
+ [key: string]: any;
220
+ }
221
+ ): void {
222
+ if (this.instance.start) {
223
+ this.instance.start(userId, traits);
224
+ } else {
225
+ this.instance._i.push(["start", userId, traits]);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Stop the SDK session
231
+ */
232
+ stop(): void {
233
+ if (this.instance.stop) {
234
+ this.instance.stop();
235
+ } else {
236
+ this.instance._i.push(["stop"]);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Set or update user identity
242
+ */
243
+ setUser(
244
+ userId?: string,
245
+ traits?: {
246
+ $set?: Record<string, any>;
247
+ $set_once?: Record<string, any>;
248
+ $counter?: Record<string, any>;
249
+ $unset?: string[];
250
+ [key: string]: any;
251
+ }
252
+ ): void {
253
+ if (this.instance.setUser) {
254
+ this.instance.setUser(userId, traits);
255
+ } else {
256
+ this.instance._i.push(["setUser", userId, traits]);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Set theme mode
262
+ */
263
+ setThemeMode(theme: "light" | "dark"): void {
264
+ if (this.instance.setThemeMode) {
265
+ this.instance.setThemeMode(theme);
266
+ } else {
267
+ this.instance._i.push(["setThemeMode", theme]);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Set language
273
+ */
274
+ setLanguage(language: string): void {
275
+ if (this.instance.setLanguage) {
276
+ this.instance.setLanguage(language);
277
+ } else {
278
+ this.instance._i.push(["setLanguage", language]);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Set custom properties
284
+ */
285
+ setCustomProperties(customProperties: Record<string, string>): void {
286
+ if (this.instance.setCustomProperties) {
287
+ this.instance.setCustomProperties(customProperties);
288
+ } else {
289
+ this.instance._i.push(["setCustomProperties", customProperties]);
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Open feedback by configuration ID
295
+ */
296
+ openFeedbackById(
297
+ feedbackConfigurationId: string,
298
+ theme?: "light" | "dark",
299
+ language?: string,
300
+ event?: string,
301
+ customProperties?: Record<string, string>,
302
+ prepopulatedAnswers?: any[]
303
+ ): void {
304
+ if (this.instance.openFeedbackById) {
305
+ this.instance.openFeedbackById(
306
+ feedbackConfigurationId,
307
+ theme,
308
+ language,
309
+ event,
310
+ customProperties,
311
+ prepopulatedAnswers
312
+ );
313
+ } else {
314
+ this.instance._i.push([
315
+ "openFeedbackById",
316
+ feedbackConfigurationId,
317
+ theme,
318
+ language,
319
+ event,
320
+ customProperties,
321
+ prepopulatedAnswers,
322
+ ]);
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Open feedback by configuration name
328
+ */
329
+ openFeedbackByName(
330
+ feedbackConfigurationName: string,
331
+ theme?: "light" | "dark",
332
+ language?: string,
333
+ event?: string,
334
+ customProperties?: Record<string, string>,
335
+ prepopulatedAnswers?: any[]
336
+ ): void {
337
+ if (this.instance.openFeedbackByName) {
338
+ this.instance.openFeedbackByName(
339
+ feedbackConfigurationName,
340
+ theme,
341
+ language,
342
+ event,
343
+ customProperties,
344
+ prepopulatedAnswers
345
+ );
346
+ } else {
347
+ this.instance._i.push([
348
+ "openFeedbackByName",
349
+ feedbackConfigurationName,
350
+ theme,
351
+ language,
352
+ event,
353
+ customProperties,
354
+ prepopulatedAnswers,
355
+ ]);
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Verify which feedback IDs are eligible
361
+ */
362
+ verifyFeedbackIds(feedbackConfigurationIds: string[]): string[] {
363
+ if (this.instance.verifyFeedbackIds) {
364
+ return this.instance.verifyFeedbackIds(feedbackConfigurationIds);
365
+ }
366
+ return [];
367
+ }
368
+
369
+ /**
370
+ * Force fetch eligible feedbacks
371
+ */
372
+ async forceFetchEligibleFeedbacks(): Promise<void> {
373
+ if (this.instance.forceFetchEligibleFeedbacks) {
374
+ return this.instance.forceFetchEligibleFeedbacks();
375
+ }
376
+ return Promise.resolve();
377
+ }
378
+
379
+ /**
380
+ * Capture page scroll event
381
+ */
382
+ capturePageScrollEvent(scrollPercent: string): void {
383
+ if (this.instance.capturePageScrollEvent) {
384
+ this.instance.capturePageScrollEvent(scrollPercent);
385
+ } else {
386
+ this.instance._i.push(["capturePageScrollEvent", scrollPercent]);
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Subscribe to form events
392
+ */
393
+ on<T extends FormEventType>(
394
+ eventType: T,
395
+ callback: (payload: FormEventPayload[T]) => void
396
+ ): () => void {
397
+ if (this.instance.on) {
398
+ return this.instance.on(eventType, callback);
399
+ }
400
+ // Fallback to queue
401
+ const subscription = { eventType, callback };
402
+ this.instance._eventSubscriptions = this.instance._eventSubscriptions || [];
403
+ this.instance._eventSubscriptions.push(subscription);
404
+ return () => {
405
+ const index = this.instance._eventSubscriptions?.indexOf(subscription);
406
+ if (index !== undefined && index > -1) {
407
+ this.instance._eventSubscriptions?.splice(index, 1);
408
+ }
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Get the internal instance (for advanced usage)
414
+ */
415
+ getInstance(): EncatchGlobal {
416
+ return this.instance;
417
+ }
418
+ }
419
+
420
+ // Export a default instance for convenience (SSR-safe)
421
+ export const encatch = typeof window !== "undefined" ? new Encatch() : (null as any);
422
+
423
+ // Export types
424
+ export type { EncatchConfig, OnFormEventHandler, FormEventBuilder } from "./@types/encatch-type";
425
+
426
+ // Encatch class is already exported above with "export class Encatch"
427
+