@encatch/web-sdk 0.0.35 → 1.0.0-beta.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.
Files changed (33) hide show
  1. package/README.md +140 -638
  2. package/dist/encatch.es.js +2 -0
  3. package/dist/encatch.es.js.map +1 -0
  4. package/dist/encatch.iife.js +2 -0
  5. package/dist/encatch.iife.js.map +1 -0
  6. package/dist/index.d.ts +191 -0
  7. package/package.json +32 -50
  8. package/dist-sdk/plugin/sdk/core-wrapper-BMvOyc0u.js +0 -22926
  9. package/dist-sdk/plugin/sdk/module-DC2Edddk.js +0 -481
  10. package/dist-sdk/plugin/sdk/module.js +0 -5
  11. package/dist-sdk/plugin/sdk/preview-sdk.html +0 -1182
  12. package/dist-sdk/plugin/sdk/vite.svg +0 -15
  13. package/dist-sdk/plugin/sdk/web-form-engine-core.css +0 -1
  14. package/index.d.ts +0 -207
  15. package/src/@types/encatch-type.ts +0 -111
  16. package/src/encatch-instance.ts +0 -161
  17. package/src/feedback-api-types.ts +0 -18
  18. package/src/hooks/useDevice.ts +0 -30
  19. package/src/hooks/useFeedbackInterval.ts +0 -71
  20. package/src/hooks/useFeedbackTriggers.ts +0 -342
  21. package/src/hooks/useFetchElligibleFeedbackConfiguration.ts +0 -363
  22. package/src/hooks/useFetchFeedbackConfigurationDetails.ts +0 -92
  23. package/src/hooks/usePageChangeTracker.ts +0 -88
  24. package/src/hooks/usePrepopulatedAnswers.ts +0 -123
  25. package/src/hooks/useRefineTextForm.ts +0 -55
  26. package/src/hooks/useSubmitFeedbackForm.ts +0 -134
  27. package/src/hooks/useUserSession.ts +0 -53
  28. package/src/module.tsx +0 -428
  29. package/src/store/formResponses.ts +0 -211
  30. package/src/utils/browser-details.ts +0 -36
  31. package/src/utils/duration-utils.ts +0 -158
  32. package/src/utils/feedback-frequency-storage.ts +0 -214
  33. package/src/utils/feedback-storage.ts +0 -166
