Appearance
Resiliencia: fallback de erro e teclado
Regra base: nenhuma tela pode ficar branca, presa ou mentindo. Todo caminho de falha precisa terminar em uma das tres saidas abaixo.
| Tipo de falha | Saida obrigatoria | Peca |
|---|---|---|
| Erro de render (crash JS) | Tela de erro com reiniciar / tentar de novo / ver detalhe | AppErrorBoundary |
| Falha de fetch de lista | Estado de erro com botao de recarregar | PaginatedListStateView |
| Falha de fetch de secao | Estado de erro com botao de recarregar | ScreenState (error) |
| Falha de acao do usuario | Toast com a mensagem do backend | useToast |
1. Error boundary
components/ui/AppErrorBoundary captura erro de render e mostra AppErrorFallback (lottie consecutive-error, titulo, descricao, Reiniciar o app, Tentar carregar de novo, Ver detalhes do erro). O detalhe abre AppErrorDetailsModal com app/dispositivo, rota, mensagem, stack e component stack, com botao de copiar o relatorio inteiro.
Ele esta montado em dois niveis no app/_layout.tsx:
tsx
<AppErrorBoundary scope="root"> // pega crash de provider/boot
...
<AppErrorBoundary scope="navigation"> // pega crash de tela, providers seguem vivosO boundary interno e o que importa no dia a dia: o crash de uma tela nao derruba sessao, tema nem navegacao, e o Tentar carregar de novo remonta so a subarvore.
AppErrorFallback traz o proprio SafeAreaProvider e nao usa useTheme/useAuth, porque o boundary root roda acima desses providers.
Toda entrada e registrada com logError('ui', ...), que ja alimenta Sentry e o useErrorTracking (que por sua vez dispara o NavigationErrorRecoveryModal apos falhas repetidas).
Quando adicionar um boundary novo: em qualquer subarvore que renderize conteudo de terceiros ou pesado (WebView de documento, preview, editor). scope e so um rotulo de log.
2. Falha de fetch nunca vira estado vazio
O interceptor do config/api.ts nao mostra toast, so rejeita. Um .catch(logCaughtError(...)) e portanto silencioso para o usuario: a tela cai no estado vazio e a pessoa acha que nao existe dado, quando na verdade a rede caiu.
hooks/use-paginated-list.ts expoe error e failed (Boolean(error) && pages.length === 0). Listas usam PaginatedListStateView, que decide entre skeleton, erro e vazio:
tsx
ListEmptyComponent={
<PaginatedListStateView
loading={showListLoading}
failed={failed}
emptyIcon={FileText}
emptyTitle={t('...')}
onRetry={handleRefresh}
/>
}Secoes de tela usam ScreenState, que agora aceita error, errorTitle, errorDescription e onRetry, e prioriza erro sobre vazio. O visual vem de components/ui/ErrorState (textos padrao em errorState.*).
3. Teclado cobrindo formulario
O app roda com edgeToEdgeEnabled: true. No Android 15+ isso faz o android:windowSoftInputMode="adjustResize" deixar de redimensionar a janela, entao o teclado passa por cima dos campos mesmo com o manifest correto. Foi por isso que o problema aparecia mais no Android.
Nao usamos react-native-keyboard-controller: e dependencia nativa e quebraria o fluxo de teste no Expo Go. A solucao e em JS:
hooks/use-keyboard-inset.tsescutakeyboardWillChangeFrame/keyboardWillHide(iOS) ekeyboardDidShow/keyboardDidHide(Android) e devolve a altura atual do teclado.components/ui/FormScrollViewe umScrollViewque somakeyboardInset + insets.bottomnopaddingBottom, comkeyboardShouldPersistTaps="handled"ekeyboardDismissMode="on-drag".
Toda tela com formulario usa FormScrollView no lugar de ScrollView. Lista com apenas um campo de busca no header nao precisa.
Sheets do @gorhom/bottom-sheet ja tratam teclado por conta propria (android_keyboardInputMode="adjustResize" + keyboardBehavior no components/ui/BottomSheet), entao input dentro de sheet nao precisa de FormScrollView.
Estado do modulo de contratos
O modulo contracts foi o primeiro varrido por completo. Padrao adotado, que serve de modelo para os demais:
Leitura guarda uma flag de falha ao lado do loading no proprio store, e a lista renderiza ContractFlowErrorState (mesma moldura do ContractFlowEmptyState, com botao de recarregar) em vez do estado vazio:
| Store / hook | Flag | Onde aparece |
|---|---|---|
use-contract-review | loadFailed, validatorsFailed, commentsFailed, repliesFailed, revisionsFailed | etapa Revisao, telas de comentario, resposta, revisao e validadores |
use-contract-signatures | loadFailed | etapa Assinatura |
use-contract-provider | loadFailed | etapa Prestador |
use-contract-detail | panelsFailed | paineis de aditivo e compliance |
use-contract-model-ui | templateFailed, previewFailed, optionsFailed | etapa Modelo, previa, sheet de opcoes |
use-contract-part-search | searchFailed (via resolvePartSearchFailure) | modal de adicionar parte |
use-contract-signature-workflow | failed | somado ao failed da etapa Assinatura |
Escrita ja estava coberta: as acoes fazem throw e quem chama usa useError().handleError, que toasta a mensagem do backend.
Boundaries do modulo: cada etapa do editor roda dentro de um AppErrorBoundary com key={activeStep} e scope={contract-editor:<etapa>}, entao um crash de etapa nao derruba o contrato inteiro. O DocumentPaperPreview tem boundary proprio com fallback embutido (DocumentPaperPreviewFallback), porque a previa monta WebView de HTML de terceiro.
Degradacao aceita (nao vira UI de erro): preview de substituicao de variavel nos itens de revisao (use-contract-review.ts, item aparece sem previa) e parse de mensagem do WebView do documento (use-contract-review-validator-document.ts, sem efeito visivel).
Estado dos modulos de template e workflow
Aqui estava o problema mais grave da varredura: tela travada, nao so estado vazio.
use-template-structure.ts tinha tres acoes async que ligavam a flag e so a desligavam no caminho feliz. Qualquer falha de rede deixava a flag ligada para sempre:
| Acao | Flag presa | Sintoma |
|---|---|---|
init | loading | tela presa no skeleton, com toast de erro |
saveDraft | saving | botao salvar preso em "salvando" |
publish | publishing | botao publicar preso |
O corpo de cada uma passou para dentro de runTemplateStructureTask, com .finally(() => set({ <flag>: false })). A regra vale para qualquer acao async de store: flag ligada exige finally, nunca reset no fim do caminho feliz. Coberto por __tests__/modules/templates/hooks/use-template-structure-loading.test.ts.
Demais fallbacks:
| Store / hook | Flag | Onde aparece |
|---|---|---|
use-templates | loadFailed | lista de modelos |
use-workflows | loadFailed | lista de fluxos |
use-signature-workflows | loadFailed | lista de fluxos de assinatura |
use-revision-workflows-list-screen | failed | lista de fluxos de revisao |
use-signature-workflows-list-screen | failed | lista de fluxos de assinatura |
use-template-signature-flow | flowsFailed | seletor de fluxo no template |
use-template-types | failed | seletor de tipo de contrato |
A previa da estrutura (renderPreview) passou a toastar com useError().handleError no lugar de so logar, tanto ao abrir o painel quanto ao trocar valor de variavel.
Boundaries: EditorPanelScreenLayout embrulha o conteudo em AppErrorBoundary (scope=editor-panel:<titulo>), o que cobre de uma vez os paineis de workflow, revisao, assinatura, compliance e template. TemplateStructureScreen e TemplateEditorScreen tem boundary proprio.
Degradacao aceita: use-template-document-fonts (o documento renderiza com a fonte padrao se a fonte customizada nao carregar).