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

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.intern.md CHANGED
@@ -17,6 +17,7 @@ O SoluCX Widget oferece quatro modos de renderização flexíveis para integraç
17
17
 
18
18
  - [`SoluCXWidget.tsx`](src/SoluCXWidget.tsx) - Componente principal
19
19
  - [`useWidgetState.ts`](src/hooks/useWidgetState.ts) - Hook para gerenciamento de estado
20
+ - [`useWidgetHeight.ts`](src/hooks/useWidgetHeight.ts) - Hook para gerenciamento de altura (dinâmica/fixa)
20
21
  - [`WidgetEventService`](src/services/widgetEventService.ts) - Serviço de eventos
21
22
  - [`StorageService`](src/services/storage.ts) - Persistência local
22
23
 
@@ -30,7 +31,9 @@ src/
30
31
  │ ├── InlineWidget.tsx # Widget inline
31
32
  │ └── OverlayWidget.tsx # Widget overlay (top/bottom)
32
33
  ├── hooks/ # Hooks personalizados
33
- └── useWidgetState.ts # Estado do widget
34
+ ├── useWidgetState.ts # Estado do widget
35
+ │ ├── useWidgetHeight.ts # Gerenciamento de altura (dinâmica/fixa)
36
+ │ └── useHeightAnimation.ts # Animação de altura
34
37
  ├── services/ # Serviços
35
38
  │ ├── widgetEventService.ts # Gerenciamento de eventos
36
39
  │ └── storage.ts # Persistência de dados