@@ -1,211 +0,0 @@
1
- import { signal, computed } from "@preact/signals";
2
-
3
- import {
4
- Answer,
5
- CombinedQuestion,
6
- QuestionResponse,
7
- QuestionType,
8
- QuestionTypes,
9
- } from "@encatch/schema";
10
-
11
- // Type for the entire form responses store
12
- interface FormResponsesStore {
13
- questions: QuestionResponse[];
14
- questionsData: Record<string, CombinedQuestion>;
15
- }
16
-
17
- // Create the initial store state
18
- const initialStore: FormResponsesStore = {
19
- questions: [],
20
- questionsData: {},
21
- };
22
-
23
- // Create signals for the store
24
- const formResponsesSignal = signal<FormResponsesStore>(initialStore);
25
-
26
- // Create a computed property to access the store (similar to Solid's store access)
27
- const formResponses = computed(() => formResponsesSignal.value);
28
-
29
- // Helper function to update the store
30
- const setFormResponses = (
31
- key: keyof FormResponsesStore,
32
- updater: any[] | Record<string, any> | ((current: any) => any)
33
- ) => {
34
- formResponsesSignal.value = {
35
- ...formResponsesSignal.value,
36
- [key]:
37
- typeof updater === "function"
38
- ? updater(formResponsesSignal.value[key])
39
- : updater,
40
- };
41
- };
42
-
43
- // Helper function to update a question response
44
- const updateQuestionResponse = (
45
- questionId: string,
46
- answerValue: any,
47
- type: QuestionType,
48
- error?: string
49
- ) => {
50
- // Check if answer is empty and should be removed instead
51
- const isEmpty =
52
- answerValue === null ||
53
- answerValue === undefined ||
54
- answerValue === "" ||
55
- (Array.isArray(answerValue) && answerValue.length === 0);
56
-
57
- // If empty and no error, remove the question response instead of updating it
58
- if (isEmpty && !error) {
59
- removeQuestionResponse(questionId);
60
- return;
61
- }
62
-
63
- // Transform the answer value into the correct format based on question type
64
- let formattedAnswer: Answer;
65
-
66
- switch (type) {
67
- case QuestionTypes.RATING:
68
- formattedAnswer = { rating: answerValue };
69
- break;
70
- case QuestionTypes.NPS:
71
- formattedAnswer = { nps: answerValue };
72
- break;
73
- case QuestionTypes.LONG_TEXT:
74
- formattedAnswer = { longText: answerValue };
75
- break;
76
- case QuestionTypes.SHORT_ANSWER:
77
- formattedAnswer = { shortAnswer: answerValue };
78
- break;
79
- case QuestionTypes.SINGLE_CHOICE:
80
- formattedAnswer = { singleChoice: answerValue };
81
- break;
82
- case QuestionTypes.MULTIPLE_CHOICE_MULTIPLE:
83
- formattedAnswer = {
84
- multipleChoiceMultiple: Array.isArray(answerValue)
85
- ? answerValue
86
- : [answerValue],
87
- };
88
- break;
89
- case QuestionTypes.NESTED_SELECTION:
90
- formattedAnswer = {
91
- nestedSelection: Array.isArray(answerValue)
92
- ? answerValue
93
- : [answerValue],
94
- };
95
- break;
96
- case QuestionTypes.ANNOTATION:
97
- formattedAnswer = { annotation: answerValue };
98
- break;
99
- default:
100
- // If we can't determine the type, just use the answer as is (assuming it's already correctly formatted)
101
- formattedAnswer = answerValue as Answer;
102
- }
103
-
104
- setFormResponses("questions", (questions) => {
105
- const existingIndex = questions.findIndex(
106
- (q) => q.questionId === questionId
107
- );
108
-
109
- if (existingIndex !== -1) {
110
- // Update existing response
111
- return questions.map((q, index) =>
112
- index === existingIndex
113
- ? { ...q, answer: formattedAnswer, type, error }
114
- : q
115
- );
116
- } else {
117
- // Add new response
118
- return [
119
- ...questions,
120
- { questionId: questionId, answer: formattedAnswer, type, error },
121
- ];
122
- }
123
- });
124
-
125
- // Emit the event after updating the store
126
- // surveySDK.emit("question_answered", {
127
- // feedbackId: "your-feedback-id", // <-- Replace with actual feedbackId from context/store
128
- // questionId,
129
- // questionText: getQuestionData(questionId)?.title || "",
130
- // answer: formattedAnswer,
131
- // timestamp: Date.now(),
132
- // });
133
- };
134
-
135
- // Helper function to set/clear validation errors for a question
136
- const setQuestionError = (questionId: string, error?: string) => {
137
- const existingQuestion = getQuestionResponse(questionId);
138
- if (existingQuestion) {
139
- // Update the error for existing question
140
- setFormResponses("questions", (questions) =>
141
- questions.map((q) => (q.questionId === questionId ? { ...q, error } : q))
142
- );
143
- } else if (error) {
144
- // If there's an error but no existing question response, create one with empty answer
145
- // This is useful for required fields that haven't been filled out yet
146
- setFormResponses("questions", (questions) => [
147
- ...questions,
148
- {
149
- questionId: questionId,
150
- answer: {} as any,
151
- type: QuestionTypes.SHORT_ANSWER,
152
- error,
153
- },
154
- ]);
155
- }
156
- };
157
-
158
- // Helper function to clear all validation errors in the form
159
- const clearValidationErrors = () => {
160
- setFormResponses("questions", (questions) =>
161
- questions.map((q) => ({ ...q, error: undefined }))
162
- );
163
- };
164
-
165
- // Helper function to check if there are any validation errors in the form
166
- const hasValidationErrors = () => {
167
- return formResponsesSignal.value.questions.some(
168
- (q) => q.error !== undefined && q.error !== null && q.error !== ""
169
- );
170
- };
171
-
172
- // Helper function to remove a question response
173
- const removeQuestionResponse = (questionId: string) => {
174
- setFormResponses("questions", (questions) =>
175
- questions.filter((q) => q.questionId !== questionId)
176
- );
177
- };
178
-
179
- // Helper function to get a question response
180
- const getQuestionResponse = (questionId: string) => {
181
- return formResponsesSignal.value.questions.find(
182
- (q) => q.questionId === questionId
183
- );
184
- };
185
-
186
- // Helper function to clear all responses
187
- const clearResponses = () => {
188
- setFormResponses("questions", []);
189
- };
190
-
191
- // Function to store questions data
192
- const storeQuestions = (questions: Record<string, CombinedQuestion>) => {
193
- setFormResponses("questionsData", questions);
194
- };
195
-
196
- const getQuestionData = (questionId: string): CombinedQuestion | undefined => {
197
- return formResponsesSignal.value.questionsData[questionId];
198
- };
199
-
200
- export {
201
- formResponses,
202
- updateQuestionResponse,
203
- getQuestionResponse,
204
- removeQuestionResponse,
205
- setQuestionError,
206
- hasValidationErrors,
207
- clearValidationErrors,
208
- clearResponses,
209
- storeQuestions,
210
- getQuestionData,
211
- };
@@ -1,36 +0,0 @@
1
-
2
- //import DeviceDetector from "device-detector-js";
3
- import { DeviceInfo } from "@encatch/schema";
4
- import { UAParser } from "ua-parser-js";
5
-
6
- export const getDeviceInfo = (userAgent: string, deviceId: string): DeviceInfo => {
7
- const { browser, device, os } = UAParser(userAgent);
8
- const resolvedDeviceType =
9
- device?.type ||
10
- (os?.name?.toLowerCase().includes("windows") ||
11
- os?.name?.toLowerCase().includes("mac") ||
12
- os?.name?.toLowerCase().includes("linux")
13
- ? "desktop"
14
- : "Unknown");
15
-
16
- const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
17
- const theme = isDarkMode ? "dark" : "light";
18
-
19
- // console.log((new UAParser(userAgent)).getResult());
20
- return {
21
- $deviceType: resolvedDeviceType,
22
- $timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
23
- $theme: theme || "system", // This would need to be determined separately
24
- $os: os?.name || "Unknown",
25
- $osVersion: os?.version || "Unknown",
26
- $appVersion: browser?.version || "Unknown", // This would need to be provided from app context
27
- $app: browser?.name || "Unknown", // This would need to be provided from app context
28
- $language: typeof navigator !== "undefined" ? navigator.language : "Unknown",
29
- $deviceId: deviceId,
30
- };
31
- };
32
-
33
- // Helper Function to Generate a UUID
34
- export function generateUUID(): string {
35
- return crypto.randomUUID();
36
- }
@@ -1,158 +0,0 @@
1
- /**
2
- * Utility functions for duration calculations and frequency eligibility
3
- */
4
-
5
- import { FeedbackFrequencyTracking } from "./feedback-frequency-storage";
6
-
7
- /**
8
- * Convert duration value and unit to milliseconds
9
- */
10
- export const convertDurationToMs = (
11
- value: string | number,
12
- unit: "minutes" | "hours" | "days" | "weeks" | "months" | "years"
13
- ): number => {
14
- const numValue = typeof value === "string" ? parseInt(value, 10) : value;
15
-
16
- if (isNaN(numValue) || numValue < 0) {
17
- console.warn(
18
- `Invalid duration value: ${value}. Defaulting to 0 milliseconds.`
19
- );
20
- return 0;
21
- }
22
-
23
- switch (unit) {
24
- case "minutes":
25
- return numValue * 60 * 1000;
26
- case "hours":
27
- return numValue * 60 * 60 * 1000;
28
- case "days":
29
- return numValue * 24 * 60 * 60 * 1000;
30
- case "weeks":
31
- return numValue * 7 * 24 * 60 * 60 * 1000;
32
- case "months":
33
- return numValue * 30 * 24 * 60 * 60 * 1000; // Approximate
34
- case "years":
35
- return numValue * 365 * 24 * 60 * 60 * 1000; // Approximate
36
- default:
37
- console.error(`Unknown duration unit: ${unit}. Defaulting to 0.`);
38
- return 0;
39
- }
40
- };
41
-
42
- /**
43
- * Check if a feedback is eligible based on frequency rules
44
- * @param tracking - The frequency tracking data
45
- * @param currentTime - Current timestamp in milliseconds
46
- * @param isManual - Whether this is a manual feedback (only checks date range, bypasses all other rules)
47
- */
48
- export const isEligibleByFrequency = (
49
- tracking: FeedbackFrequencyTracking,
50
- currentTime: number = Date.now(),
51
- isManual: boolean = false
52
- ): boolean => {
53
- const {
54
- frequencyConfig,
55
- lastSubmittedAt,
56
- lastDismissedAt,
57
- responseCount,
58
- } = tracking;
59
-
60
- // For manual feedbacks: Only check start/stop dates, bypass all other frequency rules
61
- if (isManual) {
62
- // Check if within date range
63
- const startDate = new Date(frequencyConfig.startDate).getTime();
64
- const stopDate = new Date(frequencyConfig.stopDate).getTime();
65
-
66
- return currentTime >= startDate && currentTime <= stopDate;
67
- }
68
-
69
- // For non-manual feedbacks: Apply all frequency rules
70
-
71
- // Check if response limit reached
72
- const stopCount = parseInt(frequencyConfig.stopWhenResponsesCount, 10);
73
-
74
- if (stopCount > 0 && responseCount >= stopCount) {
75
- return false;
76
- }
77
-
78
- // For non-recurring types (S = one-time), check if already submitted
79
- if (frequencyConfig.surveyType !== "R") {
80
- const eligible = responseCount === 0;
81
- return eligible;
82
- }
83
-
84
- // For recurring type "R"
85
- const durationMs = convertDurationToMs(
86
- frequencyConfig.recurringValue,
87
- frequencyConfig.recurringUnit
88
- );
89
-
90
- // Determine which timestamp to use based on showOnce
91
- let referenceTime: number | null = null;
92
-
93
- if (frequencyConfig.showOnce === "Y") {
94
- // Use the most recent of submitted or dismissed
95
- const maxTime = Math.max(lastSubmittedAt || 0, lastDismissedAt || 0);
96
- referenceTime = maxTime > 0 ? maxTime : null;
97
- } else {
98
- // Only use submission time
99
- referenceTime = lastSubmittedAt;
100
- }
101
-
102
- // If never interacted (or only dismissed when showOnce=N), it's eligible
103
- if (referenceTime === null) {
104
- return true;
105
- }
106
-
107
- // Check if enough time has passed
108
- const nextEligibleTime = referenceTime + durationMs;
109
- const eligible = currentTime >= nextEligibleTime;
110
-
111
- return eligible;
112
- };
113
-
114
- /**
115
- * Calculate the next eligible time for a feedback
116
- * Returns null if already eligible or never interacted
117
- */
118
- export const getNextEligibleTime = (
119
- tracking: FeedbackFrequencyTracking
120
- ): Date | null => {
121
- const {
122
- frequencyConfig,
123
- lastSubmittedAt,
124
- lastDismissedAt,
125
- responseCount,
126
- } = tracking;
127
-
128
- // If response limit reached, never eligible again
129
- const stopCount = parseInt(frequencyConfig.stopWhenResponsesCount, 10);
130
- if (stopCount > 0 && responseCount >= stopCount) {
131
- return null;
132
- }
133
-
134
- // For non-recurring, if already submitted, never eligible again
135
- if (frequencyConfig.surveyType !== "R" && responseCount > 0) {
136
- return null;
137
- }
138
-
139
- // If never interacted, eligible now
140
- let referenceTime: number | null = null;
141
- if (frequencyConfig.showOnce === "Y") {
142
- const maxTime = Math.max(lastSubmittedAt || 0, lastDismissedAt || 0);
143
- referenceTime = maxTime > 0 ? maxTime : null;
144
- } else {
145
- referenceTime = lastSubmittedAt;
146
- }
147
-
148
- if (referenceTime === null) {
149
- return null; // Already eligible
150
- }
151
-
152
- const durationMs = convertDurationToMs(
153
- frequencyConfig.recurringValue,
154
- frequencyConfig.recurringUnit
155
- );
156
-
157
- return new Date(referenceTime + durationMs);
158
- };
@@ -1,214 +0,0 @@
1
- /**
2
- * Storage utilities for tracking feedback frequency and eligibility
3
- * Uses localStorage for persistence across sessions
4
- */
5
-
6
- /**
7
- * Feedback frequency tracking data
8
- * Matches the actual schema structure from @encatch/schema
9
- */
10
- export interface FeedbackFrequencyTracking {
11
- feedbackConfigurationId: string;
12
- lastSubmittedAt: number | null; // timestamp in ms
13
- lastDismissedAt: number | null; // timestamp when viewed but not submitted
14
- responseCount: number; // total number of submissions
15
- frequencyConfig: {
16
- surveyType: "R" | "S"; // "R" = recurring, "S" = one-time
17
- recurringValue: string; // e.g., "15"
18
- recurringUnit: "minutes" | "hours" | "days" | "weeks" | "months" | "years";
19
- showOnce: "Y" | "N"; // "Y" = show once after dismissed/submitted, "N" = can show again
20
- stopWhenResponsesCount: number; // e.g., "5" or "0" for unlimited
21
- startDate: string; // ISO date string
22
- stopDate: string; // ISO date string
23
- };
24
- lastUpdated: number; // timestamp of last update
25
- }
26
-
27
- /**
28
- * Storage key for frequency tracking
29
- */
30
- const FREQUENCY_TRACKING_KEY = "encatch_frequency_tracking";
31
-
32
- /**
33
- * Get all frequency tracking data from localStorage
34
- */
35
- export const getFrequencyTracking = (): Record<
36
- string,
37
- FeedbackFrequencyTracking
38
- > => {
39
- try {
40
- const stored = localStorage.getItem(FREQUENCY_TRACKING_KEY);
41
- if (!stored) return {};
42
-
43
- const parsed = JSON.parse(stored);
44
- return parsed;
45
- } catch (error) {
46
- console.error("[Frequency Storage] Error reading tracking data:", error);
47
- return {};
48
- }
49
- };
50
-
51
- /**
52
- * Get frequency tracking for a specific feedback
53
- */
54
- export const getFrequencyTrackingById = (
55
- feedbackConfigurationId: string
56
- ): FeedbackFrequencyTracking | null => {
57
- const allTracking = getFrequencyTracking();
58
- return allTracking[feedbackConfigurationId] || null;
59
- };
60
-
61
- /**
62
- * Update frequency tracking for a feedback
63
- * @param tracking - The tracking data to update
64
- */
65
- export const updateFrequencyTracking = (
66
- tracking: FeedbackFrequencyTracking
67
- ): void => {
68
- try {
69
- const allTracking = getFrequencyTracking();
70
- tracking.lastUpdated = Date.now();
71
- allTracking[tracking.feedbackConfigurationId] = tracking;
72
- localStorage.setItem(
73
- FREQUENCY_TRACKING_KEY,
74
- JSON.stringify(allTracking)
75
- );
76
- } catch (error) {
77
- console.error("[Frequency Storage] Error updating tracking:", error);
78
- }
79
- };
80
-
81
- /**
82
- * Record a feedback submission
83
- */
84
- export const recordFeedbackSubmission = (
85
- feedbackConfigurationId: string,
86
- frequencyConfig: FeedbackFrequencyTracking["frequencyConfig"]
87
- ): void => {
88
- const existing = getFrequencyTrackingById(feedbackConfigurationId);
89
-
90
- const newTracking: FeedbackFrequencyTracking = {
91
- feedbackConfigurationId,
92
- lastSubmittedAt: Date.now(),
93
- lastDismissedAt: existing?.lastDismissedAt || null, // Preserve dismissed time
94
- responseCount: (existing?.responseCount || 0) + 1,
95
- frequencyConfig,
96
- lastUpdated: Date.now(),
97
- };
98
-
99
- updateFrequencyTracking(newTracking);
100
- };
101
-
102
- /**
103
- * Record a feedback dismissal (viewed but not submitted)
104
- */
105
- export const recordFeedbackDismissal = (
106
- feedbackConfigurationId: string,
107
- frequencyConfig: FeedbackFrequencyTracking["frequencyConfig"]
108
- ): void => {
109
- // Only record dismissal if showOnce is "Y"
110
- if (frequencyConfig.showOnce !== "Y") {
111
- return;
112
- }
113
-
114
- const existing = getFrequencyTrackingById(feedbackConfigurationId);
115
-
116
- const newTracking: FeedbackFrequencyTracking = {
117
- feedbackConfigurationId,
118
- lastSubmittedAt: existing?.lastSubmittedAt || null,
119
- lastDismissedAt: Date.now(),
120
- responseCount: existing?.responseCount || 0,
121
- frequencyConfig,
122
- lastUpdated: Date.now(),
123
- };
124
-
125
- updateFrequencyTracking(newTracking);
126
- };
127
-
128
- /**
129
- * Initialize or update tracking with latest config from server
130
- * This ensures we always have the latest frequency rules
131
- */
132
- export const initializeOrUpdateTracking = (
133
- feedbackConfigurationId: string,
134
- frequencyConfig: FeedbackFrequencyTracking["frequencyConfig"]
135
- ): void => {
136
- const existing = getFrequencyTrackingById(feedbackConfigurationId);
137
-
138
- if (existing) {
139
- // Update existing tracking with latest config
140
- existing.frequencyConfig = frequencyConfig;
141
- updateFrequencyTracking(existing);
142
- } else {
143
- // Initialize new tracking
144
- const newTracking: FeedbackFrequencyTracking = {
145
- feedbackConfigurationId,
146
- lastSubmittedAt: null,
147
- lastDismissedAt: null,
148
- responseCount: 0,
149
- frequencyConfig,
150
- lastUpdated: Date.now(),
151
- };
152
- updateFrequencyTracking(newTracking);
153
- }
154
- };
155
-
156
- /**
157
- * Clear frequency tracking for a specific feedback
158
- */
159
- export const clearFrequencyTrackingById = (
160
- feedbackConfigurationId: string
161
- ): void => {
162
- try {
163
- const allTracking = getFrequencyTracking();
164
- delete allTracking[feedbackConfigurationId];
165
- localStorage.setItem(
166
- FREQUENCY_TRACKING_KEY,
167
- JSON.stringify(allTracking)
168
- );
169
- } catch (error) {
170
- console.error(
171
- "[Frequency Storage] Error clearing tracking by ID:",
172
- error
173
- );
174
- }
175
- };
176
-
177
- /**
178
- * Clear all frequency tracking data
179
- */
180
- export const clearAllFrequencyTracking = (): void => {
181
- try {
182
- localStorage.removeItem(FREQUENCY_TRACKING_KEY);
183
- } catch (error) {
184
- console.error("[Frequency Storage] Error clearing all tracking:", error);
185
- }
186
- };
187
-
188
- /**
189
- * Get statistics about tracked feedbacks
190
- */
191
- export const getFrequencyTrackingStats = (): {
192
- totalTracked: number;
193
- totalSubmissions: number;
194
- feedbacksWithResponses: number;
195
- feedbacksAtLimit: number;
196
- } => {
197
- const allTracking = getFrequencyTracking();
198
- const trackingArray = Object.values(allTracking);
199
-
200
- return {
201
- totalTracked: trackingArray.length,
202
- totalSubmissions: trackingArray.reduce(
203
- (sum, t) => sum + t.responseCount,
204
- 0
205
- ),
206
- feedbacksWithResponses: trackingArray.filter((t) => t.responseCount > 0)
207
- .length,
208
- feedbacksAtLimit: trackingArray.filter(
209
- (t) =>
210
- t.frequencyConfig.stopWhenResponsesCount > 0 &&
211
- t.responseCount >= t.frequencyConfig.stopWhenResponsesCount
212
- ).length,
213
- };
214
- };