@solucx/react-native-solucx-widget 0.2.0 → 0.2.2
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/lib/SoluCXWidget.d.ts +12 -0
- package/lib/SoluCXWidget.d.ts.map +1 -0
- package/lib/SoluCXWidget.js +110 -0
- package/lib/SoluCXWidget.js.map +1 -0
- package/lib/components/CloseButton.d.ts +8 -0
- package/lib/components/CloseButton.d.ts.map +1 -0
- package/lib/components/CloseButton.js +31 -0
- package/lib/components/CloseButton.js.map +1 -0
- package/lib/components/InlineWidget.d.ts +10 -0
- package/lib/components/InlineWidget.d.ts.map +1 -0
- package/lib/components/InlineWidget.js +19 -0
- package/lib/components/InlineWidget.js.map +1 -0
- package/lib/components/ModalWidget.d.ts +10 -0
- package/lib/components/ModalWidget.d.ts.map +1 -0
- package/lib/components/ModalWidget.js +27 -0
- package/lib/components/ModalWidget.js.map +1 -0
- package/lib/components/OverlayWidget.d.ts +12 -0
- package/lib/components/OverlayWidget.d.ts.map +1 -0
- package/lib/components/OverlayWidget.js +55 -0
- package/lib/components/OverlayWidget.js.map +1 -0
- package/lib/constants/Constants.d.ts +3 -0
- package/lib/constants/Constants.d.ts.map +1 -0
- package/lib/constants/Constants.js +10 -0
- package/lib/constants/Constants.js.map +1 -0
- package/lib/constants/webViewConstants.d.ts +12 -0
- package/lib/constants/webViewConstants.d.ts.map +1 -0
- package/lib/constants/webViewConstants.js +19 -0
- package/lib/constants/webViewConstants.js.map +1 -0
- package/lib/hooks/index.d.ts +3 -0
- package/lib/hooks/index.d.ts.map +1 -0
- package/lib/hooks/index.js +8 -0
- package/lib/hooks/index.js.map +1 -0
- package/lib/hooks/useDeviceInfoCollector.d.ts +14 -0
- package/lib/hooks/useDeviceInfoCollector.d.ts.map +1 -0
- package/lib/hooks/useDeviceInfoCollector.js +54 -0
- package/lib/hooks/useDeviceInfoCollector.js.map +1 -0
- package/lib/hooks/useHeightAnimation.d.ts +9 -0
- package/lib/hooks/useHeightAnimation.d.ts.map +1 -0
- package/lib/hooks/useHeightAnimation.js +19 -0
- package/lib/hooks/useHeightAnimation.js.map +1 -0
- package/lib/hooks/useWidgetHeight.d.ts +13 -0
- package/lib/hooks/useWidgetHeight.d.ts.map +1 -0
- package/lib/hooks/useWidgetHeight.js +21 -0
- package/lib/hooks/useWidgetHeight.js.map +1 -0
- package/lib/hooks/useWidgetState.d.ts +15 -0
- package/lib/hooks/useWidgetState.d.ts.map +1 -0
- package/lib/hooks/useWidgetState.js +79 -0
- package/lib/hooks/useWidgetState.js.map +1 -0
- package/lib/index.d.ts +13 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +43 -0
- package/lib/index.js.map +1 -0
- package/lib/interfaces/WidgetCallbacks.d.ts +14 -0
- package/lib/interfaces/WidgetCallbacks.d.ts.map +1 -0
- package/lib/interfaces/WidgetCallbacks.js +3 -0
- package/lib/interfaces/WidgetCallbacks.js.map +1 -0
- package/lib/interfaces/WidgetData.d.ts +21 -0
- package/lib/interfaces/WidgetData.d.ts.map +1 -0
- package/lib/interfaces/WidgetData.js +3 -0
- package/lib/interfaces/WidgetData.js.map +1 -0
- package/lib/interfaces/WidgetOptions.d.ts +9 -0
- package/lib/interfaces/WidgetOptions.d.ts.map +1 -0
- package/lib/interfaces/WidgetOptions.js +3 -0
- package/lib/interfaces/WidgetOptions.js.map +1 -0
- package/lib/interfaces/WidgetResponse.d.ts +10 -0
- package/lib/interfaces/WidgetResponse.d.ts.map +1 -0
- package/lib/interfaces/WidgetResponse.js +12 -0
- package/lib/interfaces/WidgetResponse.js.map +1 -0
- package/lib/interfaces/WidgetSamplerLog.d.ts +7 -0
- package/lib/interfaces/WidgetSamplerLog.d.ts.map +1 -0
- package/lib/interfaces/WidgetSamplerLog.js +3 -0
- package/lib/interfaces/WidgetSamplerLog.js.map +1 -0
- package/lib/interfaces/index.d.ts +12 -0
- package/lib/interfaces/index.d.ts.map +1 -0
- package/lib/interfaces/index.js +3 -0
- package/lib/interfaces/index.js.map +1 -0
- package/lib/services/ClientVersionCollector.d.ts +2 -0
- package/lib/services/ClientVersionCollector.d.ts.map +1 -0
- package/lib/services/ClientVersionCollector.js +20 -0
- package/lib/services/ClientVersionCollector.js.map +1 -0
- package/lib/services/storage.d.ts +8 -0
- package/lib/services/storage.d.ts.map +1 -0
- package/lib/services/storage.js +23 -0
- package/lib/services/storage.js.map +1 -0
- package/lib/services/widgetBootstrapService.d.ts +5 -0
- package/lib/services/widgetBootstrapService.d.ts.map +1 -0
- package/lib/services/widgetBootstrapService.js +60 -0
- package/lib/services/widgetBootstrapService.js.map +1 -0
- package/lib/services/widgetEventService.d.ts +19 -0
- package/lib/services/widgetEventService.d.ts.map +1 -0
- package/lib/services/widgetEventService.js +79 -0
- package/lib/services/widgetEventService.js.map +1 -0
- package/lib/services/widgetValidationService.d.ts +18 -0
- package/lib/services/widgetValidationService.d.ts.map +1 -0
- package/lib/services/widgetValidationService.js +71 -0
- package/lib/services/widgetValidationService.js.map +1 -0
- package/lib/styles/widgetStyles.d.ts +87 -0
- package/lib/styles/widgetStyles.d.ts.map +1 -0
- package/lib/styles/widgetStyles.js +59 -0
- package/lib/styles/widgetStyles.js.map +1 -0
- package/lib/utils/urlUtils.d.ts +3 -0
- package/lib/utils/urlUtils.d.ts.map +1 -0
- package/lib/utils/urlUtils.js +13 -0
- package/lib/utils/urlUtils.js.map +1 -0
- package/package.json +5 -3
- package/src/SoluCXWidget.tsx +23 -17
- package/src/__tests__/SoluCXWidget.rendering.test.tsx +492 -153
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +9 -10
- package/src/__tests__/integration/webview-communication.test.tsx +9 -9
- package/src/__tests__/useWidgetState.test.ts +2 -2
- package/src/__tests__/widgetBootstrapService.test.ts +45 -10
- package/src/hooks/useWidgetState.ts +3 -3
- package/src/interfaces/WidgetCallbacks.ts +0 -1
- package/src/services/widgetBootstrapService.ts +13 -4
- package/src/services/widgetEventService.ts +1 -1
|
@@ -82,7 +82,7 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
82
82
|
const callbacks: WidgetCallbacks = {
|
|
83
83
|
onOpened: (userId) => analytics.track('widget_shown', { userId }),
|
|
84
84
|
onQuestionAnswered: () => analytics.track('question_answered'),
|
|
85
|
-
|
|
85
|
+
onCompleted: (userId) => {
|
|
86
86
|
analytics.track('survey_completed', { userId });
|
|
87
87
|
},
|
|
88
88
|
onClosed: () => analytics.track('widget_closed'),
|
|
@@ -117,10 +117,6 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
117
117
|
nativeEvent: { data: 'FORM_RESIZE-400' },
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
await waitFor(() => {
|
|
121
|
-
expect(analytics.track).toHaveBeenCalledWith('question_answered', undefined);
|
|
122
|
-
}, { timeout: 100 }).catch(() => {}); // May not be called yet
|
|
123
|
-
|
|
124
120
|
// 4. Customer answers first question
|
|
125
121
|
fireEvent(webview, 'message', {
|
|
126
122
|
nativeEvent: { data: 'QUESTION_ANSWERED' },
|
|
@@ -130,6 +126,9 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
130
126
|
expect(analytics.track).toHaveBeenCalledWith('question_answered');
|
|
131
127
|
});
|
|
132
128
|
|
|
129
|
+
// Reset counter after first question to track only upcoming interactions
|
|
130
|
+
analytics.track.mockClear();
|
|
131
|
+
|
|
133
132
|
// 5. Customer goes to next page
|
|
134
133
|
fireEvent(webview, 'message', {
|
|
135
134
|
nativeEvent: { data: 'FORM_PAGECHANGED-2' },
|
|
@@ -142,7 +141,7 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
142
141
|
|
|
143
142
|
await waitFor(() => {
|
|
144
143
|
expect(analytics.track).toHaveBeenCalledWith('question_answered');
|
|
145
|
-
expect(analytics.track).toHaveBeenCalledTimes(
|
|
144
|
+
expect(analytics.track).toHaveBeenCalledTimes(1); // Called once after reset
|
|
146
145
|
});
|
|
147
146
|
|
|
148
147
|
// 7. Customer submits survey
|
|
@@ -165,8 +164,8 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
165
164
|
expect(analytics.track).toHaveBeenCalledWith('widget_closed');
|
|
166
165
|
});
|
|
167
166
|
|
|
168
|
-
// Verify complete tracking flow
|
|
169
|
-
expect(analytics.track).toHaveBeenCalledTimes(
|
|
167
|
+
// Verify complete tracking flow after reset (question_answered + survey_completed + widget_closed)
|
|
168
|
+
expect(analytics.track).toHaveBeenCalledTimes(3);
|
|
170
169
|
});
|
|
171
170
|
});
|
|
172
171
|
|
|
@@ -177,7 +176,7 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
177
176
|
|
|
178
177
|
const callbacks: WidgetCallbacks = {
|
|
179
178
|
onError: errorHandler,
|
|
180
|
-
|
|
179
|
+
onCompleted: completionHandler,
|
|
181
180
|
};
|
|
182
181
|
|
|
183
182
|
const { getByTestId } = render(
|
|
@@ -298,7 +297,7 @@ describe('E2E: Widget Lifecycle', () => {
|
|
|
298
297
|
callbacks={{
|
|
299
298
|
onPageChanged: pageChangeHandler,
|
|
300
299
|
onQuestionAnswered: questionHandler,
|
|
301
|
-
|
|
300
|
+
onCompleted: completionHandler,
|
|
302
301
|
}}
|
|
303
302
|
/>
|
|
304
303
|
);
|
|
@@ -167,9 +167,9 @@ describe('Integration: WebView Communication', () => {
|
|
|
167
167
|
});
|
|
168
168
|
|
|
169
169
|
it('should handle FORM_COMPLETED message with userId', async () => {
|
|
170
|
-
const
|
|
170
|
+
const mockOnCompleted = jest.fn();
|
|
171
171
|
const callbacks: WidgetCallbacks = {
|
|
172
|
-
|
|
172
|
+
onCompleted: mockOnCompleted,
|
|
173
173
|
};
|
|
174
174
|
|
|
175
175
|
const { getByTestId } = render(
|
|
@@ -186,7 +186,7 @@ describe('Integration: WebView Communication', () => {
|
|
|
186
186
|
});
|
|
187
187
|
|
|
188
188
|
await waitFor(() => {
|
|
189
|
-
expect(
|
|
189
|
+
expect(mockOnCompleted).toHaveBeenCalledWith(expect.any(String));
|
|
190
190
|
});
|
|
191
191
|
});
|
|
192
192
|
|
|
@@ -290,10 +290,10 @@ describe('Integration: WebView Communication', () => {
|
|
|
290
290
|
});
|
|
291
291
|
|
|
292
292
|
it('should handle completeSoluCXWidget survey event', async () => {
|
|
293
|
-
const
|
|
293
|
+
const mockOnCompleted = jest.fn();
|
|
294
294
|
|
|
295
295
|
const { getByTestId } = render(
|
|
296
|
-
<SoluCXWidget {...surveyProps} callbacks={{
|
|
296
|
+
<SoluCXWidget {...surveyProps} callbacks={{ onCompleted: mockOnCompleted }} />
|
|
297
297
|
);
|
|
298
298
|
|
|
299
299
|
await waitFor(() => {
|
|
@@ -305,7 +305,7 @@ describe('Integration: WebView Communication', () => {
|
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
await waitFor(() => {
|
|
308
|
-
expect(
|
|
308
|
+
expect(mockOnCompleted).toHaveBeenCalled();
|
|
309
309
|
});
|
|
310
310
|
});
|
|
311
311
|
});
|
|
@@ -359,14 +359,14 @@ describe('Integration: WebView Communication', () => {
|
|
|
359
359
|
const mockOnPageChanged = jest.fn();
|
|
360
360
|
const mockOnQuestionAnswered = jest.fn();
|
|
361
361
|
const mockOnResize = jest.fn();
|
|
362
|
-
const
|
|
362
|
+
const mockOnCompleted = jest.fn();
|
|
363
363
|
const mockOnClosed = jest.fn();
|
|
364
364
|
|
|
365
365
|
const callbacks: WidgetCallbacks = {
|
|
366
366
|
onPageChanged: mockOnPageChanged,
|
|
367
367
|
onQuestionAnswered: mockOnQuestionAnswered,
|
|
368
368
|
onResize: mockOnResize,
|
|
369
|
-
|
|
369
|
+
onCompleted: mockOnCompleted,
|
|
370
370
|
onClosed: mockOnClosed,
|
|
371
371
|
};
|
|
372
372
|
|
|
@@ -404,7 +404,7 @@ describe('Integration: WebView Communication', () => {
|
|
|
404
404
|
expect(mockOnResize).toHaveBeenCalledTimes(2);
|
|
405
405
|
expect(mockOnPageChanged).toHaveBeenCalledTimes(1);
|
|
406
406
|
expect(mockOnQuestionAnswered).toHaveBeenCalledTimes(1);
|
|
407
|
-
expect(
|
|
407
|
+
expect(mockOnCompleted).toHaveBeenCalledTimes(1);
|
|
408
408
|
expect(mockOnClosed).toHaveBeenCalledTimes(1);
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -78,7 +78,7 @@ describe('WidgetEventService', () => {
|
|
|
78
78
|
it('should handle FORM_COMPLETED event correctly', async () => {
|
|
79
79
|
const result = await service.handleMessage('FORM_COMPLETED', true);
|
|
80
80
|
|
|
81
|
-
expect(mockCallbacks.
|
|
81
|
+
expect(mockCallbacks.onCompleted).toHaveBeenCalledWith('test-user-123');
|
|
82
82
|
expect(result).toEqual({ status: 'success' });
|
|
83
83
|
});
|
|
84
84
|
|
|
@@ -112,7 +112,7 @@ describe('WidgetEventService', () => {
|
|
|
112
112
|
it('should adapt completeSoluCXWidget to FORM_COMPLETED', async () => {
|
|
113
113
|
const result = await service.handleMessage('completeSoluCXWidget', false);
|
|
114
114
|
|
|
115
|
-
expect(mockCallbacks.
|
|
115
|
+
expect(mockCallbacks.onCompleted).toHaveBeenCalledWith('test-user-123');
|
|
116
116
|
expect(result).toEqual({ status: 'success' });
|
|
117
117
|
});
|
|
118
118
|
|
|
@@ -156,27 +156,62 @@ describe('widgetBootstrapService', () => {
|
|
|
156
156
|
expect(result).toBe('https://widget.url/test');
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
it('should
|
|
159
|
+
it('should throw error when response is not ok', async () => {
|
|
160
160
|
const mockResponse = {
|
|
161
161
|
ok: false,
|
|
162
|
+
status: 404,
|
|
163
|
+
statusText: 'Not Found',
|
|
162
164
|
};
|
|
163
165
|
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
164
166
|
|
|
165
|
-
|
|
166
|
-
'api-key',
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
167
|
+
await expect(
|
|
168
|
+
requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id')
|
|
169
|
+
).rejects.toThrow('Failed to fetch widget URL: 404 Not Found');
|
|
170
|
+
});
|
|
170
171
|
|
|
171
|
-
|
|
172
|
+
it('should throw error with status 500', async () => {
|
|
173
|
+
const mockResponse = {
|
|
174
|
+
ok: false,
|
|
175
|
+
status: 500,
|
|
176
|
+
statusText: 'Internal Server Error',
|
|
177
|
+
};
|
|
178
|
+
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
179
|
+
|
|
180
|
+
await expect(
|
|
181
|
+
requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id')
|
|
182
|
+
).rejects.toThrow('Failed to fetch widget URL: 500 Internal Server Error');
|
|
172
183
|
});
|
|
173
184
|
|
|
174
|
-
it('should
|
|
185
|
+
it('should throw error when fetch is not available', async () => {
|
|
175
186
|
(global as any).fetch = undefined;
|
|
176
187
|
|
|
177
|
-
|
|
188
|
+
await expect(
|
|
189
|
+
requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id')
|
|
190
|
+
).rejects.toThrow('Fetch is not available');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should throw error when response payload does not contain url', async () => {
|
|
194
|
+
const mockResponse = {
|
|
195
|
+
ok: true,
|
|
196
|
+
json: jest.fn().mockResolvedValue({}),
|
|
197
|
+
};
|
|
198
|
+
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
199
|
+
|
|
200
|
+
await expect(
|
|
201
|
+
requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id')
|
|
202
|
+
).rejects.toThrow('Widget URL not found in response');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should throw error when response payload url is null', async () => {
|
|
206
|
+
const mockResponse = {
|
|
207
|
+
ok: true,
|
|
208
|
+
json: jest.fn().mockResolvedValue({ url: null }),
|
|
209
|
+
};
|
|
210
|
+
(global.fetch as jest.Mock).mockResolvedValue(mockResponse);
|
|
178
211
|
|
|
179
|
-
expect(
|
|
212
|
+
await expect(
|
|
213
|
+
requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id')
|
|
214
|
+
).rejects.toThrow('Widget URL not found in response');
|
|
180
215
|
});
|
|
181
216
|
});
|
|
182
217
|
});
|
|
@@ -10,9 +10,9 @@ import { StorageService } from '../services/storage';
|
|
|
10
10
|
|
|
11
11
|
function getUserId(widgetData: WidgetData): string {
|
|
12
12
|
return (
|
|
13
|
-
widgetData
|
|
14
|
-
widgetData
|
|
15
|
-
widgetData
|
|
13
|
+
widgetData?.customer_id ??
|
|
14
|
+
widgetData?.document ??
|
|
15
|
+
widgetData?.email ??
|
|
16
16
|
'default_user'
|
|
17
17
|
);
|
|
18
18
|
}
|
|
@@ -4,7 +4,6 @@ export interface WidgetCallbacks {
|
|
|
4
4
|
onPreOpen?: (userId: string) => void;
|
|
5
5
|
onOpened?: (userId: string) => void;
|
|
6
6
|
onBlock?: (blockReason: BlockReason | undefined) => void;
|
|
7
|
-
onPingError?: (error: unknown) => void;
|
|
8
7
|
onClosed?: () => void;
|
|
9
8
|
onError?: (message: string) => void;
|
|
10
9
|
onPageChanged?: (page: string) => void;
|
|
@@ -50,18 +50,27 @@ export async function requestWidgetUrl(
|
|
|
50
50
|
instanceKey: SoluCXKey,
|
|
51
51
|
requestParams: WidgetData,
|
|
52
52
|
userId: string,
|
|
53
|
-
): Promise<string
|
|
54
|
-
if (typeof fetch !== "function")
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
if (typeof fetch !== "function") {
|
|
55
|
+
throw new Error("Fetch is not available");
|
|
56
|
+
}
|
|
55
57
|
|
|
56
58
|
const params = buildRequestParams(requestParams);
|
|
57
59
|
const url = `${RATING_FORM_ENDPOINT}?${params.toString()}`;
|
|
58
60
|
const headers = buildRequestHeaders(instanceKey, userId);
|
|
59
61
|
const response = await fetch(url, { method: "GET", headers });
|
|
60
62
|
|
|
61
|
-
if (!response.ok)
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`Failed to fetch widget URL: ${response.status} ${response.statusText}`);
|
|
65
|
+
}
|
|
62
66
|
|
|
63
67
|
const payload: Payload = await response.json();
|
|
64
|
-
|
|
68
|
+
|
|
69
|
+
if (!payload?.url) {
|
|
70
|
+
throw new Error("Widget URL not found in response");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return payload.url;
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
export { buildRequestParams };
|