@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,330 @@
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
+
28
+ // Check frequency rules
29
+ const tracking = frequencyTracking[fbId];
30
+ if (!tracking) {
31
+ // Never interacted before, always eligible
32
+ return true;
33
+ }
34
+
35
+ const eligible = isEligibleByFrequency(tracking, currentTime);
36
+ return eligible;
37
+ });
38
+
39
+ return filtered;
40
+ };
41
+
42
+ interface StoredConfiguration {
43
+ feedbackConfiguration: FeedbackConfigurationItem[];
44
+ expiry: number;
45
+ feedbackInterval: number;
46
+ masterProperties: MasterProperties;
47
+ }
48
+
49
+ // MD5 hash function
50
+ const md5 = (str: string): string => {
51
+ const crypto = window.crypto || (window as any).msCrypto;
52
+ if (!crypto) {
53
+ // Fallback for older browsers
54
+ return btoa(str)
55
+ .replace(/[^a-zA-Z0-9]/g, "")
56
+ .substring(0, 16);
57
+ }
58
+
59
+ // Simple hash implementation for browser compatibility
60
+ let hash = 0;
61
+ if (str.length === 0) return hash.toString();
62
+
63
+ for (let i = 0; i < str.length; i++) {
64
+ const char = str.charCodeAt(i);
65
+ hash = (hash << 5) - hash + char;
66
+ hash = hash & hash; // Convert to 32-bit integer
67
+ }
68
+
69
+ return Math.abs(hash).toString(16);
70
+ };
71
+
72
+ // Function to check if device has changed
73
+ const isNewDevice = (deviceInfo: DeviceInfo): boolean => {
74
+ const deviceHashKey = "encatch_device_hash";
75
+
76
+ // Safety check for deviceInfo
77
+ if (!deviceInfo || typeof deviceInfo !== "object") {
78
+ console.warn(
79
+ "isNewDevice: deviceInfo is null, undefined, or not an object:",
80
+ deviceInfo
81
+ );
82
+ return true; // Treat as new device if deviceInfo is invalid
83
+ }
84
+
85
+ // Helper function to create consistent device string with sorted keys
86
+ const createDeviceString = (device: DeviceInfo): string => {
87
+ const deviceKeys = [
88
+ "$deviceType",
89
+ "$timezone",
90
+ "$theme",
91
+ "$os",
92
+ "$osVersion",
93
+ "$appVersion",
94
+ "$app",
95
+ "$language",
96
+ ];
97
+
98
+ const orderedDevice: Record<string, any> = {};
99
+ deviceKeys.forEach((key) => {
100
+ const value = device[key as keyof DeviceInfo];
101
+ if (value !== undefined && value !== null) {
102
+ orderedDevice[key] = value;
103
+ }
104
+ });
105
+
106
+ return JSON.stringify(orderedDevice);
107
+ };
108
+
109
+ // Check if device_hash flag exists in localStorage
110
+ const storedHash = localStorage.getItem(deviceHashKey);
111
+ if (!storedHash) {
112
+ // No hash found, this is a new device
113
+ const newHash = md5(createDeviceString(deviceInfo));
114
+ localStorage.setItem(deviceHashKey, newHash);
115
+ return true;
116
+ }
117
+
118
+ const currentHash = md5(createDeviceString(deviceInfo));
119
+
120
+ // Compare hashes
121
+ if (currentHash !== storedHash) {
122
+ // Device has changed, update stored hash
123
+ localStorage.setItem(deviceHashKey, currentHash);
124
+ return true;
125
+ }
126
+
127
+ // Device is the same
128
+ return false;
129
+ };
130
+
131
+ export const useFetchElligibleFeedbackConfiguration = ({
132
+ apiKey,
133
+ hostUrl,
134
+ }: {
135
+ apiKey: string;
136
+ hostUrl: string;
137
+ }) => {
138
+ const [loading, setLoading] = useState(false);
139
+ const [error, setError] = useState<string | null>(null);
140
+ const [data, setData] = useState<any>(null);
141
+ const [lastFetchParams, setLastFetchParams] =
142
+ useState<FetchConfigurationListRequest | null>(null);
143
+ const lastFetchParamsRef = useRef<FetchConfigurationListRequest | null>(null);
144
+ const [feedbackInterval, setFeedbackInterval] = useState<number>(0);
145
+ const [configSyncInterval, setConfigSyncInterval] = useState<number>(15 * 60); // Default 15 minutes
146
+ const intervalIdRef = useRef<number | null>(null);
147
+
148
+ const getStoredConfiguration = (): StoredConfiguration | null => {
149
+ const stored = sessionStorage.getItem("encatch-configuration");
150
+ if (!stored) return null;
151
+
152
+ const config = JSON.parse(stored) as StoredConfiguration;
153
+ if (Date.now() > config.expiry) {
154
+ sessionStorage.removeItem("encatch-configuration");
155
+ return null;
156
+ }
157
+
158
+ // Always use pending feedbacks as the source of truth
159
+ const pendingFeedbacks = getPendingFeedbacks();
160
+ // Filter by frequency rules (includes session views and frequency eligibility)
161
+ const filteredFeedbacks = filterByFrequencyRules(pendingFeedbacks);
162
+ config.feedbackConfiguration = filteredFeedbacks;
163
+
164
+ return config;
165
+ };
166
+
167
+ const storeConfiguration = (config: StoredConfiguration) => {
168
+ const syncInterval =
169
+ config?.masterProperties?.configSyncIntervalSeconds || 15 * 60;
170
+ const expiry = Date.now() + syncInterval * 1000;
171
+ const disableAllSurvey =
172
+ config?.masterProperties?.disableAllSurvey || false;
173
+ const feedbackIntervalConfig =
174
+ config?.masterProperties?.feedbackIntervalDuration || 1 * 60;
175
+ const feedbackConfigurations = disableAllSurvey
176
+ ? []
177
+ : config?.feedbackConfiguration || [];
178
+
179
+ const storedConfig: StoredConfiguration = {
180
+ feedbackConfiguration: feedbackConfigurations,
181
+ expiry,
182
+ feedbackInterval: feedbackIntervalConfig,
183
+ masterProperties: config?.masterProperties,
184
+ };
185
+
186
+ setFeedbackInterval(feedbackIntervalConfig);
187
+ setConfigSyncInterval(syncInterval);
188
+ sessionStorage.setItem(
189
+ "encatch-configuration",
190
+ JSON.stringify(storedConfig)
191
+ );
192
+
193
+ setPendingFeedbacks(feedbackConfigurations);
194
+
195
+ // Initialize tracking for new feedbacks (only once when fetching from API)
196
+ feedbackConfigurations.forEach((fb) => {
197
+ if (fb.frequencyAndScheduling) {
198
+ initializeOrUpdateTracking(
199
+ fb.feedbackConfigurationId,
200
+ fb.frequencyAndScheduling as any
201
+ );
202
+ }
203
+ });
204
+ };
205
+
206
+ const sdk = new EncatchApiSDK({
207
+ apiKey,
208
+ hostUrl,
209
+ appPackageName: window.location.hostname, // Or your app's package name
210
+ enableLogging: true,
211
+ });
212
+ const fetchConfiguration = async (params: FetchConfigurationListRequest) => {
213
+ setLastFetchParams(params);
214
+ lastFetchParamsRef.current = params;
215
+ setLoading(true);
216
+ setError(null);
217
+
218
+ // Check if we should use stored configuration
219
+ if (!params.force) {
220
+ const storedConfig = getStoredConfiguration();
221
+ if (storedConfig) {
222
+ const pendingFeedbacks = getPendingFeedbacks();
223
+ const filteredFeedbacks = filterByFrequencyRules(pendingFeedbacks);
224
+ setData(filteredFeedbacks);
225
+ setFeedbackInterval(storedConfig.feedbackInterval);
226
+ const syncInterval = storedConfig.masterProperties?.configSyncIntervalSeconds || 15 * 60;
227
+ setConfigSyncInterval(syncInterval);
228
+ setLoading(false);
229
+ return filteredFeedbacks;
230
+ }
231
+ } else {
232
+ sessionStorage.removeItem("encatch-configuration");
233
+ }
234
+
235
+ try {
236
+ const isNew = isNewDevice(params.deviceInfo);
237
+ let result;
238
+ if (isNew) {
239
+ result = await sdk.fetchNewDeviceConfiguration(params);
240
+ } else {
241
+ result = await sdk.fetchConfiguration(params);
242
+ }
243
+
244
+ storeConfiguration(result);
245
+
246
+ const pendingFeedbacks = getPendingFeedbacks();
247
+ setData(pendingFeedbacks);
248
+ return result;
249
+ } catch (err) {
250
+ const errorMessage =
251
+ err instanceof Error ? err.message : "An error occurred";
252
+ setError(errorMessage);
253
+ throw new Error(errorMessage);
254
+ } finally {
255
+ setLoading(false);
256
+ }
257
+ };
258
+
259
+ const checkAndRefetchIfNeeded = () => {
260
+ const storedConfig = getStoredConfiguration();
261
+ const params = lastFetchParamsRef.current;
262
+
263
+ if (!storedConfig && params) {
264
+ // Configuration has expired, refetch
265
+ fetchConfiguration({ ...params, force: true });
266
+ }
267
+ };
268
+
269
+ const refreshPendingFeedbacks = () => {
270
+ const pendingFeedbacks = getPendingFeedbacks();
271
+ const filteredFeedbacks = filterByFrequencyRules(pendingFeedbacks);
272
+ setData(filteredFeedbacks);
273
+ };
274
+
275
+ const startInterval = () => {
276
+ if (intervalIdRef.current === null && configSyncInterval > 0) {
277
+ const intervalMs = configSyncInterval * 1000;
278
+ intervalIdRef.current = window.setInterval(
279
+ checkAndRefetchIfNeeded,
280
+ intervalMs
281
+ );
282
+ }
283
+ };
284
+
285
+ const stopInterval = () => {
286
+ if (intervalIdRef.current !== null) {
287
+ window.clearInterval(intervalIdRef.current);
288
+ intervalIdRef.current = null;
289
+ }
290
+ };
291
+
292
+ useEffect(() => {
293
+ if (document.visibilityState === "visible") {
294
+ startInterval();
295
+ }
296
+
297
+ const handleVisibilityChange = () => {
298
+ if (document.visibilityState === "visible") {
299
+ checkAndRefetchIfNeeded();
300
+ startInterval();
301
+ } else {
302
+ stopInterval();
303
+ }
304
+ };
305
+
306
+ document.addEventListener("visibilitychange", handleVisibilityChange);
307
+
308
+ return () => {
309
+ stopInterval();
310
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
311
+ };
312
+ }, []);
313
+
314
+ // Restart interval when configSyncInterval changes
315
+ useEffect(() => {
316
+ if (document.visibilityState === "visible" && configSyncInterval > 0) {
317
+ stopInterval();
318
+ startInterval();
319
+ }
320
+ }, [configSyncInterval]);
321
+
322
+ return {
323
+ data,
324
+ loading,
325
+ error,
326
+ fetchConfiguration,
327
+ feedbackInterval,
328
+ refreshPendingFeedbacks,
329
+ };
330
+ };
@@ -0,0 +1,82 @@
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
+ setLoadingFetchDetails(true);
21
+ setErrorFetchDetails(null);
22
+ setSuccessFetchDetails(false);
23
+
24
+ try {
25
+ // Prepare request in the shape expected by the SDK
26
+ const sdkParams: FetchFeedbackDetailsRequest = {
27
+ formConfig: {
28
+ feedbackConfigurationId: params.formConfig.feedbackConfigurationId,
29
+ responseLanguageCode: params.formConfig.responseLanguageCode,
30
+ },
31
+ deviceInfo: params.deviceInfo,
32
+ sessionInfo: {
33
+ $sessionId: params.sessionInfo.$sessionId,
34
+ },
35
+ userInfo: params.userInfo,
36
+ };
37
+
38
+ const result: FetchFeedbackDetailsResponse = await sdk.fetchConfigurationDetails(sdkParams);
39
+
40
+ // Validate that response has the required properties of ConfigurationResponse
41
+ const isValidResponse = result &&
42
+ typeof result === 'object' &&
43
+ 'feedbackConfigurationId' in result &&
44
+ 'formConfiguration' in result &&
45
+ 'questionnaireFields' in result &&
46
+ 'otherConfigurationProperties' in result &&
47
+ 'welcomeScreenProperties' in result &&
48
+ 'endScreenProperties' in result;
49
+
50
+ if (!isValidResponse) {
51
+ throw new Error('Invalid response format from server');
52
+ }
53
+
54
+ setSuccessFetchDetails(true);
55
+
56
+ // Map camelCase SDK response to your expected ConfigurationResponse shape if needed
57
+ return {
58
+ feedbackConfigurationId: result.feedbackConfigurationId,
59
+ feedbackIdentifier: result.feedbackIdentifier,
60
+ formConfiguration: result.formConfiguration,
61
+ questionnaireFields: result.questionnaireFields,
62
+ otherConfigurationProperties: result.otherConfigurationProperties,
63
+ welcomeScreenProperties: result.welcomeScreenProperties,
64
+ endScreenProperties: result.endScreenProperties,
65
+ };
66
+ } catch (err) {
67
+ const errorMessage = err instanceof Error ? err.message : "An error occurred while fetching configuration details";
68
+ setErrorFetchDetails(errorMessage);
69
+ return null;
70
+ } finally {
71
+ setLoadingFetchDetails(false);
72
+ }
73
+ };
74
+
75
+
76
+ return {
77
+ loadingFetchDetails,
78
+ errorFetchDetails,
79
+ successFetchDetails,
80
+ fetchDetails,
81
+ };
82
+ };
@@ -0,0 +1,88 @@
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
+ };
@@ -0,0 +1,123 @@
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
+ };
@@ -0,0 +1,48 @@
1
+ import { EncatchApiSDK, RefineTextParams, RefineTextResponse } from "@encatch/api-sdk";
2
+ import { useState } from "preact/hooks";
3
+
4
+
5
+ export const useRefineTextForm = ({ apiKey, hostUrl }: { apiKey?: string; hostUrl: string }) => {
6
+ const [loadingRefineText, setLoadingRefineText] = useState(false);
7
+ const [errorRefineText, setErrorRefineText] = useState<string | null>(null);
8
+ const [resultRefineText, setResultRefineText] = useState<RefineTextResponse | null>(null);
9
+ const [successMsg, setSuccessMsg] = useState<string | null>(null);
10
+
11
+ // Instantiate the SDK once
12
+ const sdk = new EncatchApiSDK({
13
+ apiKey: apiKey || "",
14
+ hostUrl,
15
+ appPackageName: window.location.hostname,
16
+ enableLogging: true,
17
+ });
18
+
19
+ const refineText = async (params: RefineTextParams): Promise<RefineTextResponse | null> => {
20
+ setLoadingRefineText(true);
21
+ setErrorRefineText(null);
22
+ setResultRefineText(null);
23
+
24
+ try {
25
+ // Use the SDK's refineText method
26
+ const result = await sdk.refineText(params);
27
+
28
+ setSuccessMsg(result.message || "Text refined successfully");
29
+
30
+ setResultRefineText(result);
31
+ return result;
32
+ } catch (err) {
33
+ const errorMessage = err instanceof Error ? err.message : "An error occurred while refining text";
34
+ setErrorRefineText(errorMessage);
35
+ return null;
36
+ } finally {
37
+ setLoadingRefineText(false);
38
+ }
39
+ };
40
+
41
+ return {
42
+ loadingRefineText,
43
+ errorRefineText,
44
+ resultRefineText,
45
+ refineText,
46
+ successMsg
47
+ };
48
+ };