@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.
- package/package.json +31 -4
- package/src/SoluCXWidget.tsx +108 -53
- package/src/__mocks__/expo-modules-core-web.js +16 -0
- package/src/__mocks__/expo-modules-core.js +33 -0
- package/src/__tests__/ClientVersionCollector.test.ts +55 -0
- package/src/__tests__/CloseButton.test.tsx +47 -0
- package/src/__tests__/Constants.test.ts +17 -0
- package/src/__tests__/InlineWidget.rendering.test.tsx +81 -0
- package/src/__tests__/ModalWidget.rendering.test.tsx +157 -0
- package/src/__tests__/OverlayWidget.rendering.test.tsx +123 -0
- package/src/__tests__/SoluCXWidget.rendering.test.tsx +504 -0
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +352 -0
- package/src/__tests__/integration/webview-communication-simple.test.tsx +147 -0
- package/src/__tests__/integration/webview-communication.test.tsx +417 -0
- package/src/__tests__/useDeviceInfoCollector.test.ts +109 -0
- package/src/__tests__/useWidgetState.test.ts +76 -84
- package/src/__tests__/widgetBootstrapService.test.ts +182 -0
- package/src/components/ModalWidget.tsx +3 -5
- package/src/components/OverlayWidget.tsx +1 -1
- package/src/constants/Constants.ts +4 -0
- package/src/constants/webViewConstants.ts +1 -0
- package/src/hooks/useDeviceInfoCollector.ts +67 -0
- package/src/hooks/useWidgetState.ts +4 -4
- package/src/index.ts +4 -0
- package/src/interfaces/WidgetCallbacks.ts +14 -0
- package/src/interfaces/index.ts +3 -2
- package/src/services/ClientVersionCollector.ts +15 -0
- package/src/services/storage.ts +2 -2
- package/src/services/widgetBootstrapService.ts +67 -0
- package/src/services/widgetEventService.ts +14 -30
- package/src/services/widgetValidationService.ts +29 -13
- package/src/setupTests.js +43 -0
- package/src/styles/widgetStyles.ts +1 -1
- package/src/utils/urlUtils.ts +2 -2
|
@@ -1,46 +1,34 @@
|
|
|
1
1
|
import { WidgetEventService } from '../services/widgetEventService';
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
const result = await service.handleMessage('
|
|
64
|
+
it('should handle FORM_PAGECHANGED event correctly', async () => {
|
|
65
|
+
const result = await service.handleMessage('FORM_PAGECHANGED-page2', true);
|
|
96
66
|
|
|
97
|
-
expect(
|
|
67
|
+
expect(mockCallbacks.onPageChanged).toHaveBeenCalledWith('page2');
|
|
98
68
|
expect(result).toEqual({ status: 'success' });
|
|
99
69
|
});
|
|
100
70
|
|
|
101
|
-
it('should
|
|
102
|
-
const result = await service.handleMessage('
|
|
71
|
+
it('should handle QUESTION_ANSWERED event correctly', async () => {
|
|
72
|
+
const result = await service.handleMessage('QUESTION_ANSWERED', true);
|
|
103
73
|
|
|
104
|
-
expect(
|
|
74
|
+
expect(mockCallbacks.onQuestionAnswered).toHaveBeenCalledTimes(1);
|
|
75
|
+
expect(result).toEqual({ status: 'success' });
|
|
105
76
|
});
|
|
106
77
|
|
|
107
|
-
it('should handle
|
|
108
|
-
const result = await service.handleMessage('
|
|
78
|
+
it('should handle FORM_COMPLETED event correctly', async () => {
|
|
79
|
+
const result = await service.handleMessage('FORM_COMPLETED', true);
|
|
109
80
|
|
|
110
|
-
expect(
|
|
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
|
|
117
|
-
const
|
|
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(
|
|
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
|
|
127
|
-
const
|
|
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
|
-
|
|
95
|
+
expect(result).toEqual({ status: 'error', message: 'Unknown event' });
|
|
133
96
|
});
|
|
134
97
|
|
|
135
|
-
it('should
|
|
136
|
-
const result = await service.handleMessage('
|
|
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: '
|
|
101
|
+
expect(result).toEqual({ status: 'error', message: 'Unknown event' });
|
|
139
102
|
});
|
|
140
103
|
|
|
141
|
-
it('should
|
|
142
|
-
const result = await service.handleMessage('
|
|
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(
|
|
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,
|
|
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={
|
|
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';
|
|
@@ -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
|
|
14
|
-
widgetData
|
|
15
|
-
widgetData
|
|
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
|
+
}
|
package/src/interfaces/index.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/services/storage.ts
CHANGED
|
@@ -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
|
+
}
|