@solucx/react-native-solucx-widget 0.1.0

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/README.md ADDED
@@ -0,0 +1,260 @@
1
+ # @solucx/react-native-solucx-widget
2
+
3
+ [![npm version](https://badge.fury.io/js/@solucx%2Freact-native-solucx-widget.svg)](https://badge.fury.io/js/@solucx%2Freact-native-solucx-widget)
4
+ [![React Native](https://img.shields.io/badge/React%20Native-0.70+-blue.svg)](https://reactnative.dev/)
5
+ [![Expo](https://img.shields.io/badge/Expo-50+-lightgrey.svg)](https://expo.dev/)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
+ [![iOS](https://img.shields.io/badge/iOS-11+-lightgrey.svg)](https://developer.apple.com/ios/)
8
+ [![Android](https://img.shields.io/badge/Android-API%2021+-green.svg)](https://developer.android.com/)
9
+ [![License](https://img.shields.io/badge/License-Proprietary-red.svg)](LICENSE)
10
+
11
+ Um widget React Native modular para coleta de feedback e pesquisas de satisfação, desenvolvido pela SoluCX seguindo princípios de Clean Code e arquitetura escalável.
12
+
13
+ ## 🛠️ Tecnologias
14
+
15
+ ![React Native](https://img.shields.io/badge/React_Native-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)
16
+ ![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)
17
+ ![WebView](https://img.shields.io/badge/WebView-4285F4?style=for-the-badge&logo=googlechrome&logoColor=white)
18
+ ![AsyncStorage](https://img.shields.io/badge/AsyncStorage-FF6B6B?style=for-the-badge&logo=react&logoColor=white)
19
+ ![Jest](https://img.shields.io/badge/Jest-C21325?style=for-the-badge&logo=jest&logoColor=white)
20
+
21
+ ## 📋 Visão Geral
22
+
23
+ O SoluCX Widget permite integrar pesquisas de satisfação diretamente em aplicações React Native/Expo de forma simples e flexível. Desenvolvido para empresas que precisam coletar feedback em tempo real através de diferentes modalidades de apresentação.
24
+
25
+ ### 🎯 Principais Características
26
+
27
+ - **4 Modos de Renderização**: Bottom, Top, Modal e Inline
28
+ - **Persistência Automática**: Controle inteligente de frequência
29
+ - **Comunicação WebView**: Integração transparente com plataforma SoluCX
30
+ - **TypeScript**: Totalmente tipado para melhor experiência de desenvolvimento
31
+ - **Performático**: Carregamento otimizado e cache local
32
+
33
+ ## 🚀 Instalação
34
+
35
+ ```bash
36
+ npm install @solucx/react-native-solucx-widget
37
+ ```
38
+
39
+ ## 🎛️ Uso Básico
40
+
41
+ ```tsx
42
+ import React from 'react';
43
+ import { SoluCXWidget } from '@solucx/react-native-solucx-widget';
44
+
45
+ export default function MyScreen() {
46
+ return (
47
+ <SoluCXWidget
48
+ soluCXKey="sua-chave-solucx"
49
+ type="bottom"
50
+ data={{
51
+ journey: "checkout_flow",
52
+ name: "João Silva",
53
+ email: "joao@email.com",
54
+ store_id: "loja_01",
55
+ amount: 150.00
56
+ }}
57
+ options={{
58
+ width: 380,
59
+ height: 400
60
+ }}
61
+ />
62
+ );
63
+ }
64
+ ```
65
+
66
+ ## 📱 Modos de Renderização
67
+
68
+ ### Bottom (Padrão)
69
+ Widget fixo na parte inferior da tela, ideal para feedback não intrusivo.
70
+
71
+ ```tsx
72
+ <SoluCXWidget type="bottom" {...props} />
73
+ ```
74
+
75
+ ### Top
76
+ Widget fixo no topo da tela, perfeito para notificações importantes.
77
+
78
+ ```tsx
79
+ <SoluCXWidget type="top" {...props} />
80
+ ```
81
+
82
+ ### Modal
83
+ Sobreposição centralizada que bloqueia interação com o fundo.
84
+
85
+ ```tsx
86
+ <SoluCXWidget type="modal" {...props} />
87
+ ```
88
+
89
+ ### Inline
90
+ Integrado ao fluxo normal do layout, respeitando a posição no código.
91
+
92
+ ```tsx
93
+ <SoluCXWidget type="inline" {...props} />
94
+ ```
95
+
96
+ ## 🔧 API Completa
97
+
98
+ ### Props
99
+
100
+ | Propriedade | Tipo | Obrigatório | Descrição |
101
+ |------------|------|-------------|-----------|
102
+ | `soluCXKey` | `string` | ✅ | Chave de autenticação SoluCX |
103
+ | `type` | `WidgetType` | ✅ | Modo de renderização |
104
+ | `data` | `WidgetData` | ✅ | Dados do cliente/transação |
105
+ | `options` | `WidgetOptions` | ✅ | Configurações do widget |
106
+
107
+ ### WidgetData
108
+
109
+ ```typescript
110
+ interface WidgetData {
111
+ // Identificadores
112
+ transaction_id?: string;
113
+ customer_id?: string;
114
+
115
+ // Dados do cliente
116
+ name?: string;
117
+ email?: string;
118
+ phone?: string;
119
+ birth_date?: string; // Formato: YYYY-MM-DD
120
+ document?: string;
121
+
122
+ // Contexto da transação
123
+ store_id?: string;
124
+ store_name?: string;
125
+ employee_id?: string;
126
+ employee_name?: string;
127
+ amount?: number;
128
+ score?: number;
129
+ journey?: string; // Nome da jornada/fluxo
130
+
131
+ // Parâmetros customizados (prefixo param_)
132
+ param_REGIAO?: string;
133
+ [key: string]: string | number | undefined;
134
+ }
135
+ ```
136
+
137
+ ### WidgetOptions
138
+
139
+ ```typescript
140
+ interface WidgetOptions {
141
+ width?: number; // Largura (padrão: 380)
142
+ height?: number; // Altura (padrão: 400)
143
+ retry?: {
144
+ attempts?: number; // Tentativas (padrão: 3)
145
+ interval?: number; // Intervalo em ms (padrão: 1000)
146
+ };
147
+ waitDelayAfterRating?: number; // Delay após avaliação
148
+ }
149
+ ```
150
+
151
+ ### WidgetType
152
+
153
+ ```typescript
154
+ type WidgetType = "bottom" | "top" | "inline" | "modal";
155
+ ```
156
+
157
+ ## 🔄 Sistema de Eventos
158
+
159
+ O widget processa automaticamente os seguintes eventos da pesquisa:
160
+
161
+ - `FORM_OPENED` - Widget foi aberto
162
+ - `FORM_CLOSE` - Usuário fechou o widget
163
+ - `FORM_COMPLETED` - Pesquisa concluída
164
+ - `FORM_PARTIALCOMPLETED` - Completada parcialmente
165
+ - `FORM_RESIZE` - Widget redimensionado
166
+ - `FORM_ERROR` - Erro no carregamento
167
+
168
+ ## 💾 Persistência Inteligente
169
+
170
+ O widget controla automaticamente:
171
+
172
+ - **Histórico de tentativas**: Evita spam de widgets
173
+ - **Última avaliação**: Data da última interação
174
+ - **Controle de frequência**: Respeita configurações de exibição
175
+ - **Armazenamento local**: Dados persistem entre sessões
176
+
177
+ ## ⚙️ Múltiplos Widgets
178
+
179
+ ```tsx
180
+ const widgets = ['bottom', 'top'] as WidgetType[];
181
+
182
+ return (
183
+ <>
184
+ {widgets.map((type) => (
185
+ <SoluCXWidget
186
+ key={type}
187
+ soluCXKey={key}
188
+ type={type}
189
+ data={data}
190
+ options={options}
191
+ />
192
+ ))}
193
+ </>
194
+ );
195
+ ```
196
+
197
+ ## 🚨 Considerações Importantes
198
+
199
+ ### Posicionamento
200
+
201
+ ⚠️ **Comportamento Crítico**: A posição no JSX **não determina** onde widgets `top`, `bottom` e `modal` aparecem:
202
+
203
+ ```tsx
204
+ // ❌ Widget "bottom" sempre aparece embaixo, independente da posição
205
+ <Text>Conteúdo antes</Text>
206
+ <SoluCXWidget type="bottom" {...props} />
207
+ <Text>Conteúdo depois</Text>
208
+
209
+ // ✅ Apenas "inline" respeita a posição no código
210
+ <Text>Conteúdo antes</Text>
211
+ <SoluCXWidget type="inline" {...props} />
212
+ <Text>Conteúdo depois</Text>
213
+ ```
214
+
215
+ ## 🔍 Troubleshooting
216
+
217
+ ### Widget não aparece
218
+
219
+ ```typescript
220
+ // Verificações essenciais:
221
+ // ✅ Chave SoluCX válida?
222
+ // ✅ Conectividade com internet?
223
+ // ✅ Logs do WebView no console?
224
+ // ✅ Dados obrigatórios preenchidos?
225
+ ```
226
+
227
+ ### Eventos não funcionam
228
+
229
+ ```typescript
230
+ // Debug de comunicação:
231
+ const handleMessage = (message: string) => {
232
+ console.log('Widget event:', message);
233
+ };
234
+
235
+ // Verificar se JavaScript foi injetado corretamente
236
+ ```
237
+
238
+ ### Layout quebrado
239
+
240
+ ```typescript
241
+ // Ajustar dimensões para o dispositivo:
242
+ const { width, height } = Dimensions.get('window');
243
+
244
+ const options = {
245
+ width: width * 0.9,
246
+ height: Math.min(height * 0.6, 400)
247
+ };
248
+ ```
249
+
250
+ ## 📚 Compatibilidade
251
+
252
+ | Versão | React Native | Expo | iOS | Android |
253
+ |--------|--------------|------|-----|---------|
254
+ | 1.0.x | 0.70+ | 50+ | 11+ | API 21+ |
255
+
256
+ ## 📄 Licença
257
+
258
+ Este pacote é proprietário da SoluCX. O uso é restrito a clientes licenciados da plataforma SoluCX.
259
+
260
+ ---
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@solucx/react-native-solucx-widget",
3
+ "version": "0.1.0",
4
+ "description": "The React Native SDK for Solucx Widget",
5
+ "main": "src/index",
6
+ "author": " <> ()",
7
+ "homepage": "#readme",
8
+ "files": [
9
+ "lib/",
10
+ "src/",
11
+ "README.md"
12
+ ]
13
+ }
@@ -0,0 +1,117 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { Dimensions } from 'react-native';
3
+ import { WebView } from 'react-native-webview';
4
+
5
+ import { SoluCXKey, WidgetData, WidgetOptions, WidgetType } from './interfaces';
6
+ import { useWidgetState } from './hooks/useWidgetState';
7
+ import { WidgetEventService } from './services/widgetEventService';
8
+ import { buildWidgetURL } from './utils/urlUtils';
9
+ import { WEB_VIEW_MESSAGE_LISTENER } from './constants/webViewConstants';
10
+ import { ModalWidget } from './components/ModalWidget';
11
+ import { InlineWidget } from './components/InlineWidget';
12
+ import { OverlayWidget } from './components/OverlayWidget';
13
+
14
+ interface SoluCXWidgetProps {
15
+ soluCXKey: SoluCXKey;
16
+ type: WidgetType;
17
+ data: WidgetData;
18
+ options: WidgetOptions;
19
+ }
20
+
21
+ export const SoluCXWidget: React.FC<SoluCXWidgetProps> = ({
22
+ soluCXKey,
23
+ type,
24
+ data,
25
+ options
26
+ }) => {
27
+ const webviewRef = useRef<WebView>(null);
28
+ const { width } = Dimensions.get('window');
29
+
30
+ const {
31
+ widgetHeight,
32
+ isWidgetVisible,
33
+ setIsWidgetVisible,
34
+ loadSavedData,
35
+ resize,
36
+ open,
37
+ close,
38
+ userId,
39
+ } = useWidgetState(data, type);
40
+
41
+ const eventService = new WidgetEventService(setIsWidgetVisible, resize, open, userId, options);
42
+
43
+ const uri = buildWidgetURL(soluCXKey, data);
44
+ const isForm = Boolean(data.form_id);
45
+ const height = options.height ? Number(options.height) : undefined;
46
+
47
+ useEffect(() => {
48
+ loadSavedData();
49
+ }, [loadSavedData]);
50
+
51
+ const handleWebViewMessage = useCallback(async (message: string) => {
52
+ if (message && message.length > 0) {
53
+ try {
54
+ await eventService.handleMessage(message, isForm);
55
+ } catch (error) {
56
+ console.error('Error handling widget message:', error);
57
+ }
58
+ }
59
+ }, [eventService, isForm]);
60
+
61
+ const handleWebViewLoad = useCallback(() => {
62
+ webviewRef.current?.injectJavaScript(WEB_VIEW_MESSAGE_LISTENER);
63
+ }, []);
64
+
65
+ const handleClose = useCallback(() => {
66
+ if (type === 'inline' || type === 'modal') {
67
+ close();
68
+ }
69
+ setIsWidgetVisible(false);
70
+ }, [setIsWidgetVisible]);
71
+
72
+
73
+ const webViewStyle = [
74
+ { height: widgetHeight },
75
+ { width }
76
+ ];
77
+
78
+ if (type === 'modal') {
79
+ return (
80
+ <ModalWidget visible={isWidgetVisible} onClose={handleClose}>
81
+ <WebView
82
+ ref={webviewRef}
83
+ style={webViewStyle}
84
+ source={{ uri }}
85
+ onLoadEnd={handleWebViewLoad}
86
+ onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
87
+ />
88
+ </ModalWidget>
89
+ );
90
+ }
91
+
92
+ if (type === 'inline') {
93
+ return (
94
+ <InlineWidget visible={isWidgetVisible} onClose={handleClose}>
95
+ <WebView
96
+ ref={webviewRef}
97
+ style={webViewStyle}
98
+ source={{ uri }}
99
+ onLoadEnd={handleWebViewLoad}
100
+ onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
101
+ />
102
+ </InlineWidget>
103
+ );
104
+ }
105
+
106
+ return (
107
+ <OverlayWidget visible={isWidgetVisible} width={width} height={widgetHeight} position={type} onClose={handleClose}>
108
+ <WebView
109
+ ref={webviewRef}
110
+ style={webViewStyle}
111
+ source={{ uri }}
112
+ onLoadEnd={handleWebViewLoad}
113
+ onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
114
+ />
115
+ </OverlayWidget>
116
+ );
117
+ };
@@ -0,0 +1,56 @@
1
+ import { buildWidgetURL } from '../utils/urlUtils';
2
+
3
+ describe('urlUtils', () => {
4
+ describe('buildWidgetURL', () => {
5
+ const mockKey = 'test-widget-key';
6
+
7
+ it('should build URL with transaction_id when provided', () => {
8
+ const data = {
9
+ customer_id: 'customer123',
10
+ form_id: 'form456',
11
+ transaction_id: 'trans789'
12
+ };
13
+
14
+ const result = buildWidgetURL(mockKey, data);
15
+
16
+ expect(result).toContain('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget');
17
+ expect(result).toContain('customer_id=customer123');
18
+ expect(result).toContain('form_id=form456');
19
+ expect(result).toContain('transaction_id=trans789');
20
+ });
21
+
22
+ it('should build URL with empty transaction_id when not provided', () => {
23
+ const data = {
24
+ customer_id: 'customer123',
25
+ form_id: 'form456'
26
+ };
27
+
28
+ const result = buildWidgetURL(mockKey, data);
29
+
30
+ expect(result).toContain('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget');
31
+ expect(result).toContain('transaction_id=&');
32
+ expect(result).toContain('customer_id=customer123');
33
+ expect(result).toContain('form_id=form456');
34
+ });
35
+
36
+ it('should handle empty data object', () => {
37
+ const data = {};
38
+
39
+ const result = buildWidgetURL(mockKey, data);
40
+
41
+ expect(result).toBe('https://survey-link.solucx.com.br/link/test-widget-key/?mode=widget&transaction_id=&');
42
+ });
43
+
44
+ it('should encode URL parameters correctly', () => {
45
+ const data = {
46
+ email: 'test@example.com',
47
+ name: 'John Doe'
48
+ };
49
+
50
+ const result = buildWidgetURL(mockKey, data);
51
+
52
+ expect(result).toContain('email=test%40example.com');
53
+ expect(result).toContain('name=John+Doe');
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,190 @@
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
+ }));
11
+
12
+ describe('WidgetEventService', () => {
13
+ let mockSetIsWidgetVisible: jest.Mock;
14
+ let mockResize: jest.Mock;
15
+ let service: WidgetEventService;
16
+ let open: jest.Mock;
17
+ let mockUserId: string;
18
+ let mockWidgetOptions: WidgetOptions;
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+
23
+ mockSetIsWidgetVisible = jest.fn();
24
+ mockResize = jest.fn();
25
+ open = jest.fn();
26
+ mockUserId = 'test-user-123';
27
+ mockWidgetOptions = {
28
+ width: 380,
29
+ height: 400,
30
+ retry: {
31
+ attempts: 5,
32
+ interval: 1
33
+ },
34
+ waitDelayAfterRating: 60
35
+ };
36
+
37
+ mockWidgetValidationService.shouldDisplayWidget.mockResolvedValue(true);
38
+
39
+ service = new WidgetEventService(
40
+ mockSetIsWidgetVisible,
41
+ mockResize,
42
+ open,
43
+ mockUserId,
44
+ mockWidgetOptions
45
+ );
46
+ });
47
+
48
+ afterEach(() => {
49
+ jest.clearAllMocks();
50
+ jest.restoreAllMocks();
51
+ });
52
+
53
+ it('should handle FORM_OPENED event correctly', async () => {
54
+ const result = await service.handleMessage('FORM_OPENED', true);
55
+
56
+ expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
57
+ expect(open).toHaveBeenCalled();
58
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
59
+ expect(result).toEqual({ status: 'success' });
60
+ });
61
+
62
+ it('should prevent widget from opening when validation fails', async () => {
63
+ mockWidgetValidationService.shouldDisplayWidget.mockResolvedValueOnce(await Promise.resolve(false));
64
+
65
+ const result = await service.handleMessage('FORM_OPENED', true);
66
+
67
+ expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
68
+ expect(open).not.toHaveBeenCalled();
69
+ expect(mockSetIsWidgetVisible).not.toHaveBeenCalled();
70
+ expect(result).toEqual({ status: 'error', message: 'Widget not allowed' });
71
+ });
72
+
73
+ it('should handle FORM_CLOSE event correctly', async () => {
74
+ const result = await service.handleMessage('FORM_CLOSE', true);
75
+
76
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
77
+ expect(result).toEqual({ status: 'success' });
78
+ });
79
+
80
+ it('should handle FORM_RESIZE event correctly', async () => {
81
+ const result = await service.handleMessage('FORM_RESIZE-350', true);
82
+
83
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
84
+ expect(mockResize).toHaveBeenCalledWith('350');
85
+ expect(result).toEqual({ status: 'success' });
86
+ });
87
+
88
+ it('should handle FORM_ERROR event correctly', async () => {
89
+ const result = await service.handleMessage('FORM_ERROR-Something went wrong', true);
90
+
91
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
92
+ expect(result).toEqual({ status: 'error', message: 'Something went wrong' });
93
+ });
94
+
95
+ it('should adapt survey keys to widget keys correctly for non-form widgets', async () => {
96
+ const result = await service.handleMessage('closeSoluCXWidget', false);
97
+
98
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
99
+ expect(result).toEqual({ status: 'success' });
100
+ });
101
+
102
+ it('should return error for unknown events', async () => {
103
+ const result = await service.handleMessage('UNKNOWN_EVENT', true);
104
+
105
+ expect(result).toEqual({ status: 'error', message: 'Unknown event' });
106
+ });
107
+
108
+ it('should handle messages with no value correctly', async () => {
109
+ const result = await service.handleMessage('FORM_OPENED', true);
110
+
111
+ expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
112
+ expect(open).toHaveBeenCalled();
113
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
114
+ expect(result).toEqual({ status: 'success' });
115
+ });
116
+
117
+ it('should handle FORM_PAGECHANGED event correctly', async () => {
118
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
119
+ const result = await service.handleMessage('FORM_PAGECHANGED-page2', true);
120
+
121
+ expect(consoleSpy).toHaveBeenCalledWith("Page changed:", "page2");
122
+ expect(result).toEqual({ status: 'success' });
123
+
124
+ consoleSpy.mockRestore();
125
+ });
126
+
127
+ it('should handle QUESTION_ANSWERED event correctly', async () => {
128
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
129
+ const result = await service.handleMessage('QUESTION_ANSWERED', true);
130
+ expect(consoleSpy).toHaveBeenCalledWith("Question answered");
131
+ expect(result).toEqual({ status: 'success' });
132
+
133
+ consoleSpy.mockRestore();
134
+ });
135
+
136
+ it('should handle FORM_COMPLETED event correctly', async () => {
137
+ const result = await service.handleMessage('FORM_COMPLETED', true);
138
+
139
+ expect(result).toEqual({ status: 'success' });
140
+ });
141
+
142
+ it('should handle FORM_PARTIALCOMPLETED event correctly', async () => {
143
+ const result = await service.handleMessage('FORM_PARTIALCOMPLETED', true);
144
+
145
+ expect(result).toEqual({ status: 'success' });
146
+ });
147
+
148
+ it('should adapt completeSoluCXWidget to FORM_COMPLETED', async () => {
149
+ const result = await service.handleMessage('completeSoluCXWidget', false);
150
+
151
+ expect(result).toEqual({ status: 'success' });
152
+ });
153
+
154
+ it('should adapt partialSoluCXWidget to FORM_PARTIALCOMPLETED', async () => {
155
+ const result = await service.handleMessage('partialSoluCXWidget', false);
156
+
157
+ expect(result).toEqual({ status: 'success' });
158
+ });
159
+
160
+ it('should adapt dismissSoluCXWidget to FORM_CLOSE', async () => {
161
+ const result = await service.handleMessage('dismissSoluCXWidget', false);
162
+
163
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
164
+ expect(result).toEqual({ status: 'success' });
165
+ });
166
+
167
+ it('should adapt openSoluCXWidget to FORM_OPENED', async () => {
168
+ const result = await service.handleMessage('openSoluCXWidget', false);
169
+
170
+ expect(mockWidgetValidationService.shouldDisplayWidget).toHaveBeenCalledWith(mockWidgetOptions);
171
+ expect(open).toHaveBeenCalled();
172
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
173
+ expect(result).toEqual({ status: 'success' });
174
+ });
175
+
176
+ it('should adapt errorSoluCXWidget to FORM_ERROR', async () => {
177
+ const result = await service.handleMessage('errorSoluCXWidget-Network error', false);
178
+
179
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
180
+ expect(result).toEqual({ status: 'error', message: 'Network error' });
181
+ });
182
+
183
+ it('should adapt resizeSoluCXWidget to FORM_RESIZE', async () => {
184
+ const result = await service.handleMessage('resizeSoluCXWidget-400', false);
185
+
186
+ expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(true);
187
+ expect(mockResize).toHaveBeenCalledWith('400');
188
+ expect(result).toEqual({ status: 'success' });
189
+ });
190
+ });
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, Text, StyleSheet } from 'react-native';
3
+
4
+ interface CloseButtonProps {
5
+ onPress: () => void;
6
+ visible?: boolean;
7
+ }
8
+
9
+ export const CloseButton: React.FC<CloseButtonProps> = ({ onPress, visible = true }) => {
10
+ if (!visible) return null;
11
+
12
+ return (
13
+ <TouchableOpacity style={styles.closeButton} onPress={onPress}>
14
+ <Text style={styles.closeButtonText}>✕</Text>
15
+ </TouchableOpacity>
16
+ );
17
+ };
18
+
19
+ const styles = StyleSheet.create({
20
+ closeButton: {
21
+ position: 'absolute',
22
+ top: 10,
23
+ right: 10,
24
+ width: 30,
25
+ height: 30,
26
+ borderRadius: 15,
27
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
28
+ justifyContent: 'center',
29
+ alignItems: 'center',
30
+ zIndex: 10001,
31
+ },
32
+ closeButtonText: {
33
+ color: 'white',
34
+ fontSize: 16,
35
+ fontWeight: 'bold',
36
+ },
37
+ });
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import { styles, getWidgetVisibility } from '../styles/widgetStyles';
4
+ import { CloseButton } from './CloseButton';
5
+
6
+ interface InlineWidgetProps {
7
+ visible: boolean;
8
+ children?: React.ReactNode;
9
+ onClose?: () => void;
10
+ }
11
+
12
+ export const InlineWidget: React.FC<InlineWidgetProps> = ({ visible, children, onClose }) => {
13
+
14
+ return (
15
+ <View style={[styles.wrapper, getWidgetVisibility(visible)]}>
16
+ <View style={[styles.inline, getWidgetVisibility(visible)]}>
17
+ {children}
18
+ <CloseButton
19
+ visible={visible}
20
+ onPress={onClose || (() => { })}
21
+ />
22
+ </View>
23
+ </View>
24
+ );
25
+ };