@solucx/react-native-solucx-widget 0.1.16 → 0.2.1

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 (34) hide show
  1. package/package.json +31 -4
  2. package/src/SoluCXWidget.tsx +108 -53
  3. package/src/__mocks__/expo-modules-core-web.js +16 -0
  4. package/src/__mocks__/expo-modules-core.js +33 -0
  5. package/src/__tests__/ClientVersionCollector.test.ts +55 -0
  6. package/src/__tests__/CloseButton.test.tsx +47 -0
  7. package/src/__tests__/Constants.test.ts +17 -0
  8. package/src/__tests__/InlineWidget.rendering.test.tsx +81 -0
  9. package/src/__tests__/ModalWidget.rendering.test.tsx +157 -0
  10. package/src/__tests__/OverlayWidget.rendering.test.tsx +123 -0
  11. package/src/__tests__/SoluCXWidget.rendering.test.tsx +504 -0
  12. package/src/__tests__/e2e/widget-lifecycle.test.tsx +352 -0
  13. package/src/__tests__/integration/webview-communication-simple.test.tsx +147 -0
  14. package/src/__tests__/integration/webview-communication.test.tsx +417 -0
  15. package/src/__tests__/useDeviceInfoCollector.test.ts +109 -0
  16. package/src/__tests__/useWidgetState.test.ts +76 -84
  17. package/src/__tests__/widgetBootstrapService.test.ts +182 -0
  18. package/src/components/ModalWidget.tsx +3 -5
  19. package/src/components/OverlayWidget.tsx +1 -1
  20. package/src/constants/Constants.ts +4 -0
  21. package/src/constants/webViewConstants.ts +1 -0
  22. package/src/hooks/useDeviceInfoCollector.ts +67 -0
  23. package/src/hooks/useWidgetState.ts +4 -4
  24. package/src/index.ts +4 -0
  25. package/src/interfaces/WidgetCallbacks.ts +14 -0
  26. package/src/interfaces/index.ts +3 -2
  27. package/src/services/ClientVersionCollector.ts +15 -0
  28. package/src/services/storage.ts +2 -2
  29. package/src/services/widgetBootstrapService.ts +67 -0
  30. package/src/services/widgetEventService.ts +14 -30
  31. package/src/services/widgetValidationService.ts +29 -13
  32. package/src/setupTests.js +43 -0
  33. package/src/styles/widgetStyles.ts +1 -1
  34. package/src/utils/urlUtils.ts +2 -2
@@ -1,46 +1,34 @@
1
1
  import { WidgetEventService } from '../services/widgetEventService';
2
- import { WidgetOptions } from '../interfaces';
3
-
4
- const mockWidgetValidationService = {
5
- shouldDisplayWidget: jest.fn().mockResolvedValue(true)
6
- };
7
-
8
- jest.mock('../services/widgetValidationService', () => ({
9
- WidgetValidationService: jest.fn().mockImplementation(() => mockWidgetValidationService)
10
- }));
2
+ import type { WidgetCallbacks } from '../interfaces';
11
3
 
