@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.
Files changed (115) hide show
  1. package/lib/SoluCXWidget.d.ts +12 -0
  2. package/lib/SoluCXWidget.d.ts.map +1 -0
  3. package/lib/SoluCXWidget.js +110 -0
  4. package/lib/SoluCXWidget.js.map +1 -0
  5. package/lib/components/CloseButton.d.ts +8 -0
  6. package/lib/components/CloseButton.d.ts.map +1 -0
  7. package/lib/components/CloseButton.js +31 -0
  8. package/lib/components/CloseButton.js.map +1 -0
  9. package/lib/components/InlineWidget.d.ts +10 -0
  10. package/lib/components/InlineWidget.d.ts.map +1 -0
  11. package/lib/components/InlineWidget.js +19 -0
  12. package/lib/components/InlineWidget.js.map +1 -0
  13. package/lib/components/ModalWidget.d.ts +10 -0
  14. package/lib/components/ModalWidget.d.ts.map +1 -0
  15. package/lib/components/ModalWidget.js +27 -0
  16. package/lib/components/ModalWidget.js.map +1 -0
  17. package/lib/components/OverlayWidget.d.ts +12 -0
  18. package/lib/components/OverlayWidget.d.ts.map +1 -0
  19. package/lib/components/OverlayWidget.js +55 -0
  20. package/lib/components/OverlayWidget.js.map +1 -0
  21. package/lib/constants/Constants.d.ts +3 -0
  22. package/lib/constants/Constants.d.ts.map +1 -0
  23. package/lib/constants/Constants.js +10 -0
  24. package/lib/constants/Constants.js.map +1 -0
  25. package/lib/constants/webViewConstants.d.ts +12 -0
  26. package/lib/constants/webViewConstants.d.ts.map +1 -0
  27. package/lib/constants/webViewConstants.js +19 -0
  28. package/lib/constants/webViewConstants.js.map +1 -0
  29. package/lib/hooks/index.d.ts +3 -0
  30. package/lib/hooks/index.d.ts.map +1 -0
  31. package/lib/hooks/index.js +8 -0
  32. package/lib/hooks/index.js.map +1 -0
  33. package/lib/hooks/useDeviceInfoCollector.d.ts +14 -0
  34. package/lib/hooks/useDeviceInfoCollector.d.ts.map +1 -0
  35. package/lib/hooks/useDeviceInfoCollector.js +54 -0
  36. package/lib/hooks/useDeviceInfoCollector.js.map +1 -0
  37. package/lib/hooks/useHeightAnimation.d.ts +9 -0
  38. package/lib/hooks/useHeightAnimation.d.ts.map +1 -0
  39. package/lib/hooks/useHeightAnimation.js +19 -0
  40. package/lib/hooks/useHeightAnimation.js.map +1 -0
  41. package/lib/hooks/useWidgetHeight.d.ts +13 -0
  42. package/lib/hooks/useWidgetHeight.d.ts.map +1 -0
  43. package/lib/hooks/useWidgetHeight.js +21 -0
  44. package/lib/hooks/useWidgetHeight.js.map +1 -0
  45. package/lib/hooks/useWidgetState.d.ts +15 -0
  46. package/lib/hooks/useWidgetState.d.ts.map +1 -0
  47. package/lib/hooks/useWidgetState.js +79 -0
  48. package/lib/hooks/useWidgetState.js.map +1 -0
  49. package/lib/index.d.ts +13 -0
  50. package/lib/index.d.ts.map +1 -0
  51. package/lib/index.js +43 -0
  52. package/lib/index.js.map +1 -0
  53. package/lib/interfaces/WidgetCallbacks.d.ts +14 -0
  54. package/lib/interfaces/WidgetCallbacks.d.ts.map +1 -0
  55. package/lib/interfaces/WidgetCallbacks.js +3 -0
  56. package/lib/interfaces/WidgetCallbacks.js.map +1 -0
  57. package/lib/interfaces/WidgetData.d.ts +21 -0
  58. package/lib/interfaces/WidgetData.d.ts.map +1 -0
  59. package/lib/interfaces/WidgetData.js +3 -0
  60. package/lib/interfaces/WidgetData.js.map +1 -0
  61. package/lib/interfaces/WidgetOptions.d.ts +9 -0
  62. package/lib/interfaces/WidgetOptions.d.ts.map +1 -0
  63. package/lib/interfaces/WidgetOptions.js +3 -0
  64. package/lib/interfaces/WidgetOptions.js.map +1 -0
  65. package/lib/interfaces/WidgetResponse.d.ts +10 -0
  66. package/lib/interfaces/WidgetResponse.d.ts.map +1 -0
  67. package/lib/interfaces/WidgetResponse.js +12 -0
  68. package/lib/interfaces/WidgetResponse.js.map +1 -0
  69. package/lib/interfaces/WidgetSamplerLog.d.ts +7 -0
  70. package/lib/interfaces/WidgetSamplerLog.d.ts.map +1 -0
  71. package/lib/interfaces/WidgetSamplerLog.js +3 -0
  72. package/lib/interfaces/WidgetSamplerLog.js.map +1 -0
  73. package/lib/interfaces/index.d.ts +12 -0
  74. package/lib/interfaces/index.d.ts.map +1 -0
  75. package/lib/interfaces/index.js +3 -0
  76. package/lib/interfaces/index.js.map +1 -0
  77. package/lib/services/ClientVersionCollector.d.ts +2 -0
  78. package/lib/services/ClientVersionCollector.d.ts.map +1 -0
  79. package/lib/services/ClientVersionCollector.js +20 -0
  80. package/lib/services/ClientVersionCollector.js.map +1 -0
  81. package/lib/services/storage.d.ts +8 -0
  82. package/lib/services/storage.d.ts.map +1 -0
  83. package/lib/services/storage.js +23 -0
  84. package/lib/services/storage.js.map +1 -0
  85. package/lib/services/widgetBootstrapService.d.ts +5 -0
  86. package/lib/services/widgetBootstrapService.d.ts.map +1 -0
  87. package/lib/services/widgetBootstrapService.js +60 -0
  88. package/lib/services/widgetBootstrapService.js.map +1 -0
  89. package/lib/services/widgetEventService.d.ts +19 -0
  90. package/lib/services/widgetEventService.d.ts.map +1 -0
  91. package/lib/services/widgetEventService.js +79 -0
  92. package/lib/services/widgetEventService.js.map +1 -0
  93. package/lib/services/widgetValidationService.d.ts +18 -0
  94. package/lib/services/widgetValidationService.d.ts.map +1 -0
  95. package/lib/services/widgetValidationService.js +71 -0
  96. package/lib/services/widgetValidationService.js.map +1 -0
  97. package/lib/styles/widgetStyles.d.ts +87 -0
  98. package/lib/styles/widgetStyles.d.ts.map +1 -0
  99. package/lib/styles/widgetStyles.js +59 -0
  100. package/lib/styles/widgetStyles.js.map +1 -0
  101. package/lib/utils/urlUtils.d.ts +3 -0
  102. package/lib/utils/urlUtils.d.ts.map +1 -0
  103. package/lib/utils/urlUtils.js +13 -0
  104. package/lib/utils/urlUtils.js.map +1 -0
  105. package/package.json +5 -3
  106. package/src/SoluCXWidget.tsx +23 -17
  107. package/src/__tests__/SoluCXWidget.rendering.test.tsx +492 -153
  108. package/src/__tests__/e2e/widget-lifecycle.test.tsx +9 -10
  109. package/src/__tests__/integration/webview-communication.test.tsx +9 -9
  110. package/src/__tests__/useWidgetState.test.ts +2 -2
  111. package/src/__tests__/widgetBootstrapService.test.ts +45 -10
  112. package/src/hooks/useWidgetState.ts +3 -3
  113. package/src/interfaces/WidgetCallbacks.ts +0 -1
  114. package/src/services/widgetBootstrapService.ts +13 -4
  115. 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