@@ -72,20 +75,20 @@ import { SoluCXWidget } from 'solucx_widget';
72
75
  export default function MyComponent() {
73
76
  return (
74
77
  <SoluCXWidget
75
- soluCXKey="sua-chave-solucx"
76
- type="bottom" // 'top' | 'bottom' | 'modal' | 'inline'
78
+ soluCXKey='sua-chave-solucx'
79
+ type='bottom' // 'top' | 'bottom' | 'modal' | 'inline'
77
80
  data={{
78
- journey: "nome_da_jornada",
79
- name: "Nome do Cliente",
80
- email: "cliente@email.com",
81
- phone: "11999999999",
82
- store_id: "1",
83
- employee_id: "1",
81
+ journey: 'nome_da_jornada',
82
+ name: 'Nome do Cliente',
83
+ email: 'cliente@email.com',
84
+ phone: '11999999999',
85
+ store_id: '1',
86
+ employee_id: '1',
84
87
  amount: 100,
85
- param_REGIAO: "SUDESTE"
88
+ param_REGIAO: 'SUDESTE'
86
89
  }}
87
- options={{
88
- height: 400
90
+ options={{
91
+ height: 400
89
92
  }}
90
93
  />
91
94
  );
@@ -98,17 +101,17 @@ export default function MyComponent() {
98
101
 
99
102
  ```typescript
100
103
  interface SoluCXWidgetProps {
101
- soluCXKey: string; // Chave de autenticação SoluCX
102
- type: WidgetType; // Modo de renderização
103
- data: WidgetData; // Dados do cliente/transação
104
- options: WidgetOptions; // Configurações do widget
104
+ soluCXKey: string; // Chave de autenticação SoluCX
105
+ type: WidgetType; // Modo de renderização
106
+ data: WidgetData; // Dados do cliente/transação
107
+ options: WidgetOptions; // Configurações do widget
105
108
  }
106
109
  ```
107
110
 
108
111
  ### Tipos de Widget
109
112
 
110
113
  ```typescript
111
- type WidgetType = "bottom" | "top" | "inline" | "modal";
114
+ type WidgetType = 'bottom' | 'top' | 'inline' | 'modal';
112
115
  ```
113
116
 
114
117
  ### Dados do Widget
@@ -118,14 +121,14 @@ interface WidgetData {
118
121
  // Identificadores
119
122
  transaction_id?: string;
120
123
  customer_id?: string;
121
-
124
+
122
125
  // Dados do cliente
123
126
  name?: string;
124
127
  email?: string;
125
128
  phone?: string;
126
129
  birth_date?: string;
127
130
  document?: string;
128
-
131
+
129
132
  // Dados da transação
130
133
  store_id?: string;
131
134
  store_name?: string;
@@ -133,7 +136,7 @@ interface WidgetData {
133
136
  employee_name?: string;
134
137
  amount?: number;
135
138
  score?: number;
136
-
139
+
137
140
  // Parâmetros customizados
138
141
  journey?: string;
139
142
  [key: string]: string | number | undefined;
@@ -144,15 +147,44 @@ interface WidgetData {
144
147
 
145
148
  ```typescript
146
149
  interface WidgetOptions {
147
- height?: number; // Altura
148
- retry?: { // Configuração de retry
149
- attempts?: number; // Número de tentativas
150
- interval?: number; // Intervalo entre tentativas
150
+ height?: number; // Altura fixa em pontos (não pixels)
151
+ // Se não fornecido: altura dinâmica baseada em eventos de resize
152
+ // Se fornecido: altura fixa para todos os tipos de widget
153
+ retry?: {
154
+ // Configuração de retry
155
+ attempts?: number; // Número de tentativas
156
+ interval?: number; // Intervalo entre tentativas (ms)
151
157
  };
152
- waitDelayAfterRating?: number; // Delay após avaliação
158
+ waitDelayAfterRating?: number; // Delay após avaliação (ms)
153
159
  }
154
160
  ```
155
161
 
162
+ **⚙️ Gerenciamento de Altura:**
163
+
164
+ O widget possui dois modos de altura:
165
+
166
+ 1. **Altura Dinâmica (padrão)**: Quando `height` não é fornecido, o widget se ajusta automaticamente através de eventos `FORM_RESIZE` do conteúdo. Funciona para todos os tipos (`bottom`, `top`, `inline`, `modal`).
167
+
168
+ 2. **Altura Fixa**: Quando `height` é especificado, o valor é fixo e eventos de resize são ignorados. Funciona para todos os tipos de widget.
169
+
170
+ ```tsx
171
+ // Altura dinâmica - se adapta ao conteúdo
172
+ <SoluCXWidget
173
+ type="bottom"
174
+ options={{}} // height não especificado
175
+ {...props}
176
+ />
177
+
178
+ // Altura fixa de 400 pontos
179
+ <SoluCXWidget
180
+ type="modal"
181
+ options={{ height: 400 }} // altura fixa
182
+ {...props}
183
+ />
184
+ ```
185
+
186
+ **⚠️ Importante**: O valor de `height` é sempre em **pontos** (points), não pixels, seguindo o padrão do React e React Native. O sistema operacional converte automaticamente para pixels considerando a densidade da tela do dispositivo.
187
+
156
188
  ## 🔄 Sistema de Eventos
157
189
 
158
190
  O widget comunica através de mensagens WebView bidirecionais:
@@ -160,15 +192,15 @@ O widget comunica através de mensagens WebView bidirecionais:
160
192
  ### Eventos Suportados
161
193
 
162
194
  ```typescript
163
- type EventKey =
164
- | "FORM_OPENED" // Widget foi aberto
165
- | "FORM_CLOSE" // Usuário fechou o widget
166
- | "FORM_ERROR" // Erro no carregamento
167
- | "FORM_RESIZE" // Widget redimensionado
168
- | "FORM_PAGECHANGED" // Mudança de página
169
- | "QUESTION_ANSWERED" // Pergunta respondida
170
- | "FORM_COMPLETED" // Formulário concluído
171
- | "FORM_PARTIALCOMPLETED" // Completado parcialmente
195
+ type EventKey =
196
+ | 'FORM_OPENED' // Widget foi aberto
197
+ | 'FORM_CLOSE' // Usuário fechou o widget
198
+ | 'FORM_ERROR' // Erro no carregamento
199
+ | 'FORM_RESIZE' // Widget redimensionado
200
+ | 'FORM_PAGECHANGED' // Mudança de página
201
+ | 'QUESTION_ANSWERED' // Pergunta respondida
202
+ | 'FORM_COMPLETED' // Formulário concluído
203
+ | 'FORM_PARTIALCOMPLETED'; // Completado parcialmente
172
204
  ```
173
205
 
174
206
  ### Tratamento de Eventos
@@ -177,9 +209,9 @@ Os eventos são processados automaticamente pelo [`WidgetEventService`](src/serv
177
209
 
178
210
  ```typescript
179
211
  const eventService = new WidgetEventService(
180
- setIsWidgetVisible, // Função para controlar visibilidade
181
- resize, // Função para redimensionar
182
- open // Função para abrir widget
212
+ setIsWidgetVisible, // Função para controlar visibilidade
213
+ resize, // Função para redimensionar
214
+ open // Função para abrir widget
183
215
  );
184
216
  ```
185
217
 
@@ -195,10 +227,10 @@ O widget utiliza [`AsyncStorage`](@react-native-async-storage/async-storage) par
195
227
 
196
228
  ```typescript
197
229
  interface WidgetSamplerLog {
198
- attempts: number; // Número de tentativas
199
- lastAttempt: number; // Timestamp da última tentativa
200
- lastRating: number; // Timestamp da última avaliação
201
- lastParcial: number; // Timestamp do último parcial
230
+ attempts: number; // Número de tentativas
231
+ lastAttempt: number; // Timestamp da última tentativa
232
+ lastRating: number; // Timestamp da última avaliação
233
+ lastParcial: number; // Timestamp do último parcial
202
234
  }
203
235
  ```
204
236
 
@@ -211,10 +243,10 @@ Cada tipo de widget possui estilos específicos definidos em [`widgetStyles.ts`]
211
243
  ```typescript
212
244
  export const getWidgetStyles = (type: WidgetType) => {
213
245
  const styleMap = {
214
- 'bottom': { container: styles.wrapper, content: styles.bottom },
215
- 'top': { container: styles.wrapper, content: styles.top },
216
- 'inline': { container: styles.inlineWrapper, content: styles.inline },
217
- 'modal': { container: styles.wrapper, content: styles.inline }
246
+ bottom: { container: styles.wrapper, content: styles.bottom },
247
+ top: { container: styles.wrapper, content: styles.top },
248
+ inline: { container: styles.inlineWrapper, content: styles.inline },
249
+ modal: { container: styles.wrapper, content: styles.inline }
218
250
  };
219
251
 
220
252
  return styleMap[type] || styleMap.bottom;
@@ -237,11 +269,11 @@ O [`urlUtils.ts`](src/utils/urlUtils.ts) gerencia a construção de URLs da pesq
237
269
  export function buildWidgetURL(key: string, data: WidgetData): string {
238
270
  const params = new URLSearchParams(data as Record<string, string>);
239
271
  const baseURL = `${BASE_URL}/${key}/?mode=widget`;
240
-
272
+
241
273
  if (data.transaction_id) {
242
274
  return `${baseURL}&${params.toString()}`;
243
275
  }
244
-
276
+
245
277
  return `${baseURL}&transaction_id=&${params.toString()}`;
246
278
  }
247
279
  ```
@@ -336,21 +368,24 @@ export const WEB_VIEW_MESSAGE_LISTENER = `
336
368
  ### Adicionando Novos Tipos
337
369
 
338
370
  1. **Estenda o tipo WidgetType**:
371
+
339
372
  ```typescript
340
373
  // modules/solucx_widget/src/interfaces/index.ts
341
- export type WidgetType = "bottom" | "top" | "inline" | "modal" | "novo_tipo";
374
+ export type WidgetType = 'bottom' | 'top' | 'inline' | 'modal' | 'novo_tipo';
342
375
  ```
343
376
 
344
377
  2. **Adicione estilos correspondentes**:
378
+
345
379
  ```typescript
346
380
  // modules/solucx_widget/src/styles/widgetStyles.ts
347
381
  const styleMap = {
348
382
  // ... estilos existentes
349
- 'novo_tipo': { container: styles.novoWrapper, content: styles.novoContent }
383
+ novo_tipo: { container: styles.novoWrapper, content: styles.novoContent }
350
384
  };
351
385
  ```
352
386
 
353
387
  3. **Implemente lógica no componente principal**:
388
+
354
389
  ```typescript
355
390
  // modules/solucx_widget/src/SoluCXWidget.tsx
356
391
  if (type === 'novo_tipo') {
@@ -361,14 +396,14 @@ if (type === 'novo_tipo') {
361
396
  ### Adicionando Novos Eventos
362
397
 
363
398
  1. **Estenda EventKey**:
399
+
364
400
  ```typescript
365
401
  // modules/solucx_widget/src/interfaces/index.ts
366
- export type EventKey =
367
- | "FORM_OPENED"
368
- | "NOVO_EVENTO" // Novo evento
402
+ export type EventKey = 'FORM_OPENED' | 'NOVO_EVENTO'; // Novo evento
369
403
  ```
370
404
 
371
405
  2. **Implemente handler**:
406
+
372
407
  ```typescript
373
408
  // modules/solucx_widget/src/services/widgetEventService.ts
374
409
  private executeEvent(eventKey: EventKey, value: string): WidgetResponse {
@@ -380,6 +415,7 @@ private executeEvent(eventKey: EventKey, value: string): WidgetResponse {
380
415
  ```
381
416
 
382
417
  3. **Adicione testes**:
418
+
383
419
  ```typescript
384
420
  // modules/solucx_widget/src/__tests__/widgetEventService.test.ts
385
421
  it('should handle NOVO_EVENTO correctly', () => {
@@ -393,6 +429,7 @@ it('should handle NOVO_EVENTO correctly', () => {
393
429
  ### Problemas Comuns
394
430
 
395
431
  #### 1. **Widget não aparece**
432
+
396
433
  ```bash
397
434
  # Verificações:
398
435
  ✅ Chave SoluCX válida?
@@ -402,6 +439,7 @@ it('should handle NOVO_EVENTO correctly', () => {
402
439
  ```
403
440
 
404
441
  #### 2. **Eventos não funcionam**
442
+
405
443
  ```bash
406
444
  # Verificações:
407
445
  ✅ JavaScript listener injetado?
@@ -411,6 +449,7 @@ it('should handle NOVO_EVENTO correctly', () => {
411
449
  ```
412
450
 
413
451
  #### 3. **Layout quebrado**
452
+
414
453
  ```bash
415
454
  # Verificações:
416
455
  ✅ Dimensões adequadas para o dispositivo?
@@ -420,6 +459,7 @@ it('should handle NOVO_EVENTO correctly', () => {
420
459
  ```
421
460
 
422
461
  #### 4. **Storage não persiste**
462
+
423
463
  ```bash
424
464
  # Verificações:
425
465
  ✅ AsyncStorage instalado?
@@ -432,15 +472,15 @@ it('should handle NOVO_EVENTO correctly', () => {
432
472
 
433
473
  ```typescript
434
474
  // Habilitar logs detalhados
435
- console.log("Widget event received:", processedKey, value);
475
+ console.log('Widget event received:', processedKey, value);
436
476
 
437
477
  // Verificar dados persistidos
438
478
  const data = await storageService.read();
439
- console.log("Stored data:", data);
479
+ console.log('Stored data:', data);
440
480
 
441
481
  // Verificar URL construída
442
482
  const url = buildWidgetURL(soluCXKey, data);
443
- console.log("Widget URL:", url);
483
+ console.log('Widget URL:', url);
444
484
  ```
445
485
 
446
486
  ## 📚 Dependências
package/README.md CHANGED
@@ -45,17 +45,17 @@ import { SoluCXWidget } from '@solucx/react-native-solucx-widget';
45
45
  export default function MyScreen() {
46
46
  return (
47
47
  <SoluCXWidget
48
- soluCXKey="sua-chave-solucx"
49
- type="bottom"
48
+ soluCXKey='sua-chave-solucx'
49
+ type='bottom'
50
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
51
+ journey: 'checkout_flow',
52
+ name: 'João Silva',
53
+ email: 'joao@email.com',
54
+ store_id: 'loja_01',
55
+ amount: 150.0
56
56
  }}
57
57
  options={{
58
- height: 400
58
+ height: 400
59
59
  }}
60
60
  />
61
61
  );
@@ -65,43 +65,47 @@ export default function MyScreen() {
65
65
  ## 📱 Modos de Renderização
66
66
 
67
67
  ### Bottom (Padrão)
68
+
68
69
  Widget fixo na parte inferior da tela, ideal para feedback não intrusivo.
69
70
 
70
71
  ```tsx
71
- <SoluCXWidget type="bottom" {...props} />
72
+ <SoluCXWidget type='bottom' {...props} />
72
73
  ```
73
74
 
74
75
  ### Top
76
+
75
77
  Widget fixo no topo da tela, perfeito para notificações importantes.
76
78
 
77
79
  ```tsx
78
- <SoluCXWidget type="top" {...props} />
80
+ <SoluCXWidget type='top' {...props} />
79
81
  ```
80
82
 
81
83
  ### Modal
84
+
82
85
  Sobreposição centralizada que bloqueia interação com o fundo.
83
86
 
84
87
  ```tsx
85
- <SoluCXWidget type="modal" {...props} />
88
+ <SoluCXWidget type='modal' {...props} />
86
89
  ```
87
90
 
88
91
  ### Inline
92
+
89
93
  Integrado ao fluxo normal do layout, respeitando a posição no código.
90
94
 
91
95
  ```tsx
92
- <SoluCXWidget type="inline" {...props} />
96
+ <SoluCXWidget type='inline' {...props} />
93
97
  ```
94
98
 
95
99
  ## 🔧 API Completa
96
100
 
97
101
  ### Props
98
102
 
99
- | Propriedade | Tipo | Obrigatório | Descrição |
100
- |------------|------|-------------|-----------|
101
- | `soluCXKey` | `string` | ✅ | Chave de autenticação SoluCX |
102
- | `type` | `WidgetType` | ✅ | Modo de renderização |
103
- | `data` | `WidgetData` | ✅ | Dados do cliente/transação |
104
- | `options` | `WidgetOptions` | ✅ | Configurações do widget |
103
+ | Propriedade | Tipo | Obrigatório | Descrição |
104
+ | ----------- | --------------- | ----------- | ---------------------------- |
105
+ | `soluCXKey` | `string` | ✅ | Chave de autenticação SoluCX |
106
+ | `type` | `WidgetType` | ✅ | Modo de renderização |
107
+ | `data` | `WidgetData` | ✅ | Dados do cliente/transação |
108
+ | `options` | `WidgetOptions` | ✅ | Configurações do widget |
105
109
 
106
110
  ### WidgetData
107
111
 
@@ -110,14 +114,14 @@ interface WidgetData {
110
114
  // Identificadores
111
115
  transaction_id?: string;
112
116
  customer_id?: string;
113
-
117
+
114
118
  // Dados do cliente
115
119
  name?: string;
116
120
  email?: string;
117
121
  phone?: string;
118
- birth_date?: string; // Formato: YYYY-MM-DD
122
+ birth_date?: string; // Formato: YYYY-MM-DD
119
123
  document?: string;
120
-
124
+
121
125
  // Contexto da transação
122
126
  store_id?: string;
123
127
  store_name?: string;
@@ -125,8 +129,8 @@ interface WidgetData {
125
129
  employee_name?: string;
126
130
  amount?: number;
127
131
  score?: number;
128
- journey?: string; // Nome da jornada/fluxo
129
-
132
+ journey?: string; // Nome da jornada/fluxo
133
+
130
134
  // Parâmetros customizados (prefixo param_)
131
135
  param_REGIAO?: string;
132
136
  [key: string]: string | number | undefined;
@@ -137,19 +141,43 @@ interface WidgetData {
137
141
 
138
142
  ```typescript
139
143
  interface WidgetOptions {
140
- height?: number; // Altura
144
+ height?: number; // Altura fixa em pontos (points, não pixels)
145
+ // Se não fornecido, será dinâmica baseada em eventos de resize
146
+ // Se fornecido, será fixa independente do tipo de widget
141
147
  retry?: {
142
- attempts?: number; // Tentativas (padrão: 3)
143
- interval?: number; // Intervalo em ms (padrão: 1000)
148
+ attempts?: number; // Tentativas (padrão: 3)
149
+ interval?: number; // Intervalo em ms (padrão: 1000)
144
150
  };
145
151
  waitDelayAfterRating?: number; // Delay após avaliação
146
152
  }
147
153
  ```
148
154
 
155
+ **Comportamento da Altura (height):**
156
+
157
+ - **Altura Dinâmica (padrão)**: Quando `height` não é fornecido, o widget se ajusta automaticamente baseado em eventos de resize do conteúdo. Isso funciona para todos os tipos: `bottom`, `top`, `inline` e `modal`.
158
+
159
+ ```tsx
160
+ // Altura dinâmica - se adapta ao conteúdo
161
+ <SoluCXWidget
162
+ type='bottom'
163
+ options={{}} // ou options={{ retry: { attempts: 3 } }}
164
+ {...props}
165
+ />
166
+ ```
167
+
168
+ - **Altura Fixa**: Quando `height` é especificado, o valor é fixo e eventos de resize são ignorados. Funciona para todos os tipos de widget.
169
+
170
+ ```tsx
171
+ // Altura fixa de 400 pontos
172
+ <SoluCXWidget type='modal' options={{ height: 400 }} {...props} />
173
+ ```
174
+
175
+ **⚠️ Importante**: O valor de `height` é sempre em **pontos** (points), não pixels, seguindo o padrão do React e React Native. O sistema operacional converte automaticamente para pixels considerando a densidade da tela.
176
+
149
177
  ### WidgetType
150
178
 
151
179
  ```typescript
152
- type WidgetType = "bottom" | "top" | "inline" | "modal";
180
+ type WidgetType = 'bottom' | 'top' | 'inline' | 'modal';
153
181
  ```
154
182
 
155
183
  ## 🔄 Sistema de Eventos
@@ -247,8 +275,8 @@ const options = {
247
275
  ## 📚 Compatibilidade
248
276
 
249
277
  | Versão | React Native | Expo | iOS | Android |
250
- |--------|--------------|------|-----|---------|
251
- | 1.0.x | 0.70+ | 50+ | 11+ | API 21+ |
278
+ | ------ | ------------ | ---- | --- | ------- |
279
+ | 1.0.x | 0.70+ | 50+ | 11+ | API 21+ |
252
280
 
253
281
  ## 📄 Licença
254
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solucx/react-native-solucx-widget",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "The React Native SDK for Solucx Widget",
5
5
  "main": "src/index",
6
6
  "author": " <> ()",
@@ -14,10 +14,10 @@
14
14
  "README.md"
15
15
  ],
16
16
  "peerDependencies": {
17
+ "@react-native-async-storage/async-storage": "^2.2.0",
17
18
  "react": ">=18.0.0",
18
19
  "react-native": ">=0.72.0",
19
- "@react-native-async-storage/async-storage": "^2.2.0",
20
- "react-native-reanimated": "^3.17.4",
21
- "react-native-webview": "^13.16.0"
20
+ "react-native-webview": "^13.16.0",
21
+ "react-native-safe-area-context": "^5.6.1"
22
22
  }
23
23
  }
@@ -1,8 +1,9 @@
1
1
  import React, { useEffect } from 'react';
2
2
  import { View } from 'react-native';
3
- import { styles, getWidgetVisibility, useHeightAnimation } from '../styles/widgetStyles';
3
+ import { styles, getWidgetVisibility } from '../styles/widgetStyles';
4
4
  import { CloseButton } from './CloseButton';
5
- import Animated from 'react-native-reanimated';
5
+ import { Animated } from 'react-native';
6
+ import { useHeightAnimation } from '../hooks/useHeightAnimation';
6
7
 
7
8
  interface InlineWidgetProps {
8
9
  visible: boolean;
@@ -11,7 +12,12 @@ interface InlineWidgetProps {
11
12
  onClose?: () => void;
12
13
  }
13
14
 
14
- export const InlineWidget: React.FC<InlineWidgetProps> = ({ visible, height, children, onClose }) => {
15
+ export const InlineWidget: React.FC<InlineWidgetProps> = ({
16
+ visible,
17
+ height,
18
+ children,
19
+ onClose,
20
+ }) => {
15
21
  const { animatedHeightStyle, updateHeight } = useHeightAnimation(height);
16
22
 
17
23
  useEffect(() => {
@@ -20,12 +26,10 @@ export const InlineWidget: React.FC<InlineWidgetProps> = ({ visible, height, chi
20
26
 
21
27
  return (
22
28
  <View style={[styles.inlineWrapper, getWidgetVisibility(visible)]}>
23
- <Animated.View style={[styles.inline, animatedHeightStyle, getWidgetVisibility(visible)]}>
29
+ <Animated.View
30
+ style={[styles.inline, animatedHeightStyle, getWidgetVisibility(visible)]}>
24
31
  {children}
25
- <CloseButton
26
- visible={visible}
27
- onPress={onClose || (() => { })}
28
- />
32
+ <CloseButton visible={visible} onPress={onClose || (() => { })} />
29
33
  </Animated.View>
30
34
  </View>
31
35
  );
@@ -1,8 +1,8 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { Modal, SafeAreaView, View } from 'react-native';
3
- import { styles, getWidgetVisibility, useHeightAnimation } from '../styles/widgetStyles';
2
+ import { Modal, SafeAreaView, View, Animated } from 'react-native';
3
+ import { styles, getWidgetVisibility } from '../styles/widgetStyles';
4
4
  import { CloseButton } from './CloseButton';
5
- import Animated from 'react-native-reanimated';
5
+ import { useHeightAnimation } from '../hooks/useHeightAnimation';
6
6
 
7
7
  interface ModalWidgetProps {
8
8
  visible: boolean;
@@ -11,7 +11,12 @@ interface ModalWidgetProps {
11
11
  onClose?: () => void;
12
12
  }
13
13
 
14
- export const ModalWidget: React.FC<ModalWidgetProps> = ({ visible, height, children, onClose }) => {
14
+ export const ModalWidget: React.FC<ModalWidgetProps> = ({
15
+ visible,
16
+ height,
17
+ children,
18
+ onClose,
19
+ }) => {
15
20
  const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(true);
16
21
 
17
22
  const { animatedHeightStyle, updateHeight } = useHeightAnimation(height);
@@ -22,9 +27,20 @@ export const ModalWidget: React.FC<ModalWidgetProps> = ({ visible, height, child
22
27
 
23
28
  return (
24
29
  <SafeAreaView>
25
- <Modal transparent visible={isWidgetVisible} animationType="slide" hardwareAccelerated>
30
+ <Modal
31
+ transparent
32
+ visible={isWidgetVisible}
33
+ animationType="slide"
34
+ hardwareAccelerated
35
+ >
26
36
  <View style={[styles.modalOverlay, getWidgetVisibility(visible)]}>
27
- <Animated.View style={[styles.modalContent, getWidgetVisibility(visible), animatedHeightStyle]}>
37
+ <Animated.View
38
+ style={[
39
+ styles.modalContent,
40
+ getWidgetVisibility(visible),
41
+ animatedHeightStyle,
42
+ ]}
43
+ >
28
44
  {children}
29
45
  <CloseButton
30
46
  visible={visible}
@@ -1,10 +1,10 @@
1
1
  import React, { useState, useEffect } from 'react';
2
- import { View, ViewStyle } from 'react-native';
2
+ import { View, ViewStyle, Animated } from 'react-native';
3
3
  import { initialWindowMetrics } from 'react-native-safe-area-context';
4
- import { getWidgetStyles, getWidgetVisibility, useHeightAnimation } from '../styles/widgetStyles';
4
+ import { getWidgetStyles, getWidgetVisibility } from '../styles/widgetStyles';
5
5
  import { FIXED_Z_INDEX } from '../constants/webViewConstants';
6
6
  import { CloseButton } from './CloseButton';
7
- import Animated from 'react-native-reanimated';
7
+ import { useHeightAnimation } from '../hooks/useHeightAnimation';
8
8
 
9
9
  interface OverlayWidgetProps {
10
10
  visible: boolean;
@@ -23,7 +23,8 @@ export const OverlayWidget: React.FC<OverlayWidgetProps> = ({
23
23
  children,
24
24
  onClose,
25
25
  }) => {
26
- const insets = initialWindowMetrics?.insets ?? { top: 0, bottom: 0, left: 0, right: 0 };
26
+ const insets =
27
+ initialWindowMetrics?.insets ?? { top: 0, bottom: 0, left: 0, right: 0 };
27
28
  const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(true);
28
29
 
29
30
  const { animatedHeightStyle, updateHeight } = useHeightAnimation(height);
@@ -41,8 +42,8 @@ export const OverlayWidget: React.FC<OverlayWidgetProps> = ({
41
42
  width: '100%',
42
43
  height: '100%',
43
44
  zIndex: FIXED_Z_INDEX,
44
- pointerEvents: 'box-none'
45
- }
45
+ pointerEvents: 'box-none',
46
+ };
46
47
 
47
48
  const contentStyle = [
48
49
  getWidgetStyles(position).content,
@@ -55,14 +56,20 @@ export const OverlayWidget: React.FC<OverlayWidgetProps> = ({
55
56
  ...(position === 'bottom' && {
56
57
  bottom: insets.bottom,
57
58
  }),
58
- }
59
+ },
59
60
  ];
60
61
 
61
62
  return (
62
63
  <>
63
64
  {isWidgetVisible && (
64
65
  <View style={[containerStyle, getWidgetVisibility(visible)]}>
65
- <Animated.View style={[contentStyle, animatedHeightStyle, getWidgetVisibility(visible)]}>
66
+ <Animated.View
67
+ style={[
68
+ contentStyle,
69
+ animatedHeightStyle,
70
+ getWidgetVisibility(visible),
71
+ ]}
72
+ >
66
73
  {children}
67
74
  <CloseButton
68
75
  visible={visible}
@@ -1 +1,2 @@
1
1
  export { useWidgetState } from './useWidgetState';
2
+ export { useWidgetHeight } from './useWidgetHeight';
@@ -0,0 +1,22 @@
1
+ // hooks/useHeightAnimation.ts
2
+ import { useRef, useCallback } from 'react';
3
+ import { Animated } from 'react-native';
4
+
5
+ export function useHeightAnimation(initialHeight = 0, duration = 300) {
6
+ const height = useRef(new Animated.Value(initialHeight)).current;
7
+
8
+ const updateHeight = useCallback(
9
+ (toValue: number) => {
10
+ Animated.timing(height, {
11
+ toValue,
12
+ duration,
13
+ useNativeDriver: false,
14
+ }).start();
15
+ },
16
+ [height, duration],
17
+ );
18
+
19
+ const animatedHeightStyle = { height };
20
+
21
+ return { animatedHeightStyle, updateHeight, height };
22
+ }
@@ -0,0 +1,38 @@
1
+ import { useState, useCallback } from 'react';
2
+ import type { WidgetOptions } from '../interfaces';
3
+
4
+ interface UseWidgetHeightProps {
5
+ options?: WidgetOptions;
6
+ initialHeight?: number;
7
+ }
8
+
9
+ interface UseWidgetHeightReturn {
10
+ height: number;
11
+ isFixedHeight: boolean;
12
+ handleResize: (newHeight: number) => void;
13
+ }
14
+
15
+ export const useWidgetHeight = ({
16
+ options,
17
+ initialHeight = 300
18
+ }: UseWidgetHeightProps): UseWidgetHeightReturn => {
19
+ const [dynamicHeight, setDynamicHeight] = useState<number>(initialHeight);
20
+ const hasFixedHeight = typeof options?.height === 'number';
21
+
22
+ const height = hasFixedHeight ? options!.height! : dynamicHeight;
23
+
24
+ const handleResize = useCallback(
25
+ (newHeight: number) => {
26
+ if (!hasFixedHeight && newHeight > 0) {
27
+ setDynamicHeight(newHeight);
28
+ }
29
+ },
30
+ [hasFixedHeight]
31
+ );
32
+
33
+ return {
34
+ height,
35
+ isFixedHeight: hasFixedHeight,
36
+ handleResize
37
+ };
38
+ };
@@ -1,77 +1,101 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useState, useCallback, useMemo } from 'react';
2
2
  import { Dimensions } from 'react-native';
3
- import { WidgetData, WidgetOptions, WidgetType, WidgetSamplerLog } from '../interfaces';
3
+ import {
4
+ WidgetData,
5
+ WidgetOptions,
6
+ WidgetType,
7
+ WidgetSamplerLog
8
+ } from '../interfaces';
4
9
  import { StorageService } from '../services/storage';
5
10
 
6
11
  function getUserId(widgetData: WidgetData): string {
7
- return widgetData.customer_id ?? widgetData.document ?? widgetData.email ?? "default_user";
12
+ return (
13
+ widgetData.customer_id ??
14
+ widgetData.document ??
15
+ widgetData.email ??
16
+ 'default_user'
17
+ );
8
18
  }
9
19
 
10
- export const useWidgetState = (data: WidgetData, options?: WidgetOptions, type?: WidgetType) => {
11
- const [savedData, setSavedData] = useState<WidgetSamplerLog | null>(null);
12
- const [widgetHeight, setWidgetHeight] = useState<number>(0);
13
- const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(false);
20
+ export const useWidgetState = (
21
+ data: WidgetData,
22
+ options?: WidgetOptions,
23
+ type?: WidgetType
24
+ ) => {
25
+ const [savedData, setSavedData] = useState<WidgetSamplerLog | null>(null);
26
+ const [isWidgetVisible, setIsWidgetVisible] = useState<boolean>(false);
14
27
 
15
- const userId = getUserId(data);
16
- const storageService = new StorageService(userId);
17
- const screenHeight = Dimensions.get('screen').height;
18
- const height = options?.height ? Number(options.height) : undefined;
28
+ const userId = getUserId(data);
19
29
 
20
- const loadSavedData = useCallback(async () => {
21
- try {
22
- const jsonValue = await storageService.read();
23
- setSavedData(jsonValue);
24
- } catch (error) {
25
- console.error('Error loading storage data:', error);
26
- }
27
- }, [storageService]);
30
+ const storageService = useMemo(() => new StorageService(userId), [userId]);
28
31
 
29
- const saveData = useCallback(async (data: WidgetSamplerLog) => {
30
- try {
31
- await storageService.write(data);
32
- setSavedData(data);
33
- } catch (error) {
34
- console.error('Error saving storage data:', error);
35
- }
36
- }, [storageService]);
32
+ const screenHeight = Dimensions.get('screen').height;
33
+ const height = options?.height ? Number(options.height) : undefined;
37
34
 
38
- const open = useCallback(async () => {
39
- const userLogs = await storageService.read();
40
- userLogs.attempts++;
41
- userLogs.lastAttempt = Date.now();
42
- try {
43
- await storageService.write(userLogs);
44
- setSavedData(userLogs);
45
- } catch (error) {
46
- console.error('Error saving storage data:', error);
47
- }
48
- setIsWidgetVisible(true);
49
- }, []);
35
+ const [widgetHeight, setWidgetHeight] = useState<number>(height ?? 300);
50
36
 
51
- const close = useCallback(() => {
52
- setIsWidgetVisible(false);
53
- }, []);
37
+ const loadSavedData = useCallback(async () => {
38
+ try {
39
+ const jsonValue = await storageService.read();
40
+ setSavedData(jsonValue);
41
+ } catch (error) {
42
+ console.error('Error loading storage data:', error);
43
+ }
44
+ }, [storageService]);
54
45
 
55
- const resize = useCallback((value: string) => {
56
- const receivedHeight = Number(value);
57
- if (height && receivedHeight > height) {
58
- setWidgetHeight(height);
59
- return;
60
- }
46
+ const saveData = useCallback(
47
+ async (data: WidgetSamplerLog) => {
48
+ try {
49
+ await storageService.write(data);
50
+ setSavedData(data);
51
+ } catch (error) {
52
+ console.error('Error saving storage data:', error);
53
+ }
54
+ },
55
+ [storageService]
56
+ );
57
+
58
+ const open = useCallback(async () => {
59
+ const userLogs = await storageService.read();
60
+ userLogs.attempts = (userLogs.attempts || 0) + 1;
61
+ userLogs.lastAttempt = Date.now();
62
+ try {
63
+ await storageService.write(userLogs);
64
+ setSavedData(userLogs);
65
+ } catch (error) {
66
+ console.error('Error saving storage data:', error);
67
+ }
68
+ setIsWidgetVisible(true);
69
+ }, [storageService]);
70
+
71
+ const close = useCallback(() => {
72
+ setIsWidgetVisible(false);
73
+ }, []);
74
+
75
+ const resize = useCallback(
76
+ (value: string) => {
77
+ const receivedHeight = Number(value);
78
+
79
+ if (height !== undefined) {
80
+ setWidgetHeight(height);
81
+ } else if (receivedHeight > 0) {
61
82
  setWidgetHeight(receivedHeight);
62
- }, [screenHeight]);
83
+ }
84
+ },
85
+ [height]
86
+ );
63
87
 
64
- return {
65
- savedData,
66
- widgetHeight,
67
- isWidgetVisible,
68
- setIsWidgetVisible,
69
- loadSavedData,
70
- saveData,
71
- open,
72
- close,
73
- resize,
74
- userId,
75
- screenHeight
76
- };
88
+ return {
89
+ savedData,
90
+ widgetHeight,
91
+ isWidgetVisible,
92
+ setIsWidgetVisible,
93
+ loadSavedData,
94
+ saveData,
95
+ open,
96
+ close,
97
+ resize,
98
+ userId,
99
+ screenHeight
100
+ };
77
101
  };
@@ -1,111 +1,126 @@
1
- import { EventKey, SurveyEventKey, WidgetResponse, WidgetOptions } from '../interfaces';
2
- import { WidgetValidationService } from './widgetValidationService';
1
+ import {
2
+ EventKey,
3
+ SurveyEventKey,
4
+ WidgetResponse,
5
+ WidgetOptions,
6
+ } from "../interfaces";
7
+ import { WidgetValidationService } from "./widgetValidationService";
3
8
 
4
9
  export class WidgetEventService {
5
- private setIsWidgetVisible: (visible: boolean) => void;
6
- private resize: (value: string) => void;
7
- private open: () => void;
8
- private validationService: WidgetValidationService;
9
- private widgetOptions: WidgetOptions;
10
-
11
- constructor(
12
- setIsWidgetVisible: (visible: boolean) => void,
13
- resize: (value: string) => void,
14
- open: () => void,
15
- userId: string,
16
- widgetOptions: WidgetOptions
17
- ) {
18
- this.setIsWidgetVisible = setIsWidgetVisible;
19
- this.resize = resize;
20
- this.open = open;
21
- this.validationService = new WidgetValidationService(userId);
22
- this.widgetOptions = widgetOptions;
10
+ private setIsWidgetVisible: (visible: boolean) => void;
11
+ private resize: (value: string) => void;
12
+ private open: () => void;
13
+ private validationService: WidgetValidationService;
14
+ private widgetOptions: WidgetOptions;
15
+
16
+ constructor(
17
+ setIsWidgetVisible: (visible: boolean) => void,
18
+ resize: (value: string) => void,
19
+ open: () => void,
20
+ userId: string,
21
+ widgetOptions: WidgetOptions,
22
+ ) {
23
+ this.setIsWidgetVisible = setIsWidgetVisible;
24
+ this.resize = resize;
25
+ this.open = open;
26
+ this.validationService = new WidgetValidationService(userId);
27
+ this.widgetOptions = widgetOptions;
28
+ }
29
+
30
+ async handleMessage(
31
+ message: string,
32
+ isForm: boolean,
33
+ ): Promise<WidgetResponse> {
34
+ const [eventKey, value = ""] = message.split("-");
35
+ const processedKey = isForm
36
+ ? (eventKey as EventKey)
37
+ : this.adaptSurveyKeyToWidgetKey(eventKey as SurveyEventKey);
38
+
39
+ return await this.executeEvent(processedKey, value);
40
+ }
41
+
42
+ private async executeEvent(
43
+ eventKey: EventKey,
44
+ value: string,
45
+ ): Promise<WidgetResponse> {
46
+ const eventHandlers = {
47
+ FORM_OPENED: () => this.handleFormOpened(),
48
+ FORM_CLOSE: () => this.handleFormClose(),
49
+ FORM_ERROR: (value: string) => this.handleFormError(value),
50
+ FORM_PAGECHANGED: (value: string) => this.handlePageChanged(value),
51
+ QUESTION_ANSWERED: () => this.handleQuestionAnswered(),
52
+ FORM_COMPLETED: () => this.handleFormCompleted(),
53
+ FORM_PARTIALCOMPLETED: () => this.handlePartialCompleted(),
54
+ FORM_RESIZE: (value: string) => this.handleResize(value),
55
+ };
56
+
57
+ const handler = eventHandlers[eventKey];
58
+ return await (handler?.(value) || {
59
+ status: "error",
60
+ message: "Unknown event",
61
+ });
62
+ }
63
+
64
+ private async handleFormOpened(): Promise<WidgetResponse> {
65
+ const canDisplay = await this.validationService.shouldDisplayWidget(
66
+ this.widgetOptions,
67
+ );
68
+
69
+ if (!canDisplay) {
70
+ return { status: "error", message: "Widget not allowed" };
23
71
  }
24
72
 
25
- async handleMessage(message: string, isForm: boolean): Promise<WidgetResponse> {
26
- const [eventKey, value = ""] = message.split("-");
27
- const processedKey = isForm
28
- ? eventKey as EventKey
29
- : this.adaptSurveyKeyToWidgetKey(eventKey as SurveyEventKey);
30
-
31
- return await this.executeEvent(processedKey, value);
32
- }
33
-
34
- private async executeEvent(eventKey: EventKey, value: string): Promise<WidgetResponse> {
35
- const eventHandlers = {
36
- FORM_OPENED: () => this.handleFormOpened(),
37
- FORM_CLOSE: () => this.handleFormClose(),
38
- FORM_ERROR: (value: string) => this.handleFormError(value),
39
- FORM_PAGECHANGED: (value: string) => this.handlePageChanged(value),
40
- QUESTION_ANSWERED: () => this.handleQuestionAnswered(),
41
- FORM_COMPLETED: () => this.handleFormCompleted(),
42
- FORM_PARTIALCOMPLETED: () => this.handlePartialCompleted(),
43
- FORM_RESIZE: (value: string) => this.handleResize(value),
44
- };
45
-
46
- const handler = eventHandlers[eventKey];
47
- return await (handler?.(value) || { status: "error", message: "Unknown event" });
48
- }
49
-
50
- private async handleFormOpened(): Promise<WidgetResponse> {
51
- const canDisplay = await this.validationService.shouldDisplayWidget(this.widgetOptions);
52
-
53
- if (!canDisplay) {
54
- return { status: "error", message: "Widget not allowed" };
55
- }
56
-
57
- this.open();
58
- this.setIsWidgetVisible(true);
59
- return { status: "success" };
60
- }
61
-
62
- private handleFormClose(): WidgetResponse {
63
- this.setIsWidgetVisible(false);
64
- return { status: "success" };
65
- }
66
-
67
- private handleFormError(value: string): WidgetResponse {
68
- this.setIsWidgetVisible(false);
69
- return { status: "error", message: value };
70
- }
71
-
72
- private handlePageChanged(value: string): WidgetResponse {
73
- console.log("Page changed:", value);
74
- return { status: "success" };
75
- }
76
-
77
- private handleQuestionAnswered(): WidgetResponse {
78
- console.log("Question answered");
79
- return { status: "success" };
80
- }
81
-
82
- private handleFormCompleted(): WidgetResponse {
83
- // TODO: Implement completion logic
84
- return { status: "success" };
85
- }
86
-
87
- private handlePartialCompleted(): WidgetResponse {
88
- // TODO: Implement partial completion logic
89
- return { status: "success" };
90
- }
91
-
92
- private handleResize(value: string): WidgetResponse {
93
- this.setIsWidgetVisible(true);
94
- this.resize(value);
95
- return { status: "success" };
96
- }
97
-
98
- private adaptSurveyKeyToWidgetKey(key: SurveyEventKey): EventKey {
99
- const keyMapping = {
100
- closeSoluCXWidget: "FORM_CLOSE",
101
- dismissSoluCXWidget: "FORM_CLOSE",
102
- completeSoluCXWidget: "FORM_COMPLETED",
103
- partialSoluCXWidget: "FORM_PARTIALCOMPLETED",
104
- resizeSoluCXWidget: "FORM_RESIZE",
105
- openSoluCXWidget: "FORM_OPENED",
106
- errorSoluCXWidget: "FORM_ERROR",
107
- } as const;
108
-
109
- return keyMapping[key] as EventKey;
110
- }
73
+ this.open();
74
+ this.setIsWidgetVisible(true);
75
+ return { status: "success" };
76
+ }
77
+
78
+ private handleFormClose(): WidgetResponse {
79
+ this.setIsWidgetVisible(false);
80
+ return { status: "success" };
81
+ }
82
+
83
+ private handleFormError(value: string): WidgetResponse {
84
+ this.setIsWidgetVisible(false);
85
+ return { status: "error", message: value };
86
+ }
87
+
88
+ private handlePageChanged(value: string): WidgetResponse {
89
+ console.log("Page changed:", value);
90
+ return { status: "success" };
91
+ }
92
+
93
+ private handleQuestionAnswered(): WidgetResponse {
94
+ console.log("Question answered");
95
+ return { status: "success" };
96
+ }
97
+
98
+ private handleFormCompleted(): WidgetResponse {
99
+ // TODO: Implement completion logic
100
+ return { status: "success" };
101
+ }
102
+
103
+ private handlePartialCompleted(): WidgetResponse {
104
+ // TODO: Implement partial completion logic
105
+ return { status: "success" };
106
+ }
107
+
108
+ private handleResize(value: string): WidgetResponse {
109
+ this.resize(value);
110
+ return { status: "success" };
111
+ }
112
+
113
+ private adaptSurveyKeyToWidgetKey(key: SurveyEventKey): EventKey {
114
+ const keyMapping = {
115
+ closeSoluCXWidget: "FORM_CLOSE",
116
+ dismissSoluCXWidget: "FORM_CLOSE",
117
+ completeSoluCXWidget: "FORM_COMPLETED",
118
+ partialSoluCXWidget: "FORM_PARTIALCOMPLETED",
119
+ resizeSoluCXWidget: "FORM_RESIZE",
120
+ openSoluCXWidget: "FORM_OPENED",
121
+ errorSoluCXWidget: "FORM_ERROR",
122
+ } as const;
123
+
124
+ return keyMapping[key] as EventKey;
125
+ }
111
126
  }
@@ -1,30 +1,5 @@
1
1
  import { StyleSheet } from 'react-native';
2
2
  import { WidgetType } from '../interfaces';
3
- import {
4
- useSharedValue,
5
- withTiming,
6
- useAnimatedStyle,
7
- Easing,
8
- } from 'react-native-reanimated';
9
-
10
- export const useHeightAnimation = (height: number) => {
11
- const animatedHeight = useSharedValue(height);
12
-
13
- const animatedHeightStyle = useAnimatedStyle(() => {
14
- return {
15
- height: animatedHeight.value,
16
- };
17
- });
18
-
19
- const updateHeight = (newHeight: number) => {
20
- animatedHeight.value = withTiming(newHeight, {
21
- duration: 300,
22
- easing: Easing.bezier(0.25, 0.1, 0.25, 1),
23
- });
24
- };
25
-
26
- return { animatedHeightStyle, updateHeight };
27
- };
28
3
 
29
4
  export const styles = StyleSheet.create({
30
5
  wrapper: {