# @solucx/react-native-solucx-widget

[![React Native](https://img.shields.io/badge/React%20Native-0.70+-blue.svg)](https://reactnative.dev/)
[![Expo](https://img.shields.io/badge/Expo-50+-lightgrey.svg)](https://expo.dev/)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)

Widget React Native para coleta de feedback e pesquisas de satisfacao, desenvolvido pela SoluCX.

---

## Instalacao

```bash
npm install @solucx/react-native-solucx-widget
```

---

## Inicio Rapido

Voce pode integrar o widget de duas formas:

- **Por funcao**: monta um `SoluCXWidgetHost` no root e dispara a exibicao com `SoluCXWidget.create(...).show()`.
- **Por componente**: renderiza `SoluCXWidget` diretamente no JSX, no ponto exato em que o widget deve aparecer.

### 1. Uso por funcao: adicione o `SoluCXWidgetHost` no root do app

O `SoluCXWidgetHost` deve ser montado **uma unica vez** no componente raiz da aplicacao. Ele e responsavel por renderizar o widget quando solicitado via `SoluCXWidget.create(...).show()`.

```tsx
// App.tsx
import { SoluCXWidgetHost } from '@solucx/react-native-solucx-widget';

export default function App() {
  return (
    <>
      <NavigationContainer>
        <Stack.Navigator>{/* suas telas */}</Stack.Navigator>
      </NavigationContainer>

      {/* Obrigatorio: monte uma vez no root */}
      <SoluCXWidgetHost />
    </>
  );
}
```

### 2. Uso por funcao: dispare o widget em qualquer tela

Use `SoluCXWidget.create()` para construir e exibir o widget. **As opcoes de supressao (dias de espera) sao opcionais** — se nao forem passadas, o widget as busca automaticamente do painel de configuracao da jornada (configurado remotamente).

```tsx
// CheckoutScreen.tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

function onCheckoutComplete() {
  SoluCXWidget
    .create('SUA_CHAVE_SOLUCX')
    .setType('modal')
    .setData({
      journey: 'pos_venda',
      customer_id: 'user_123',
      email: 'cliente@email.com',
      name: 'Joao Silva',
    })
    .show();
}
```

Pronto! O widget vai buscar as configuracoes de supressao remotamente e decidir se deve exibir ou nao.

### 3. Uso por componente: renderize o widget diretamente no JSX

Quando voce quiser controlar a exibicao pelo proprio componente React, use `SoluCXWidget`. Nesse modo, nao e necessario montar `SoluCXWidgetHost`.

```tsx
// CheckoutScreen.tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

export function CheckoutScreen() {
  return (
    <SoluCXWidget
      soluCXKey="SUA_CHAVE_SOLUCX"
      type="inline"
      data={{
        journey: 'pos_venda',
        customer_id: 'user_123',
        email: 'cliente@email.com',
        name: 'Joao Silva',
      }}
      callbacks={{
        onOpened: (userId) => {
          console.log('Widget exibido para:', userId);
        },
      }}
    />
  );
}
```

---

## Exemplos Completos

### Exemplo 1: Uso minimo (opcoes configuradas remotamente)

Este e o caso de uso mais comum. As regras de supressao (quantos dias esperar apos cada evento) sao configuradas no **painel de configuracao da jornada** e buscadas automaticamente pelo widget. Voce nao precisa passar nenhuma opcao.

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

// Disparar apos uma compra
function afterPurchase() {
  SoluCXWidget
    .create('SUA_CHAVE_SOLUCX')
    .setType('bottom')
    .setData({
      journey: 'pos_venda',
      customer_id: 'user_123',
      email: 'cliente@email.com',
      name: 'Joao Silva',
      store_id: 'loja_01',
    })
    .show();
  // As opcoes de supressao serao buscadas automaticamente da API
  // com base na configuracao da jornada "pos_venda"
}
```

### Exemplo 2: Com callbacks para monitorar eventos

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

function showSurvey() {
  SoluCXWidget
    .create('SUA_CHAVE_SOLUCX')
    .setType('modal')
    .setData({
      journey: 'atendimento',
      customer_id: 'user_456',
      email: 'maria@email.com',
      name: 'Maria Santos',
      employee_id: 'atendente_01',
      employee_name: 'Carlos',
    })
    .setCallbacks({
      onPreOpen: (userId) => {
        console.log('Preparando widget para:', userId);
      },
      onOpened: (userId) => {
        console.log('Widget exibido para:', userId);
        analytics.track('survey_shown', { userId });
      },
      onBlocked: (reason) => {
        // Widget nao foi exibido por causa de uma regra de supressao
        console.log('Widget bloqueado:', reason);
        // Ex: "BLOCKED_BY_WIDGET_DISMISS_INTERVAL"
      },
      onClosed: () => {
        console.log('Usuario fechou o widget');
      },
      onCompleted: (userId) => {
        console.log('Pesquisa respondida por:', userId);
        analytics.track('survey_completed', { userId });
      },
      onPartialCompleted: (userId) => {
        console.log('Pesquisa parcialmente respondida por:', userId);
      },
      onError: (message) => {
        console.error('Erro no widget:', message);
      },
    })
    .show();
}
```

### Exemplo 3: Com opcoes locais (sobrescrevendo configuracao remota)

Se voce precisar sobrescrever as configuracoes da jornada para um caso especifico, passe as opcoes diretamente. Quando `setOptions()` e chamado, o widget **nao busca configuracoes da API** e usa os valores fornecidos.

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

function showUrgentSurvey() {
  SoluCXWidget
    .create('SUA_CHAVE_SOLUCX')
    .setType('modal')
    .setData({
      journey: 'nps_trimestral',
      customer_id: 'user_789',
      email: 'pedro@email.com',
    })
    .setOptions({
      height: 600,
      maxAttemptsAfterDismiss: 5,        // Máximo de 5 tentativas para essa jornada
      waitDaysAfterWidgetDismiss: 90,    // Esperar 90 dias após o usuário fechar
      waitDaysAfterWidgetSubmit: 30,     // Esperar 30 dias após resposta completa
      waitDaysAfterWidgetDisplay: 7,     // Esperar 7 dias após qualquer exibição
    })
    .setCallbacks({
      onOpened: (userId) => console.log('Abriu:', userId),
      onCompleted: (userId) => console.log('Respondeu:', userId),
    })
    .show();
}
```

### Exemplo 4: Diferentes tipos de widget

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

const data = {
  journey: 'pos_venda',
  customer_id: 'user_123',
  email: 'cliente@email.com',
};

// Bottom: barra fixa na parte inferior (padrao)
SoluCXWidget.create('KEY').setType('bottom').setData(data).show();

// Top: barra fixa no topo
SoluCXWidget.create('KEY').setType('top').setData(data).show();

// Modal: sobreposicao centralizada que bloqueia o fundo
SoluCXWidget.create('KEY').setType('modal').setData(data).show();

// Inline: integrado ao fluxo do layout (respeita a posicao no JSX)
SoluCXWidget.create('KEY').setType('inline').setData(data).show();
```

### Exemplo 5: Fechar o widget programaticamente

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

// Fechar o widget a qualquer momento
function handleTimeout() {
  SoluCXWidget.dismiss();
}
```

### Exemplo 6: Consultar logs de supressao

Os metodos de log sao **metodos de instancia** — usam os dados do builder (`instanceKey` do `create()`, `journey` e `userId` do `setData()`) para identificar automaticamente qual registro acessar. Nao e necessario passar esses parametros novamente.

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

// Cria a instancia com os dados que identificam o registro
const widget = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda', customer_id: 'user_123' });

// Consultar os logs de supressao
const logs = await widget.getWidgetLogs();
console.log(logs);
// {
//   attempts: 3,                       // quantas vezes o widget foi exibido nesta experiencia
//   answeredTransactionIds: ['txn-1'], // transacoes que ja responderam e nao devem reabrir o widget
//   lastExperienceId: 'pos_venda',     // ultima jornada associada ao contador
//   lastDisplayAttempt: 1741000000000, // ultima tentativa de exibicao bloqueada
//   lastFirstAccess: 1740000000000,    // primeira vez que o widget foi exibido
//   lastDisplay: 1741000000000,        // ultima exibicao do widget
//   lastDismiss: 1741000000000,        // ultima vez que o usuario fechou
//   lastSubmit: 0,                     // ultimo envio completo
//   lastPartialSubmit: 0,              // ultimo envio parcial
// }
```

### Exemplo 7: Sobrescrever datas de eventos (debug/teste)

Use `overrideTimestamp` para simular cenarios de teste sem esperar os dias reais.

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

const widget = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda', customer_id: 'user_123' });

// Informar primeiro acesso do usuário 
await widget.overrideTimestamp('lastFirstAccess', new Date('2026-01-01T00:00:00Z'));

// Ou usar timestamp em milissegundos diretamente
await widget.overrideTimestamp('lastFirstAccess', 1700000000000);

// Resetar um campo para "nunca aconteceu" (valor 0)
await widget.overrideTimestamp('lastSubmit', 0);

// Depois de sobrescrever, pode exibir o widget normalmente
widget.show();
```

**Campos disponiveis para `overrideTimestamp`:**

| Campo | Descricao |
|---|---|
| `lastDisplayAttempt` | Ultima tentativa de exibicao (widget foi bloqueado) |
| `lastFirstAccess` | Primeira vez que o widget foi exibido para o usuario |
| `lastDisplay` | Ultima exibicao do widget |
| `lastDismiss` | Ultima vez que o usuario fechou o widget |
| `lastSubmit` | Ultimo envio completo da pesquisa |
| `lastPartialSubmit` | Ultimo envio parcial da pesquisa |
| `answeredTransactionIds` | Lista de `transaction_id` que ja responderam e devem ser bloqueados localmente |

### Exemplo 8: Limpar logs de supressao

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

const widget = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda', customer_id: 'user_123' });

// Limpar TODOS os logs (reseta contadores e timestamps)
await widget.clearWidgetLogs();

// Apos limpar, o widget vai se comportar como se nunca tivesse sido exibido
widget.show();
```

### Exemplo 9: Forcar exibicao do widget (ignorar bloqueio)

Se o widget esta sendo bloqueado por uma regra de supressao e voce quer forca-lo a exibir (por exemplo, para testes), sobrescreva a data do evento que esta bloqueando:

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

const widget = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda', customer_id: 'user_123' });

// Verificar qual evento esta bloqueando
const logs = await widget.getWidgetLogs();
console.log('Ultimo dismiss:', new Date(logs.lastDismiss));
console.log('Tentativas:', logs.attempts);

// Resetar o dismiss para desbloquear
await widget.overrideTimestamp('lastDismiss', 0);

// Agora o widget vai exibir (se nao houver outra regra bloqueando)
widget.show();
```

### Exemplo 10: Isolamento por jornada — dois widgets independentes

Cada combinacao de `instanceKey + journey + userId` tem seus proprios logs. Widgets de jornadas diferentes nao interferem entre si.

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

// Widget da jornada "pos_venda"
const widgetPosVenda = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda', customer_id: 'user_123' });

// Widget da jornada "atendimento"
const widgetAtendimento = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'atendimento', customer_id: 'user_123' });

// Os logs sao independentes:
const logsPosVenda = await widgetPosVenda.getWidgetLogs();
const logsAtendimento = await widgetAtendimento.getWidgetLogs();
// logsPosVenda pode ter 5 tentativas enquanto logsAtendimento tem 0

// Limpar os logs de uma jornada NAO afeta a outra
await widgetPosVenda.clearWidgetLogs();
// Apenas os logs de pos_venda foram resetados
```

### Exemplo 11: Widget sem userId (dispositivo anonimo)

Quando nao ha `customer_id`, `email` ou `cpf`, os logs sao compartilhados por todos os usuarios daquela jornada naquele dispositivo.

```tsx
import { SoluCXWidget } from '@solucx/react-native-solucx-widget';

// Sem userId — logs sao por instanceKey:journey (compartilhado no dispositivo)
const widget = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda' });

const logs = await widget.getWidgetLogs();
// Chave de armazenamento: "SUA_CHAVE_SOLUCX:pos_venda"

// Com userId — logs sao por instanceKey:journey:userId (isolado por usuario)
const widgetComUser = SoluCXWidget
  .create('SUA_CHAVE_SOLUCX')
  .setData({ journey: 'pos_venda', customer_id: 'user_123' });

const logsComUser = await widgetComUser.getWidgetLogs();
// Chave de armazenamento: "SUA_CHAVE_SOLUCX:pos_venda:user_123"
```

---

## Opcoes de Supressao (waitDays)

> **Importante:** Voce **nao precisa passar essas opcoes no codigo**. Elas podem (e devem) ser configuradas remotamente no painel de configuracao da jornada. O widget busca automaticamente as configuracoes da API quando `setOptions()` nao e chamado.

As opcoes controlam quantos dias o widget deve esperar apos cada tipo de evento antes de exibir novamente:

| Opcao | Descricao |
|---|---|
| `enabled` | Habilita/desabilita o widget. Quando `false`, bloqueia imediatamente sem validar outras regras |
| `samplingPercentage` | Porcentagem de usuarios que verao o widget (0-100). Valor 100 ou undefined = sem amostragem |
| `maxAttemptsAfterDismiss` | Numero maximo de tentativas de exibicao por experiencia |
| `waitDaysAfterWidgetDisplayAttempt` | Dias apos uma tentativa de exibicao bloqueada |
| `waitDaysAfterWidgetFirstAccess` | Dias apos o primeiro acesso do usuario a jornada |
| `waitDaysAfterWidgetDisplay` | Dias apos qualquer exibicao do widget |
| `waitDaysAfterWidgetDismiss` | Dias apos o usuario fechar o widget |
| `waitDaysAfterWidgetSubmit` | Dias apos o usuario responder a pesquisa |
| `waitDaysAfterWidgetPartialSubmit` | Dias apos o usuario responder parcialmente |

**Regras:**
- `enabled: false` bloqueia o widget **imediatamente** com `BLOCKED_BY_DISABLED`, sem verificar nenhuma outra regra
- `enabled: true` ou `undefined` = widget habilitado (comportamento padrao)
- `samplingPercentage`: sorteia se o usuario vera o widget. Ex: `50` = 50% de chance. Valor `100` ou `undefined` = todos veem. Valor `0` = ninguem ve
- Valor `0` ou `undefined` nos campos de dias = sem restricao (o campo nao bloqueia)
- Se **qualquer** regra bloquear, o widget nao exibe e chama `onBlocked(reason)`
- Quando bloqueado, o `reason` indica qual regra bloqueou (ex: `"BLOCKED_BY_WIDGET_DISMISS_INTERVAL"`)
- `maxAttemptsAfterDismiss` conta quantas vezes o widget foi exibido para aquela experiencia (jornada). Se o ID da experiencia (`journey` ou `form_id`) mudar, o contador de tentativas e resetado automaticamente
- Quando o usuario engaja na pesquisa (`QUESTION_ANSWERED`, envio completo ou envio parcial), o contador de tentativas tambem e resetado para nao bloquear exibicoes futuras por tentativas antigas
- Quando existe `transaction_id` e essa transacao ja foi concluida ou parcialmente concluida antes, o widget bloqueia localmente e nao faz chamadas de opcoes nem de preflight

---

## API Completa

### `SoluCXWidget` (classe principal)

#### Metodos de construcao (builder pattern)

| Metodo | Descricao |
|---|---|
| `SoluCXWidget.create(soluCXKey)` | Cria uma nova instancia do widget |
| `.setType(type)` | Define o tipo: `'bottom'`, `'top'`, `'modal'`, `'inline'` |
| `.setData(data)` | Define os dados do usuario/transacao |
| `.setOptions(options)` | Define opcoes locais (**opcional** - se nao chamar, busca da API) |
| `.setCallbacks(callbacks)` | Define callbacks de eventos |
| `.show()` | Exibe o widget |

#### Metodos estaticos

| Metodo | Descricao |
|---|---|
| `SoluCXWidget.dismiss()` | Fecha o widget programaticamente |

#### Metodos de instancia (log management)

Usam os dados do builder (instanceKey, journey, userId) para acessar os logs da combinacao correta.

| Metodo | Descricao |
|---|---|
| `.getWidgetLogs()` | Retorna os logs de supressao |
| `.overrideTimestamp(field, date)` | Sobrescreve uma data de evento (debug/teste) |
| `.clearWidgetLogs()` | Limpa todos os logs |

> **Isolamento de armazenamento:** Os logs sao isolados por `instanceKey:journey:userId`. Um app com 2 widgets de jornadas diferentes tera dados de supressao independentes. O `userId` e opcional — quando presente, os logs sao por usuario.

### `SoluCXWidgetHost` (componente React)

Componente que deve ser montado **uma vez** no root do app quando a integracao for feita por funcao com `SoluCXWidget.create(...).show()`. Nao e necessario ao usar `SoluCXWidgetView` diretamente.

```tsx
import { SoluCXWidgetHost } from '@solucx/react-native-solucx-widget';

// No root do app
<SoluCXWidgetHost />
```

### `SoluCXWidgetView` (componente React)

Componente para uso direto no JSX. Pode ser usado no lugar do fluxo por funcao quando fizer mais sentido controlar a renderizacao pelo componente.

```tsx
import { SoluCXWidgetView } from '@solucx/react-native-solucx-widget';

<SoluCXWidgetView
  soluCXKey="SUA_CHAVE_SOLUCX"
  type="inline"
  data={{ journey: 'pos_venda', customer_id: 'user_123' }}
/>
```

**Props:**

| Prop | Descricao |
|---|---|
| `soluCXKey` | Chave de instancia do widget |
| `type` | Tipo do widget: `'bottom'`, `'top'`, `'modal'`, `'inline'` |
| `data` | Dados do usuario/transacao |
| `options` | Opcoes locais de configuracao e supressao |
| `callbacks` | Callbacks de ciclo de vida e eventos |

### WidgetData

```typescript
interface WidgetData {
  journey?: string;          // Nome da jornada (obrigatorio na pratica)
  email?: string;            // Email do usuario
  name?: string;             // Nome do usuario
  cpf?: string;              // CPF
  document?: string;         // Documento
  phone?: string;            // Telefone
  phone2?: string;           // Telefone secundario
  birth_date?: string;       // Data de nascimento (YYYY-MM-DD)
  transaction_id?: string;   // ID da transacao
  customer_id?: string;      // ID do cliente
  store_id?: string;         // ID da loja
  store_name?: string;       // Nome da loja
  employee_id?: string;      // ID do funcionario
  employee_name?: string;    // Nome do funcionario
  amount?: number;           // Valor da transacao
  score?: number;            // Score
  [key: `param_${string}`]: string | number | undefined;  // Parametros customizados
}
```

### WidgetCallbacks

```typescript
interface WidgetCallbacks {
  onPreOpen?: (userId: string) => void;        // Antes de abrir
  onOpened?: (userId: string) => void;         // Widget aberto
  onBlocked?: (reason: BlockReason) => void;   // Widget bloqueado por regra de supressao
  onClosed?: () => void;                       // Usuario fechou
  onCompleted?: (userId: string) => void;      // Pesquisa respondida
  onPartialCompleted?: (userId: string) => void; // Pesquisa parcialmente respondida
  onError?: (message: string) => void;         // Erro no widget
  onPageChanged?: (page: string) => void;      // Pagina mudou
  onQuestionAnswered?: () => void;             // Pergunta respondida
  onResize?: (height: string) => void;         // Widget redimensionou
}
```

### WidgetOptions

```typescript
interface WidgetOptions {
  enabled?: boolean;  // Habilita/desabilita o widget (default: true). Quando false, bloqueia sem validar.
  samplingPercentage?: number;  // Porcentagem de usuarios que verao o widget (0-100). Default: 100 (todos).
  height?: number;  // Altura fixa em pontos (se nao informado, altura dinamica)
  maxAttemptsAfterDismiss?: number;  // Maximo de tentativas por experiencia (0 ou undefined = sem limite)
  // Regras de supressao (opcionais - configuradas remotamente pelo painel da jornada):
  waitDaysAfterWidgetDisplayAttempt?: number;
  waitDaysAfterWidgetFirstAccess?: number;
  waitDaysAfterWidgetDisplay?: number;
  waitDaysAfterWidgetDismiss?: number;
  waitDaysAfterWidgetSubmit?: number;
  waitDaysAfterWidgetPartialSubmit?: number;
}
```

### BlockReason

Valores possiveis retornados no callback `onBlocked`:

| Valor | Significado |
|---|---|
| `BLOCKED_BY_DISABLED` | Widget desabilitado (enabled: false) |
| `BLOCKED_BY_SAMPLING` | Usuario nao foi selecionado pela amostragem (samplingPercentage) |
| `BLOCKED_BY_MAX_ATTEMPTS` | Numero maximo de tentativas atingido para esta experiencia |
| `BLOCKED_BY_WIDGET_DISPLAY_ATTEMPT_INTERVAL` | Dentro do intervalo apos tentativa de exibicao |
| `BLOCKED_BY_WIDGET_FIRST_ACCESS_INTERVAL` | Dentro do intervalo apos primeira visualizacao |
| `BLOCKED_BY_WIDGET_DISPLAY_INTERVAL` | Dentro do intervalo apos exibicao |
| `BLOCKED_BY_WIDGET_DISMISS_INTERVAL` | Dentro do intervalo apos fechamento |
| `BLOCKED_BY_WIDGET_SUBMIT_INTERVAL` | Dentro do intervalo apos envio completo |
| `BLOCKED_BY_WIDGET_PARTIAL_SUBMIT_INTERVAL` | Dentro do intervalo apos envio parcial |

---

## Fluxo Interno

```
App chama SoluCXWidget.create('KEY').setData({...}).show()
  |
  v
SoluCXWidgetHost recebe a configuracao
  |
  v
Widget monta e executa bootstrap():
  |
  +-- Se setOptions() FOI chamado --> usa opcoes locais
  +-- Se setOptions() NAO foi chamado --> busca opcoes da API (painel da jornada)
  |
  +-- shouldDisplayWidget() verifica enabled + maxAttemptsAfterDismiss + 6 regras de supressao
  +-- (validacao LOCAL, antes de chamar a API, para reduzir carga no servidor):
  |     +-- Se transaction_id ja respondido --> onBlocked('BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED'), widget NAO exibe
  |     +-- Se enabled === false --> onBlocked('BLOCKED_BY_DISABLED'), widget NAO exibe
  |     +-- Sorteia baseado em samplingPercentage --> se nao selecionado, onBlocked('BLOCKED_BY_SAMPLING')
  |     +-- Se experiencia mudou --> reseta contador de tentativas
  |     +-- Se tentativas >= maxAttemptsAfterDismiss --> onBlocked('BLOCKED_BY_MAX_ATTEMPTS')
  |     +-- Para cada regra: timestamp do evento + dias de espera > agora?
  |     +-- Se QUALQUER regra bloqueia --> onBlocked(reason), widget NAO exibe
  |     +-- Se NENHUMA bloqueia --> continua
  |
  +-- Busca URL do formulario na API (preflight)
  |
  +-- Se EXIBE:
  |     +-- Incrementa contador de tentativas
  |     +-- Salva lastDisplay (e lastFirstAccess na primeira vez)
  |     +-- Renderiza WebView com o formulario
  |     +-- onOpened(userId)
  |
  +-- Eventos do usuario:
        +-- Fecha widget --> salva lastDismiss, onClosed()
      +-- Responde pergunta --> zera contador de tentativas, onQuestionAnswered()
    +-- Responde pesquisa --> zera contador de tentativas, salva transaction_id respondido, salva lastSubmit, onCompleted(userId)
    +-- Resposta parcial --> zera contador de tentativas, salva transaction_id respondido, salva lastPartialSubmit, onPartialCompleted(userId)
```

---

## Compatibilidade

| Versao | React Native | Expo | iOS | Android |
|---|---|---|---|---|
| 1.0.x | 0.70+ | 50+ | 11+ | API 21+ |

## Licenca

Proprietario - SoluCX. Uso restrito a clientes licenciados.

---