12
4
  describe('WidgetEventService', () => {
13
5
  let mockSetIsWidgetVisible: jest.Mock;
14
6
  let mockResize: jest.Mock;
15
- let service: WidgetEventService;
16
- let open: jest.Mock;
17
7
  let mockUserId: string;
18
- let mockWidgetOptions: WidgetOptions;
8
+ let mockCallbacks: WidgetCallbacks;
9
+ let service: WidgetEventService;
19
10
 
20
11
  beforeEach(() => {
21
12
  jest.clearAllMocks();
22
13
 
23
14
  mockSetIsWidgetVisible = jest.fn();
24
15
  mockResize = jest.fn();
25
- open = jest.fn();
26
16
  mockUserId = 'test-user-123';
27
- mockWidgetOptions = {
28
- height: 400,
29
- retry: {
30
- attempts: 5,
31
- interval: 1
32
- },
33
- waitDelayAfterRating: 60
17
+ mockCallbacks = {
18
+ onClosed: jest.fn(),
19
+ onError: jest.fn(),
20
+ onPageChanged: jest.fn(),
21
+ onQuestionAnswered: jest.fn(),
22
+ onCompleted: jest.fn(),
23
+ onPartialCompleted: jest.fn(),
24
+ onResize: jest.fn(),
34
25
  };
35
26
 
36
- mockWidgetValidationService.shouldDisplayWidget.mockResolvedValue(true);
37
-
38
27
  service = new WidgetEventService(
39
28
  mockSetIsWidgetVisible,
40
29
  mockResize,
41
- open,
42
30
  mockUserId,
43
- mockWidgetOptions
31
+ mockCallbacks,
44
32
  );
45
33
  });
46
34
 
@@ -49,38 +37,19 @@ describe('WidgetEventService', () => {
49
37
  jest.restoreAllMocks();
50
38
  });
51
39
 
52
- it('should handle FORM_OPENED event correctly', async () => {
53
- const result = await service.handleMessage('FORM_OPENED', true);
54
-
55
- expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
56
- expect(open).toHaveBeenCalled();
57
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
58
- expect(result).toEqual({ status: 'success' });
59
- });
60
-
61
- it('should prevent widget from opening when validation fails', async () => {
62
- mockWidgetValidationService.shouldDisplayWidget.mockResolvedValueOnce(await Promise.resolve(false));
63
-
64
- const result = await service.handleMessage('FORM_OPENED', true);
65
-
66
- expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
67
- expect(open).not.toHaveBeenCalled();
68
- expect(mockSetIsWidgetVisible).not.toHaveBeenCalled();
69
- expect(result).toEqual({ status: 'error', message: 'Widget not allowed' });
70
- });
71
-
72
40
  it('should handle FORM_CLOSE event correctly', async () => {
73
41
  const result = await service.handleMessage('FORM_CLOSE', true);
74
42
 
75
43
  expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
44
+ expect(mockCallbacks.onClosed).toHaveBeenCalledTimes(1);
76
45
  expect(result).toEqual({ status: 'success' });
77
46
  });
78
47
 
79
48
  it('should handle FORM_RESIZE event correctly', async () => {
80
49
  const result = await service.handleMessage('FORM_RESIZE-350', true);
81
50
 
82
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
83
51
  expect(mockResize).toHaveBeenCalledWith('350');
52
+ expect(mockCallbacks.onResize).toHaveBeenCalledWith('350');
84
53
  expect(result).toEqual({ status: 'success' });
85
54
  });
86
55
 
@@ -88,71 +57,69 @@ describe('WidgetEventService', () => {
88
57
  const result = await service.handleMessage('FORM_ERROR-Something went wrong', true);
89
58
 
90
59
  expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
60
+ expect(mockCallbacks.onError).toHaveBeenCalledWith('Something went wrong');
91
61
  expect(result).toEqual({ status: 'error', message: 'Something went wrong' });
92
62
  });
93
63
 
94
- it('should adapt survey keys to widget keys correctly for non-form widgets', async () => {
95
- const result = await service.handleMessage('closeSoluCXWidget', false);
64
+ it('should handle FORM_PAGECHANGED event correctly', async () => {
65
+ const result = await service.handleMessage('FORM_PAGECHANGED-page2', true);
96
66
 
97
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
67
+ expect(mockCallbacks.onPageChanged).toHaveBeenCalledWith('page2');
98
68
  expect(result).toEqual({ status: 'success' });
99
69
  });
100
70
 
101
- it('should return error for unknown events', async () => {
102
- const result = await service.handleMessage('UNKNOWN_EVENT', true);
71
+ it('should handle QUESTION_ANSWERED event correctly', async () => {
72
+ const result = await service.handleMessage('QUESTION_ANSWERED', true);
103
73
 
104
- expect(result).toEqual({ status: 'error', message: 'Unknown event' });
74
+ expect(mockCallbacks.onQuestionAnswered).toHaveBeenCalledTimes(1);
75
+ expect(result).toEqual({ status: 'success' });
105
76
  });
106
77
 
107
- it('should handle messages with no value correctly', async () => {
108
- const result = await service.handleMessage('FORM_OPENED', true);
78
+ it('should handle FORM_COMPLETED event correctly', async () => {
79
+ const result = await service.handleMessage('FORM_COMPLETED', true);
109
80
 
110
- expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
111
- expect(open).toHaveBeenCalled();
112
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
81
+ expect(mockCallbacks.onCompleted).toHaveBeenCalledWith('test-user-123');
113
82
  expect(result).toEqual({ status: 'success' });
114
83
  });
115
84
 
116
- it('should handle FORM_PAGECHANGED event correctly', async () => {
117
- const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
118
- const result = await service.handleMessage('FORM_PAGECHANGED-page2', true);
85
+ it('should handle FORM_PARTIALCOMPLETED event correctly', async () => {
86
+ const result = await service.handleMessage('FORM_PARTIALCOMPLETED', true);
119
87
 
120
- expect(consoleSpy).toHaveBeenCalledWith("Page changed:", "page2");
88
+ expect(mockCallbacks.onPartialCompleted).toHaveBeenCalledWith('test-user-123');
121
89
  expect(result).toEqual({ status: 'success' });
122
-
123
- consoleSpy.mockRestore();
124
90
  });
125
91
 
126
- it('should handle QUESTION_ANSWERED event correctly', async () => {
127
- const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
128
- const result = await service.handleMessage('QUESTION_ANSWERED', true);
129
- expect(consoleSpy).toHaveBeenCalledWith("Question answered");
130
- expect(result).toEqual({ status: 'success' });
92
+ it('should return error for unknown events', async () => {
93
+ const result = await service.handleMessage('UNKNOWN_EVENT', true);
131
94
 
132
- consoleSpy.mockRestore();
95
+ expect(result).toEqual({ status: 'error', message: 'Unknown event' });
133
96
  });
134
97
 
135
- it('should handle FORM_COMPLETED event correctly', async () => {
136
- const result = await service.handleMessage('FORM_COMPLETED', true);
98
+ it('should return error when FORM_OPENED is sent since it was removed from event handlers', async () => {
99
+ const result = await service.handleMessage('FORM_OPENED', true);
137
100
 
138
- expect(result).toEqual({ status: 'success' });
101
+ expect(result).toEqual({ status: 'error', message: 'Unknown event' });
139
102
  });
140
103
 
141
- it('should handle FORM_PARTIALCOMPLETED event correctly', async () => {
142
- const result = await service.handleMessage('FORM_PARTIALCOMPLETED', true);
104
+ it('should adapt closeSoluCXWidget to FORM_CLOSE', async () => {
105
+ const result = await service.handleMessage('closeSoluCXWidget', false);
143
106
 
107
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
108
+ expect(mockCallbacks.onClosed).toHaveBeenCalledTimes(1);
144
109
  expect(result).toEqual({ status: 'success' });
145
110
  });
146
111
 
147
112
  it('should adapt completeSoluCXWidget to FORM_COMPLETED', async () => {
148
113
  const result = await service.handleMessage('completeSoluCXWidget', false);
149
114
 
115
+ expect(mockCallbacks.onCompleted).toHaveBeenCalledWith('test-user-123');
150
116
  expect(result).toEqual({ status: 'success' });
151
117
  });
152
118
 
153
119
  it('should adapt partialSoluCXWidget to FORM_PARTIALCOMPLETED', async () => {
154
120
  const result = await service.handleMessage('partialSoluCXWidget', false);
155
121
 
122
+ expect(mockCallbacks.onPartialCompleted).toHaveBeenCalledWith('test-user-123');
156
123
  expect(result).toEqual({ status: 'success' });
157
124
  });
158
125
 
@@ -160,15 +127,7 @@ describe('WidgetEventService', () => {
160
127
  const result = await service.handleMessage('dismissSoluCXWidget', false);
161
128
 
162
129
  expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
163
- expect(result).toEqual({ status: 'success' });
164
- });
165
-
166
- it('should adapt openSoluCXWidget to FORM_OPENED', async () => {
167
- const result = await service.handleMessage('openSoluCXWidget', false);
168
-
169
- expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
170
- expect(open).toHaveBeenCalled();
171
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
130
+ expect(mockCallbacks.onClosed).toHaveBeenCalledTimes(1);
172
131
  expect(result).toEqual({ status: 'success' });
173
132
  });
174
133
 
@@ -176,14 +135,47 @@ describe('WidgetEventService', () => {
176
135
  const result = await service.handleMessage('errorSoluCXWidget-Network error', false);
177
136
 
178
137
  expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
138
+ expect(mockCallbacks.onError).toHaveBeenCalledWith('Network error');
179
139
  expect(result).toEqual({ status: 'error', message: 'Network error' });
180
140
  });
181
141
 
182
142
  it('should adapt resizeSoluCXWidget to FORM_RESIZE', async () => {
183
143
  const result = await service.handleMessage('resizeSoluCXWidget-400', false);
184
144
 
185
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
186
145
  expect(mockResize).toHaveBeenCalledWith('400');
146
+ expect(mockCallbacks.onResize).toHaveBeenCalledWith('400');
147
+ expect(result).toEqual({ status: 'success' });
148
+ });
149
+
150
+ it('should adapt openSoluCXWidget to FORM_OPENED which returns unknown event', async () => {
151
+ const result = await service.handleMessage('openSoluCXWidget', false);
152
+
153
+ expect(result).toEqual({ status: 'error', message: 'Unknown event' });
154
+ });
155
+
156
+ it('should not call any callback when event is unknown', async () => {
157
+ await service.handleMessage('SOME_RANDOM_EVENT', true);
158
+
159
+ expect(mockSetIsWidgetVisible).not.toHaveBeenCalled();
160
+ expect(mockResize).not.toHaveBeenCalled();
161
+ expect(mockCallbacks.onClosed).not.toHaveBeenCalled();
162
+ expect(mockCallbacks.onError).not.toHaveBeenCalled();
163
+ expect(mockCallbacks.onPageChanged).not.toHaveBeenCalled();
164
+ expect(mockCallbacks.onQuestionAnswered).not.toHaveBeenCalled();
165
+ expect(mockCallbacks.onPartialCompleted).not.toHaveBeenCalled();
166
+ expect(mockCallbacks.onResize).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('should work without callbacks when none are provided', async () => {
170
+ const serviceWithoutCallbacks = new WidgetEventService(
171
+ mockSetIsWidgetVisible,
172
+ mockResize,
173
+ mockUserId,
174
+ );
175
+
176
+ const result = await serviceWithoutCallbacks.handleMessage('FORM_CLOSE', true);
177
+
178
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
187
179
  expect(result).toEqual({ status: 'success' });
188
180
  });
189
- });
181
+ });
@@ -0,0 +1,182 @@
1
+ import { requestWidgetUrl, buildRequestParams } from '../services/widgetBootstrapService';
2
+ import { WidgetData } from '../interfaces';
3
+
4
+ // Mock das dependências
5
+ jest.mock('../constants/Constants', () => ({
6
+ SDK_NAME: 'rn-widget-sdk',
7
+ SDK_VERSION: '0.1.16',
8
+ }));
9
+
10
+ jest.mock('../services/ClientVersionCollector', () => ({
11
+ getClientVersion: jest.fn(() => '1.0.0'),
12
+ }));
13
+
14
+ jest.mock('../hooks/useDeviceInfoCollector', () => ({
15
+ getDeviceInfo: jest.fn(() => ({
16
+ platform: 'ios',
17
+ osVersion: '16.0',
18
+ deviceType: 'phone',
19
+ model: 'iPhone 14 Pro',
20
+ })),
21
+ }));
22
+
23
+ // Mock global do fetch
24
+ const originalFetch = global.fetch;
25
+
26
+ describe('widgetBootstrapService', () => {
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ global.fetch = jest.fn();
30
+ });
31
+
32
+ afterEach(() => {
33
+ global.fetch = originalFetch;
34
+ });
35
+
36
+ describe('buildRequestParams', () => {
37
+ it('should build URL parameters from widget data', () => {
38
+ const data: WidgetData = {
39
+ customer_id: 'cust-123',
40
+ email: 'test@example.com',
41
+ journey: 'checkout',
42
+ };
43
+
44
+ const params = buildRequestParams(data);
45
+
46
+ expect(params.get('customer_id')).toBe('cust-123');
47
+ expect(params.get('email')).toBe('test@example.com');
48
+ expect(params.get('journey')).toBe('checkout');
49
+ expect(params.get('transactionId')).toBe('');
50
+ expect(params.get('attemptId')).toBe('');
51
+ });
52
+
53
+ it('should skip undefined and null values', () => {
54
+ const data: WidgetData = {
55
+ customer_id: 'cust-123',
56
+ email: undefined,
57
+ name: null as any,
58
+ };
59
+
60
+ const params = buildRequestParams(data);
61
+
62
+ expect(params.has('customer_id')).toBe(true);
63
+ expect(params.has('email')).toBe(false);
64
+ expect(params.has('name')).toBe(false);
65
+ });
66
+
67
+ it('should convert numbers to strings', () => {
68
+ const data: WidgetData = {
69
+ amount: 100.50,
70
+ score: 5,
71
+ };
72
+
73
+ const params = buildRequestParams(data);
74
+
75
+ expect(params.get('amount')).toBe('100.5');
76
+ expect(params.get('score')).toBe('5');
77
+ });
78
+ });
79
+
80
+ describe('requestWidgetUrl', () => {
81
+ it('should send request with correct Sec-CH-UA headers', async () => {
82
+ const mockResponse = {
83
+ ok: true,
84
+ json: jest.fn().mockResolvedValue({ url: 'https://widget.url' }),
85
+ };
86
+ (global.fetch as jest.Mock).mockResolvedValue(mockResponse);
87
+
88
+ const data: WidgetData = {
89
+ customer_id: 'cust-123',
90
+ journey: 'test',
91
+ };
92
+
93
+ await requestWidgetUrl('api-key-123', data, 'user-id-456');
94
+
95
+ expect(global.fetch).toHaveBeenCalledWith(
96
+ expect.stringContaining('customer_id=cust-123'),
97
+ expect.objectContaining({
98
+ method: 'GET',
99
+ headers: expect.objectContaining({
100
+ 'x-solucx-api-key': 'api-key-123',
101
+ 'x-solucx-device-id': 'user-id-456',
102
+ 'Sec-CH-UA': '"rn-widget-sdk";v="0.1.16", "app";v="1.0.0"',
103
+ 'Sec-CH-UA-Platform': '"ios"',
104
+ 'Sec-CH-UA-Mobile': '?1',
105
+ 'Sec-CH-UA-Platform-Version': '"16.0"',
106
+ 'Sec-CH-UA-Model': '"iPhone 14 Pro"',
107
+ 'User-Agent': 'rn-widget-sdk/0.1.16 (ios; OS 16.0; iPhone 14 Pro) App/1.0.0',
108
+ }),
109
+ })
110
+ );
111
+ });
112
+
113
+ it('should set Sec-CH-UA-Mobile to ?0 for tablets', async () => {
114
+ const { getDeviceInfo } = require('../hooks/useDeviceInfoCollector');
115
+ getDeviceInfo.mockReturnValueOnce({
116
+ platform: 'android',
117
+ osVersion: '13.0',
118
+ deviceType: 'tablet',
119
+ model: 'Samsung Galaxy Tab S8',
120
+ });
121
+
122
+ const mockResponse = {
123
+ ok: true,
124
+ json: jest.fn().mockResolvedValue({ url: 'https://widget.url' }),
125
+ };
126
+ (global.fetch as jest.Mock).mockResolvedValue(mockResponse);
127
+
128
+ const data: WidgetData = { customer_id: 'cust-123' };
129
+
130
+ await requestWidgetUrl('api-key-123', data, 'user-id-456');
131
+
132
+ expect(global.fetch).toHaveBeenCalledWith(
133
+ expect.any(String),
134
+ expect.objectContaining({
135
+ headers: expect.objectContaining({
136
+ 'Sec-CH-UA-Mobile': '?0',
137
+ 'Sec-CH-UA-Platform': '"android"',
138
+ }),
139
+ })
140
+ );
141
+ });
142
+
143
+ it('should return widget URL on success', async () => {
144
+ const mockResponse = {
145
+ ok: true,
146
+ json: jest.fn().mockResolvedValue({ url: 'https://widget.url/test' }),
147
+ };
148
+ (global.fetch as jest.Mock).mockResolvedValue(mockResponse);
149
+
150
+ const result = await requestWidgetUrl(
151
+ 'api-key',
152
+ { customer_id: 'cust' },
153
+ 'user-id'
154
+ );
155
+
156
+ expect(result).toBe('https://widget.url/test');
157
+ });
158
+
159
+ it('should return undefined when response is not ok', async () => {
160
+ const mockResponse = {
161
+ ok: false,
162
+ };
163
+ (global.fetch as jest.Mock).mockResolvedValue(mockResponse);
164
+
165
+ const result = await requestWidgetUrl(
166
+ 'api-key',
167
+ { customer_id: 'cust' },
168
+ 'user-id'
169
+ );
170
+
171
+ expect(result).toBeUndefined();
172
+ });
173
+
174
+ it('should return undefined when fetch is not available', async () => {
175
+ (global as any).fetch = undefined;
176
+
177
+ const result = await requestWidgetUrl('api-key', { customer_id: 'cust' }, 'user-id');
178
+
179
+ expect(result).toBeUndefined();
180
+ });
181
+ });
182
+ });
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { Modal, SafeAreaView, View, Animated } from 'react-native';
2
+ import { Modal, View, Animated } from 'react-native';
3
+ import { SafeAreaView } from 'react-native-safe-area-context';
3
4
  import { styles, getWidgetVisibility } from '../styles/widgetStyles';
4
5
  import { CloseButton } from './CloseButton';
5
6
  import { useHeightAnimation } from '../hooks/useHeightAnimation';
@@ -17,8 +18,6 @@ export const ModalWidget: React.FC<ModalWidgetProps> = ({
17
18
  children,
18
19
  onClose,
19
20
  }) => {
20
- const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(true);
21
-
22
21
  const { animatedHeightStyle, updateHeight } = useHeightAnimation(height);
23
22
 
24
23
  useEffect(() => {
@@ -29,7 +28,7 @@ export const ModalWidget: React.FC<ModalWidgetProps> = ({
29
28
  <SafeAreaView>
30
29
  <Modal
31
30
  transparent
32
- visible={isWidgetVisible}
31
+ visible={visible}
33
32
  animationType="slide"
34
33
  hardwareAccelerated
35
34
  >
@@ -45,7 +44,6 @@ export const ModalWidget: React.FC<ModalWidgetProps> = ({
45
44
  <CloseButton
46
45
  visible={visible}
47
46
  onPress={() => {
48
- setIsWidgetVisible(false);
49
47
  if (onClose) {
50
48
  onClose();
51
49
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { View, ViewStyle, Animated } from 'react-native';
2
+ import { View, type ViewStyle, Animated } from 'react-native';
3
3
  import { initialWindowMetrics } from 'react-native-safe-area-context';
4
4
  import { getWidgetStyles, getWidgetVisibility } from '../styles/widgetStyles';
5
5
  import { FIXED_Z_INDEX } from '../constants/webViewConstants';
@@ -0,0 +1,4 @@
1
+ import packageJson from "../../package.json";
2
+
3
+ export const SDK_VERSION = packageJson.version;
4
+ export const SDK_NAME = "rn-widget-sdk";
@@ -1,4 +1,5 @@
1
1
  export const BASE_URL = 'https://survey-link.solucx.com.br/link';
2
+ export const RATING_FORM_ENDPOINT = 'https://widget-api.solucx.com.br/widget/preflight';
2
3
  export const STORAGE_KEY = '@solucxWidgetLog';
3
4
  export const DEFAULT_CHANNEL_NUMBER = 1;
4
5
  export const DEFAULT_CHANNEL = 'widget';
@@ -0,0 +1,67 @@
1
+ import { Platform, Dimensions } from "react-native";
2
+
3
+ export interface DeviceInfo {
4
+ platform: "ios" | "android" | "web" | "windows" | "macos";
5
+ osVersion: string;
6
+ screenWidth: number;
7
+ screenHeight: number;
8
+ windowWidth: number;
9
+ windowHeight: number;
10
+ scale: number;
11
+ fontScale: number;
12
+ deviceType?: "tablet" | "phone";
13
+ model: string;
14
+ }
15
+
16
+ const isTablet = (): boolean => {
17
+ const { width, height } = Dimensions.get("screen");
18
+ const aspectRatio = height / width;
19
+
20
+ return Math.min(width, height) >= 600 && aspectRatio < 1.6;
21
+ };
22
+
23
+ const getDeviceModel = (): string => {
24
+ try {
25
+ const DeviceInfo = require('react-native-device-info');
26
+ if (DeviceInfo?.default?.getModel) {
27
+ return DeviceInfo.default.getModel();
28
+ }
29
+ if (DeviceInfo?.getModel) {
30
+ return DeviceInfo.getModel();
31
+ }
32
+ } catch (error) {
33
+ // react-native-device-info não disponível
34
+ }
35
+
36
+ try {
37
+ const constants = Platform.constants as any;
38
+ if (constants?.Model) {
39
+ return constants.Model;
40
+ }
41
+ if (constants?.model) {
42
+ return constants.model;
43
+ }
44
+ } catch (error) {
45
+ // Nada disponível
46
+ }
47
+
48
+ return 'unknown';
49
+ };
50
+
51
+ export const getDeviceInfo = (): DeviceInfo => {
52
+ const screen = Dimensions.get("screen");
53
+ const window = Dimensions.get("window");
54
+
55
+ return {
56
+ platform: Platform.OS,
57
+ osVersion: Platform.Version.toString(),
58
+ screenWidth: screen.width,
59
+ screenHeight: screen.height,
60
+ windowWidth: window.width,
61
+ windowHeight: window.height,
62
+ scale: screen.scale,
63
+ fontScale: screen.fontScale,
64
+ deviceType: isTablet() ? "tablet" : "phone",
65
+ model: getDeviceModel(),
66
+ };
67
+ };
@@ -1,6 +1,6 @@
1
1
  import { useState, useCallback, useMemo } from 'react';
2
2
  import { Dimensions } from 'react-native';
3
- import {
3
+ import type {
4
4
  WidgetData,
5
5
  WidgetOptions,
6
6
  WidgetType,
@@ -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
  }
package/src/index.ts CHANGED
@@ -2,7 +2,11 @@ export { SoluCXWidget } from './SoluCXWidget';
2
2
  export { useWidgetState } from './hooks/useWidgetState';
3
3
  export { WidgetEventService } from './services/widgetEventService';
4
4
  export { StorageService } from './services/storage';
5
+ export { getDeviceInfo, type DeviceInfo } from './hooks/useDeviceInfoCollector';
6
+ export { requestWidgetUrl } from './services/widgetBootstrapService';
5
7
  export { buildWidgetURL } from './utils/urlUtils';
6
8
  export { ModalWidget } from './components/ModalWidget';
7
9
  export { getWidgetStyles, styles } from './styles/widgetStyles';
10
+ export { SDK_NAME, SDK_VERSION } from './constants/Constants';
11
+ export { getClientVersion } from './services/ClientVersionCollector';
8
12
  export * from './interfaces';
@@ -0,0 +1,14 @@
1
+ import { BlockReason } from "../services/widgetValidationService";
2
+
3
+ export interface WidgetCallbacks {
4
+ onPreOpen?: (userId: string) => void;
5
+ onOpened?: (userId: string) => void;
6
+ onBlock?: (blockReason: BlockReason | undefined) => void;
7
+ onClosed?: () => void;
8
+ onError?: (message: string) => void;
9
+ onPageChanged?: (page: string) => void;
10
+ onQuestionAnswered?: () => void;
11
+ onCompleted?: (userId: string) => void;
12
+ onPartialCompleted?: (userId: string) => void;
13
+ onResize?: (height: string) => void;
14
+ }
@@ -1,7 +1,6 @@
1
1
  export type SoluCXKey = string;
2
2
  export type WidgetType = "bottom" | "top" | "inline" | "modal";
3
3
  export type EventKey =
4
- | "FORM_OPENED"
5
4
  | "FORM_CLOSE"
6
5
  | "FORM_ERROR"
7
6
  | "FORM_PAGECHANGED"
@@ -21,4 +20,6 @@ export type { WidgetResponse } from './WidgetResponse';
21
20
  export type { WidgetData } from './WidgetData';
22
21
  export type { WidgetOptions } from './WidgetOptions';
23
22
  export type { WidgetSamplerLog } from './WidgetSamplerLog';
24
- export type { WidgetError } from './WidgetResponse';
23
+ export type { WidgetError } from './WidgetResponse';
24
+ export type { WidgetCallbacks } from './WidgetCallbacks';
25
+ export type { WidgetDisplayResult } from '../services/widgetValidationService';
@@ -0,0 +1,15 @@
1
+ export const getClientVersion = (): string => {
2
+ try {
3
+ const DeviceInfo = require("react-native-device-info");
4
+ if (DeviceInfo?.default?.getVersion) {
5
+ return DeviceInfo.default.getVersion();
6
+ }
7
+ if (DeviceInfo?.getVersion) {
8
+ return DeviceInfo.getVersion();
9
+ }
10
+ } catch (error) {
11
+ // react-native-device-info não disponível
12
+ }
13
+
14
+ return "unknown";
15
+ };
@@ -1,5 +1,5 @@
1
1
  import AsyncStorage from '@react-native-async-storage/async-storage';
2
- import { WidgetSamplerLog } from '../interfaces';
2
+ import type { WidgetSamplerLog } from '../interfaces';
3
3
  import { STORAGE_KEY } from '../constants/webViewConstants';
4
4
 
5
5
  export class StorageService {
@@ -18,4 +18,4 @@ export class StorageService {
18
18
  const json = await AsyncStorage.getItem(this.key);
19
19
  return json ? JSON.parse(json) as WidgetSamplerLog : {} as WidgetSamplerLog;
20
20
  }
21
- }
21
+ }