- onPartialCompleted: (userId) => {
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(2); // Called twice
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(4);
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
- onPartialCompleted: completionHandler,
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
- onPartialCompleted: completionHandler,
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 mockOnPartialCompleted = jest.fn();
170
+ const mockOnCompleted = jest.fn();
171
171
  const callbacks: WidgetCallbacks = {
172
- onPartialCompleted: mockOnPartialCompleted,
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(mockOnPartialCompleted).toHaveBeenCalledWith(expect.any(String));
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 mockOnPartialCompleted = jest.fn();
293
+ const mockOnCompleted = jest.fn();
294
294
 
295
295
  const { getByTestId } = render(
296
- <SoluCXWidget {...surveyProps} callbacks={{ onPartialCompleted: mockOnPartialCompleted }} />
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(mockOnPartialCompleted).toHaveBeenCalled();
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 mockOnPartialCompleted = jest.fn();
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
- onPartialCompleted: mockOnPartialCompleted,
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(mockOnPartialCompleted).toHaveBeenCalledTimes(1);
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.onPartialCompleted).toHaveBeenCalledWith('test-user-123');
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.onPartialCompleted).toHaveBeenCalledWith('test-user-123');
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 return undefined when response is not ok', async () => {
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
- const result = await requestWidgetUrl(
166
- 'api-key',
167
- { customer_id: 'cust' },
168
- 'user-id'
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
- expect(result).toBeUndefined();
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 return undefined when fetch is not available', async () => {
185
+ it('should throw error when fetch is not available', async () => {
175
186
  (global as any).fetch = undefined;
176
187
 
177
- const result = await requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id');
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(result).toBeUndefined();
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.customer_id ??
14
- widgetData.document ??
15
- widgetData.email ??
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 | undefined> {
54
- if (typeof fetch !== "function") return;
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) return;
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
- return payload?.url;
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 };
@@ -79,7 +79,7 @@ export class WidgetEventService {
79
79
  }
80
80
 
81
81
  private handleFormCompleted(): WidgetResponse {
82
- this.callbacks?.onPartialCompleted?.(this.userId);
82
+ this.callbacks?.onCompleted?.(this.userId);
83
83
  return { status: "success" };
84
84
  }
85
85