@encatch/web-sdk 0.0.35-beta.8 → 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.
- package/README.md +140 -638
- package/dist/encatch.es.js +2 -0
- package/dist/encatch.es.js.map +1 -0
- package/dist/encatch.iife.js +2 -0
- package/dist/encatch.iife.js.map +1 -0
- package/dist/index.d.ts +191 -0
- package/package.json +32 -50
- package/dist-sdk/plugin/sdk/core-wrapper-BMvOyc0u.js +0 -22926
- package/dist-sdk/plugin/sdk/module-DC2Edddk.js +0 -481
- package/dist-sdk/plugin/sdk/module.js +0 -5
- package/dist-sdk/plugin/sdk/preview-sdk.html +0 -1182
- package/dist-sdk/plugin/sdk/vite.svg +0 -15
- package/dist-sdk/plugin/sdk/web-form-engine-core.css +0 -1
- package/index.d.ts +0 -207
- package/src/@types/encatch-type.ts +0 -111
- package/src/encatch-instance.ts +0 -161
- package/src/feedback-api-types.ts +0 -18
- package/src/hooks/useDevice.ts +0 -30
- package/src/hooks/useFeedbackInterval.ts +0 -71
- package/src/hooks/useFeedbackTriggers.ts +0 -342
- package/src/hooks/useFetchElligibleFeedbackConfiguration.ts +0 -363
- package/src/hooks/useFetchFeedbackConfigurationDetails.ts +0 -92
- package/src/hooks/usePageChangeTracker.ts +0 -88
- package/src/hooks/usePrepopulatedAnswers.ts +0 -123
- package/src/hooks/useRefineTextForm.ts +0 -55
- package/src/hooks/useSubmitFeedbackForm.ts +0 -134
- package/src/hooks/useUserSession.ts +0 -53
- package/src/module.tsx +0 -428
- package/src/store/formResponses.ts +0 -211
- package/src/utils/browser-details.ts +0 -36
- package/src/utils/duration-utils.ts +0 -158
- package/src/utils/feedback-frequency-storage.ts +0 -214
- package/src/utils/feedback-storage.ts +0 -166
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from "preact/hooks";
|
|
2
|
-
import {
|
|
3
|
-
FetchConfigurationListRequest,
|
|
4
|
-
DeviceInfo,
|
|
5
|
-
FeedbackConfigurationItem,
|
|
6
|
-
MasterProperties,
|
|
7
|
-
} from "@encatch/schema";
|
|
8
|
-
import { EncatchApiSDK } from "@encatch/api-sdk";
|
|
9
|
-
import { setPendingFeedbacks, getPendingFeedbacks } from "../utils/feedback-storage";
|
|
10
|
-
import {
|
|
11
|
-
getFrequencyTracking,
|
|
12
|
-
initializeOrUpdateTracking,
|
|
13
|
-
} from "../utils/feedback-frequency-storage";
|
|
14
|
-
import { isEligibleByFrequency } from "../utils/duration-utils";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Filter feedbacks based on frequency rules
|
|
18
|
-
*/
|
|
19
|
-
const filterByFrequencyRules = (
|
|
20
|
-
feedbacks: FeedbackConfigurationItem[]
|
|
21
|
-
): FeedbackConfigurationItem[] => {
|
|
22
|
-
const frequencyTracking = getFrequencyTracking();
|
|
23
|
-
const currentTime = Date.now();
|
|
24
|
-
|
|
25
|
-
const filtered = feedbacks.filter((fb) => {
|
|
26
|
-
const fbId = fb.feedbackConfigurationId;
|
|
27
|
-
const isManual = fb.triggerProperties?.isManual === true;
|
|
28
|
-
|
|
29
|
-
// Check frequency rules
|
|
30
|
-
const tracking = frequencyTracking[fbId];
|
|
31
|
-
|
|
32
|
-
if (!tracking) {
|
|
33
|
-
// No tracking data yet
|
|
34
|
-
if (isManual && fb.frequencyAndScheduling) {
|
|
35
|
-
// For manual feedbacks without tracking, check date range directly
|
|
36
|
-
const startDate = new Date(fb.frequencyAndScheduling.startDate).getTime();
|
|
37
|
-
const stopDate = new Date(fb.frequencyAndScheduling.stopDate).getTime();
|
|
38
|
-
return currentTime >= startDate && currentTime <= stopDate;
|
|
39
|
-
}
|
|
40
|
-
// Never interacted before, always eligible for non-manual
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Pass isManual flag to eligibility check
|
|
45
|
-
// Manual feedbacks: Only check date range, bypass all other frequency rules
|
|
46
|
-
// Non-manual feedbacks: Apply all frequency rules
|
|
47
|
-
const eligible = isEligibleByFrequency(tracking, currentTime, isManual);
|
|
48
|
-
return eligible;
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
return filtered;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
interface StoredConfiguration {
|
|
55
|
-
feedbackConfiguration: FeedbackConfigurationItem[];
|
|
56
|
-
expiry: number;
|
|
57
|
-
feedbackInterval: number;
|
|
58
|
-
masterProperties: MasterProperties;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// MD5 hash function
|
|
62
|
-
const md5 = (str: string): string => {
|
|
63
|
-
const crypto = window.crypto || (window as any).msCrypto;
|
|
64
|
-
if (!crypto) {
|
|
65
|
-
// Fallback for older browsers
|
|
66
|
-
return btoa(str)
|
|
67
|
-
.replace(/[^a-zA-Z0-9]/g, "")
|
|
68
|
-
.substring(0, 16);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Simple hash implementation for browser compatibility
|
|
72
|
-
let hash = 0;
|
|
73
|
-
if (str.length === 0) return hash.toString();
|
|
74
|
-
|
|
75
|
-
for (let i = 0; i < str.length; i++) {
|
|
76
|
-
const char = str.charCodeAt(i);
|
|
77
|
-
hash = (hash << 5) - hash + char;
|
|
78
|
-
hash = hash & hash; // Convert to 32-bit integer
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return Math.abs(hash).toString(16);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// Function to check if device has changed
|
|
85
|
-
const isNewDevice = (deviceInfo: DeviceInfo): boolean => {
|
|
86
|
-
const deviceHashKey = "encatch_device_hash";
|
|
87
|
-
|
|
88
|
-
// Safety check for deviceInfo
|
|
89
|
-
if (!deviceInfo || typeof deviceInfo !== "object") {
|
|
90
|
-
console.warn(
|
|
91
|
-
"isNewDevice: deviceInfo is null, undefined, or not an object:",
|
|
92
|
-
deviceInfo
|
|
93
|
-
);
|
|
94
|
-
return true; // Treat as new device if deviceInfo is invalid
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Helper function to create consistent device string with sorted keys
|
|
98
|
-
const createDeviceString = (device: DeviceInfo): string => {
|
|
99
|
-
const deviceKeys = [
|
|
100
|
-
"$deviceType",
|
|
101
|
-
"$timezone",
|
|
102
|
-
"$theme",
|
|
103
|
-
"$os",
|
|
104
|
-
"$osVersion",
|
|
105
|
-
"$appVersion",
|
|
106
|
-
"$app",
|
|
107
|
-
"$language",
|
|
108
|
-
"$deviceId",
|
|
109
|
-
];
|
|
110
|
-
|
|
111
|
-
const orderedDevice: Record<string, any> = {};
|
|
112
|
-
deviceKeys.forEach((key) => {
|
|
113
|
-
const value = device[key as keyof DeviceInfo];
|
|
114
|
-
if (value !== undefined && value !== null) {
|
|
115
|
-
orderedDevice[key] = value;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
return JSON.stringify(orderedDevice);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// Check if device_hash flag exists in localStorage
|
|
123
|
-
const storedHash = localStorage.getItem(deviceHashKey);
|
|
124
|
-
if (!storedHash) {
|
|
125
|
-
// No hash found, this is a new device
|
|
126
|
-
const newHash = md5(createDeviceString(deviceInfo));
|
|
127
|
-
localStorage.setItem(deviceHashKey, newHash);
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const currentHash = md5(createDeviceString(deviceInfo));
|
|
132
|
-
|
|
133
|
-
// Compare hashes
|
|
134
|
-
if (currentHash !== storedHash) {
|
|
135
|
-
// Device has changed, update stored hash
|
|
136
|
-
localStorage.setItem(deviceHashKey, currentHash);
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Device is the same
|
|
141
|
-
return false;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export const useFetchElligibleFeedbackConfiguration = ({
|
|
145
|
-
apiKey,
|
|
146
|
-
hostUrl,
|
|
147
|
-
}: {
|
|
148
|
-
apiKey: string;
|
|
149
|
-
hostUrl: string;
|
|
150
|
-
}) => {
|
|
151
|
-
const [loading, setLoading] = useState(false);
|
|
152
|
-
const [error, setError] = useState<string | null>(null);
|
|
153
|
-
const [data, setData] = useState<any>(null);
|
|
154
|
-
const [allFeedbacks, setAllFeedbacks] = useState<FeedbackConfigurationItem[]>([]); // Store all feedbacks including manual
|
|
155
|
-
const [lastFetchParams, setLastFetchParams] =
|
|
156
|
-
useState<FetchConfigurationListRequest | null>(null);
|
|
157
|
-
const lastFetchParamsRef = useRef<FetchConfigurationListRequest | null>(null);
|
|
158
|
-
const [feedbackInterval, setFeedbackInterval] = useState<number>(0);
|
|
159
|
-
const [configSyncInterval, setConfigSyncInterval] = useState<number>(15 * 60); // Default 15 minutes
|
|
160
|
-
const intervalIdRef = useRef<number | null>(null);
|
|
161
|
-
|
|
162
|
-
const getStoredConfiguration = (): StoredConfiguration | null => {
|
|
163
|
-
const stored = sessionStorage.getItem("encatch-configuration");
|
|
164
|
-
if (!stored) return null;
|
|
165
|
-
|
|
166
|
-
const config = JSON.parse(stored) as StoredConfiguration;
|
|
167
|
-
if (Date.now() > config.expiry) {
|
|
168
|
-
sessionStorage.removeItem("encatch-configuration");
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Always use pending feedbacks as the source of truth
|
|
173
|
-
const pendingFeedbacks = getPendingFeedbacks();
|
|
174
|
-
// Filter by frequency rules (includes session views and frequency eligibility)
|
|
175
|
-
const filteredFeedbacks = filterByFrequencyRules(pendingFeedbacks);
|
|
176
|
-
config.feedbackConfiguration = filteredFeedbacks;
|
|
177
|
-
|
|
178
|
-
return config;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
const storeConfiguration = (config: StoredConfiguration) => {
|
|
182
|
-
const syncInterval =
|
|
183
|
-
config?.masterProperties?.configSyncIntervalSeconds || 15 * 60;
|
|
184
|
-
const expiry = Date.now() + syncInterval * 1000;
|
|
185
|
-
const disableAllSurvey =
|
|
186
|
-
config?.masterProperties?.disableAllSurvey || false;
|
|
187
|
-
const feedbackIntervalConfig =
|
|
188
|
-
config?.masterProperties?.feedbackIntervalDuration || 1 * 60;
|
|
189
|
-
const feedbackConfigurations = disableAllSurvey
|
|
190
|
-
? []
|
|
191
|
-
: config?.feedbackConfiguration || [];
|
|
192
|
-
|
|
193
|
-
// Store ALL feedbacks (including manual) for lookup by openFeedbackById/openFeedbackByName
|
|
194
|
-
setAllFeedbacks(feedbackConfigurations);
|
|
195
|
-
|
|
196
|
-
// Include all feedbacks in pending list
|
|
197
|
-
// Manual feedbacks will respect frequency rules but bypass showOnce
|
|
198
|
-
const pendingConfigurations = feedbackConfigurations || [];
|
|
199
|
-
|
|
200
|
-
const storedConfig: StoredConfiguration = {
|
|
201
|
-
feedbackConfiguration: pendingConfigurations,
|
|
202
|
-
expiry,
|
|
203
|
-
feedbackInterval: feedbackIntervalConfig,
|
|
204
|
-
masterProperties: config?.masterProperties,
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
setFeedbackInterval(feedbackIntervalConfig);
|
|
208
|
-
setConfigSyncInterval(syncInterval);
|
|
209
|
-
sessionStorage.setItem(
|
|
210
|
-
"encatch-configuration",
|
|
211
|
-
JSON.stringify(storedConfig)
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// Persist all configurations (including manual)
|
|
215
|
-
setPendingFeedbacks(pendingConfigurations);
|
|
216
|
-
|
|
217
|
-
// Initialize tracking only for non-manual feedbacks
|
|
218
|
-
// Manual feedbacks don't need tracking as they only check date range
|
|
219
|
-
pendingConfigurations.forEach((fb) => {
|
|
220
|
-
const isManual = fb.triggerProperties?.isManual === true;
|
|
221
|
-
if (fb.frequencyAndScheduling && !isManual) {
|
|
222
|
-
initializeOrUpdateTracking(
|
|
223
|
-
fb.feedbackConfigurationId,
|
|
224
|
-
fb.frequencyAndScheduling as any
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const sdk = new EncatchApiSDK({
|
|
231
|
-
apiKey,
|
|
232
|
-
hostUrl,
|
|
233
|
-
appPackageName: window.location.hostname, // Or your app's package name
|
|
234
|
-
enableLogging: true,
|
|
235
|
-
});
|
|
236
|
-
const fetchConfiguration = async (params: FetchConfigurationListRequest) => {
|
|
237
|
-
// Prevent API calls if apiKey is blank
|
|
238
|
-
if (!apiKey || apiKey.trim() === '') {
|
|
239
|
-
console.warn('fetchConfiguration: API key is required');
|
|
240
|
-
setError('API key is required');
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
setLastFetchParams(params);
|
|
245
|
-
lastFetchParamsRef.current = params;
|
|
246
|
-
setLoading(true);
|
|
247
|
-
setError(null);
|
|
248
|
-
|
|
249
|
-
// Check if we should use stored configuration
|
|
250
|
-
if (!params.force) {
|
|
251
|
-
const storedConfig = getStoredConfiguration();
|
|
252
|
-
if (storedConfig) {
|
|
253
|
-
const pendingFeedbacks = getPendingFeedbacks();
|
|
254
|
-
const filteredFeedbacks = filterByFrequencyRules(pendingFeedbacks);
|
|
255
|
-
setData(filteredFeedbacks);
|
|
256
|
-
setAllFeedbacks(pendingFeedbacks); // Populate allFeedbacks for manual feedback lookup
|
|
257
|
-
setFeedbackInterval(storedConfig.feedbackInterval);
|
|
258
|
-
const syncInterval = storedConfig.masterProperties?.configSyncIntervalSeconds || 15 * 60;
|
|
259
|
-
setConfigSyncInterval(syncInterval);
|
|
260
|
-
setLoading(false);
|
|
261
|
-
return filteredFeedbacks;
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
264
|
-
sessionStorage.removeItem("encatch-configuration");
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const isNew = isNewDevice(params.deviceInfo);
|
|
269
|
-
let result;
|
|
270
|
-
if (isNew) {
|
|
271
|
-
result = await sdk.fetchNewDeviceConfiguration(params);
|
|
272
|
-
} else {
|
|
273
|
-
result = await sdk.fetchConfiguration(params);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
storeConfiguration(result);
|
|
277
|
-
|
|
278
|
-
const pendingFeedbacks = getPendingFeedbacks();
|
|
279
|
-
setData(pendingFeedbacks);
|
|
280
|
-
return result;
|
|
281
|
-
} catch (err) {
|
|
282
|
-
const errorMessage =
|
|
283
|
-
err instanceof Error ? err.message : "An error occurred";
|
|
284
|
-
setError(errorMessage);
|
|
285
|
-
throw new Error(errorMessage);
|
|
286
|
-
} finally {
|
|
287
|
-
setLoading(false);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const checkAndRefetchIfNeeded = () => {
|
|
292
|
-
const storedConfig = getStoredConfiguration();
|
|
293
|
-
const params = lastFetchParamsRef.current;
|
|
294
|
-
|
|
295
|
-
if (!storedConfig && params) {
|
|
296
|
-
// Configuration has expired, refetch
|
|
297
|
-
fetchConfiguration({ ...params, force: true });
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const refreshPendingFeedbacks = () => {
|
|
302
|
-
const pendingFeedbacks = getPendingFeedbacks();
|
|
303
|
-
const filteredFeedbacks = filterByFrequencyRules(pendingFeedbacks);
|
|
304
|
-
setData(filteredFeedbacks);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const startInterval = () => {
|
|
308
|
-
if (intervalIdRef.current === null && configSyncInterval > 0) {
|
|
309
|
-
const intervalMs = configSyncInterval * 1000;
|
|
310
|
-
intervalIdRef.current = window.setInterval(
|
|
311
|
-
checkAndRefetchIfNeeded,
|
|
312
|
-
intervalMs
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const stopInterval = () => {
|
|
318
|
-
if (intervalIdRef.current !== null) {
|
|
319
|
-
window.clearInterval(intervalIdRef.current);
|
|
320
|
-
intervalIdRef.current = null;
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
useEffect(() => {
|
|
325
|
-
if (document.visibilityState === "visible") {
|
|
326
|
-
startInterval();
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const handleVisibilityChange = () => {
|
|
330
|
-
if (document.visibilityState === "visible") {
|
|
331
|
-
checkAndRefetchIfNeeded();
|
|
332
|
-
startInterval();
|
|
333
|
-
} else {
|
|
334
|
-
stopInterval();
|
|
335
|
-
}
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
339
|
-
|
|
340
|
-
return () => {
|
|
341
|
-
stopInterval();
|
|
342
|
-
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
343
|
-
};
|
|
344
|
-
}, []);
|
|
345
|
-
|
|
346
|
-
// Restart interval when configSyncInterval changes
|
|
347
|
-
useEffect(() => {
|
|
348
|
-
if (document.visibilityState === "visible" && configSyncInterval > 0) {
|
|
349
|
-
stopInterval();
|
|
350
|
-
startInterval();
|
|
351
|
-
}
|
|
352
|
-
}, [configSyncInterval]);
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
data,
|
|
356
|
-
allFeedbacks, // Export all feedbacks including manual ones
|
|
357
|
-
loading,
|
|
358
|
-
error,
|
|
359
|
-
fetchConfiguration,
|
|
360
|
-
feedbackInterval,
|
|
361
|
-
refreshPendingFeedbacks,
|
|
362
|
-
};
|
|
363
|
-
};
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { EncatchApiSDK } from "@encatch/api-sdk";
|
|
2
|
-
import { FetchFeedbackDetailsRequest, FetchFeedbackDetailsResponse} from "@encatch/schema";
|
|
3
|
-
import { useState } from "preact/hooks";
|
|
4
|
-
|
|
5
|
-
export const useFetchFeedbackConfigurationDetails = (
|
|
6
|
-
{apiKey, hostUrl}:{apiKey: string, hostUrl:string}
|
|
7
|
-
) => {
|
|
8
|
-
const [loadingFetchDetails, setLoadingFetchDetails] = useState(false);
|
|
9
|
-
const [errorFetchDetails, setErrorFetchDetails] = useState<string | null>(null);
|
|
10
|
-
const [successFetchDetails, setSuccessFetchDetails] = useState(false);
|
|
11
|
-
// Instantiate the SDK once
|
|
12
|
-
const sdk = new EncatchApiSDK({
|
|
13
|
-
apiKey,
|
|
14
|
-
hostUrl,
|
|
15
|
-
appPackageName: window.location.hostname,
|
|
16
|
-
enableLogging: true,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const fetchDetails = async (params: FetchFeedbackDetailsRequest): Promise<(FetchFeedbackDetailsResponse) | null> => {
|
|
20
|
-
// Prevent API calls if apiKey is blank
|
|
21
|
-
if (!apiKey || apiKey.trim() === '') {
|
|
22
|
-
console.warn('fetchDetails: API key is required');
|
|
23
|
-
setErrorFetchDetails('API key is required');
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
setLoadingFetchDetails(true);
|
|
28
|
-
setErrorFetchDetails(null);
|
|
29
|
-
setSuccessFetchDetails(false);
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
// Prepare request in the shape expected by the SDK
|
|
33
|
-
const sdkParams: FetchFeedbackDetailsRequest = {
|
|
34
|
-
formConfig: {
|
|
35
|
-
feedbackConfigurationId: params.formConfig.feedbackConfigurationId,
|
|
36
|
-
responseLanguageCode: params.formConfig.responseLanguageCode,
|
|
37
|
-
},
|
|
38
|
-
deviceInfo: params.deviceInfo,
|
|
39
|
-
sessionInfo: {
|
|
40
|
-
$sessionId: params.sessionInfo.$sessionId,
|
|
41
|
-
},
|
|
42
|
-
userInfo: params.userInfo,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const result: FetchFeedbackDetailsResponse = await sdk.fetchConfigurationDetails(sdkParams);
|
|
46
|
-
|
|
47
|
-
// Validate that response has the required properties of ConfigurationResponse
|
|
48
|
-
const isValidResponse = result &&
|
|
49
|
-
typeof result === 'object' &&
|
|
50
|
-
'feedbackConfigurationId' in result &&
|
|
51
|
-
'formConfiguration' in result &&
|
|
52
|
-
'questionnaireFields' in result &&
|
|
53
|
-
'otherConfigurationProperties' in result &&
|
|
54
|
-
'welcomeScreenProperties' in result &&
|
|
55
|
-
'endScreenProperties' in result &&
|
|
56
|
-
'appearanceProperties' in result;
|
|
57
|
-
|
|
58
|
-
if (!isValidResponse) {
|
|
59
|
-
throw new Error('Invalid response format from server');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
setSuccessFetchDetails(true);
|
|
63
|
-
|
|
64
|
-
// Map camelCase SDK response to your expected ConfigurationResponse shape if needed
|
|
65
|
-
return {
|
|
66
|
-
feedbackConfigurationId: result.feedbackConfigurationId,
|
|
67
|
-
feedbackIdentifier: result.feedbackIdentifier,
|
|
68
|
-
formConfiguration: result.formConfiguration,
|
|
69
|
-
questionnaireFields: result.questionnaireFields,
|
|
70
|
-
otherConfigurationProperties: result.otherConfigurationProperties,
|
|
71
|
-
welcomeScreenProperties: result.welcomeScreenProperties,
|
|
72
|
-
endScreenProperties: result.endScreenProperties,
|
|
73
|
-
// Include appearanceProperties if available in the response
|
|
74
|
-
appearanceProperties: result.appearanceProperties || null,
|
|
75
|
-
};
|
|
76
|
-
} catch (err) {
|
|
77
|
-
const errorMessage = err instanceof Error ? err.message : "An error occurred while fetching configuration details";
|
|
78
|
-
setErrorFetchDetails(errorMessage);
|
|
79
|
-
return null;
|
|
80
|
-
} finally {
|
|
81
|
-
setLoadingFetchDetails(false);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
loadingFetchDetails,
|
|
88
|
-
errorFetchDetails,
|
|
89
|
-
successFetchDetails,
|
|
90
|
-
fetchDetails,
|
|
91
|
-
};
|
|
92
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { useEffect } from "preact/hooks";
|
|
2
|
-
|
|
3
|
-
interface LocationChangeProps {
|
|
4
|
-
eventName?: string;
|
|
5
|
-
trackEvent: (eventName: string, properties?: Record<string, any>) => void;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
// Utility function to extract path from href, handling hash-based routing
|
|
9
|
-
const extractPathFromHref = (href: string): string => {
|
|
10
|
-
try {
|
|
11
|
-
const url = new URL(href);
|
|
12
|
-
// For hash-based routing, we want the hash part as the path
|
|
13
|
-
if (url.hash && url.hash.length > 1) {
|
|
14
|
-
// Remove the leading '#' and return the hash path
|
|
15
|
-
return url.hash.substring(1);
|
|
16
|
-
}
|
|
17
|
-
// For regular routing, return the pathname
|
|
18
|
-
return url.pathname;
|
|
19
|
-
} catch (error) {
|
|
20
|
-
// Fallback: try to extract path manually
|
|
21
|
-
const urlObj = new URL(href, window.location.origin);
|
|
22
|
-
if (urlObj.hash && urlObj.hash.length > 1) {
|
|
23
|
-
return urlObj.hash.substring(1);
|
|
24
|
-
}
|
|
25
|
-
return urlObj.pathname;
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const usePageChangeTracker = (props: LocationChangeProps) => {
|
|
30
|
-
const trackLocationChange = () => {
|
|
31
|
-
const href = window.location.href;
|
|
32
|
-
const path = extractPathFromHref(href);
|
|
33
|
-
|
|
34
|
-
props.trackEvent(props.eventName || "pageLoad", {
|
|
35
|
-
url: href,
|
|
36
|
-
path: path,
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
// Track initial page load
|
|
42
|
-
trackLocationChange();
|
|
43
|
-
|
|
44
|
-
// Track subsequent navigation changes
|
|
45
|
-
const observer = new MutationObserver(() => {
|
|
46
|
-
trackLocationChange();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Observe the document title for changes
|
|
50
|
-
observer.observe(document.querySelector("title") || document.head, {
|
|
51
|
-
subtree: true,
|
|
52
|
-
characterData: true,
|
|
53
|
-
childList: true,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Listen for popstate events (browser back/forward)
|
|
57
|
-
window.addEventListener("popstate", trackLocationChange);
|
|
58
|
-
|
|
59
|
-
// Listen for pushstate events (React Router navigation)
|
|
60
|
-
const originalPushState = window.history.pushState;
|
|
61
|
-
window.history.pushState = function (
|
|
62
|
-
data: any,
|
|
63
|
-
unused: string,
|
|
64
|
-
url?: string | URL | null
|
|
65
|
-
) {
|
|
66
|
-
originalPushState.call(this, data, unused, url);
|
|
67
|
-
trackLocationChange();
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Listen for React Router navigation events if available
|
|
71
|
-
if (window.encatch?.router) {
|
|
72
|
-
window.encatch.router.subscribe((location: { pathname: string }) => {
|
|
73
|
-
trackLocationChange();
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return () => {
|
|
78
|
-
observer.disconnect();
|
|
79
|
-
window.removeEventListener("popstate", trackLocationChange);
|
|
80
|
-
// Restore original pushState
|
|
81
|
-
window.history.pushState = originalPushState;
|
|
82
|
-
};
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
trackLocationChange,
|
|
87
|
-
};
|
|
88
|
-
};
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
// import { QuestionnaireFields } from "src/element/types";
|
|
2
|
-
import { QuestionnaireFieldsResponse } from "@encatch/schema";
|
|
3
|
-
import { PrepopulatedAnswer } from "../feedback-api-types";
|
|
4
|
-
import { updateQuestionResponse } from "../store/formResponses";
|
|
5
|
-
|
|
6
|
-
// Helper function to extract the actual value from AnswerFormat
|
|
7
|
-
const extractValueFromAnswerFormat = (value: any, questionId: string): any => {
|
|
8
|
-
// If value is not an object, return it as-is (primitive value)
|
|
9
|
-
if (typeof value !== "object" || value === null) {
|
|
10
|
-
return value;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Define the mapping of answer format properties to their values
|
|
14
|
-
const answerFormatMap = {
|
|
15
|
-
nps: value.nps,
|
|
16
|
-
rating: value.rating,
|
|
17
|
-
long_text: value.long_text,
|
|
18
|
-
short_answer: value.short_answer,
|
|
19
|
-
single_choice: value.single_choice,
|
|
20
|
-
multiple_choice_multiple: value.multiple_choice_multiple,
|
|
21
|
-
nested_selection: value.nested_selection,
|
|
22
|
-
annotation: value.annotation,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Find the first property that exists in the value object
|
|
26
|
-
for (const [property, propertyValue] of Object.entries(answerFormatMap)) {
|
|
27
|
-
if (property in value) {
|
|
28
|
-
return propertyValue;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// If no known property is found, log a warning and return null
|
|
33
|
-
console.warn(`Unknown answer format for questionId ${questionId}:`, value);
|
|
34
|
-
return null;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Function to process prepopulated answers and update the form responses store
|
|
38
|
-
const processPrepopulatedAnswers = (
|
|
39
|
-
prepopulatedAnswers: PrepopulatedAnswer[],
|
|
40
|
-
questionnaireFields: QuestionnaireFieldsResponse
|
|
41
|
-
) => {
|
|
42
|
-
try {
|
|
43
|
-
const { sections, questions } = questionnaireFields;
|
|
44
|
-
|
|
45
|
-
if (!sections || !questions) {
|
|
46
|
-
console.warn(
|
|
47
|
-
"Invalid questionnaire fields structure for prepopulated answers"
|
|
48
|
-
);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
prepopulatedAnswers.forEach((answer) => {
|
|
53
|
-
try {
|
|
54
|
-
const { sectionIndex, questionIndex, value } = answer;
|
|
55
|
-
|
|
56
|
-
// Validate indices
|
|
57
|
-
if (sectionIndex < 0 || sectionIndex >= sections.length) {
|
|
58
|
-
console.warn(
|
|
59
|
-
`Invalid sectionIndex ${sectionIndex} for prepopulated answer`
|
|
60
|
-
);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const section = sections[sectionIndex];
|
|
65
|
-
if (
|
|
66
|
-
!section?.questionIds ||
|
|
67
|
-
questionIndex < 0 ||
|
|
68
|
-
questionIndex >= section.questionIds.length
|
|
69
|
-
) {
|
|
70
|
-
console.warn(
|
|
71
|
-
`Invalid questionIndex ${questionIndex} for section ${sectionIndex}`
|
|
72
|
-
);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Get the question ID
|
|
77
|
-
const questionId = section.questionIds[questionIndex];
|
|
78
|
-
if (!questionId) {
|
|
79
|
-
console.warn(
|
|
80
|
-
`No questionId found at section ${sectionIndex}, question ${questionIndex}`
|
|
81
|
-
);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Get the question details to determine the type
|
|
86
|
-
const question = questions[questionId];
|
|
87
|
-
if (!question) {
|
|
88
|
-
console.warn(
|
|
89
|
-
`Question details not found for questionId ${questionId}`
|
|
90
|
-
);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Update the form response using the store's helper function
|
|
95
|
-
// The value is already in AnswerFormat, so we extract the actual value
|
|
96
|
-
const questionType = question.type;
|
|
97
|
-
const actualValue = extractValueFromAnswerFormat(value, questionId);
|
|
98
|
-
|
|
99
|
-
if (actualValue === null) {
|
|
100
|
-
return; // Skip if unknown format
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Update the question response
|
|
104
|
-
updateQuestionResponse(questionId, actualValue, questionType);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.error(
|
|
107
|
-
"Error processing individual prepopulated answer:",
|
|
108
|
-
error,
|
|
109
|
-
answer
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error("Error processing prepopulated answers:", error);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const usePrepopulatedAnswers = () => {
|
|
119
|
-
return {
|
|
120
|
-
processPrepopulatedAnswers,
|
|
121
|
-
extractValueFromAnswerFormat,
|
|
122
|
-
};
|
|
123
|
-
};
|