Bloqueio do app e biometria
Um bloqueio no estilo bancário que protege o app autenticado por trás de um desbloqueio biométrico (Face ID / Touch ID / impressão digital). Apenas nativo; na web essa camada inteira é um passthrough.
Por que biometria, e não passkey WebAuthn, no nativo
Passkeys WebAuthn reais não rodam dentro do WebView do Capacitor: a origem é
https://localhost, e não velon.finance (sem o entitlement associated-domains), então
navigator.credentials.get() falha. O equivalente nativo é uma verificação biométrica
do dono do dispositivo via o plugin local Biometric (iOS LAContext, Android
BiometricPrompt), que desbloqueia uma sessão que já está autenticada.
Na web, o fluxo de login com passkey WebAuthn real permanece inalterado e segue sendo a “passkey” canônica.
Gatilhos de bloqueio
| Gatilho | Comportamento |
|---|---|
| Cold start | Se há uma sessão e a biometria está disponível, o app abre bloqueado. |
| Background → foreground | Bloqueia após um curto período de tolerância (~1.5s) para que as folhas do SO (o próprio prompt do Face ID, a share sheet) não causem auto-bloqueio. |
| Inatividade | Bloqueia após ~2 min sem interação enquanto em foreground. |
A janela de tolerância importa: apresentar o prompt biométrico pode levar o WebView rapidamente para background. Sem ela, o app se bloquearia em loop. Enquanto um bloqueio está visível ou um desbloqueio está em andamento, as transições para background são ignoradas.
Regra de segurança: nunca prender o usuário
O bloqueio só é armado quando o dispositivo realmente tem uma biometria/credencial cadastrada. Se
nenhuma existe, o app não bloqueia (caso contrário, todo background forçaria um
logout completo). A tela de desbloqueio sempre oferece “Entrar com senha” → logout →
/login como saída de emergência.
Anatomia da tela de bloqueio
┌─────────────────────────────┐
│ │
│ [ Velon logo ] │ white wordmark on velon-navy
│ │
│ App bloqueado │
│ Use sua biometria para │
│ desbloquear e continuar. │
│ │
│ ( 👆 ) │ fingerprint button → biometric prompt
│ Desbloquear │
│ │
│ Entrar com senha │ → logout → /login
└─────────────────────────────┘- Overlay em tela cheia
fixed inset-0 z-[100]sobrebg-velon-navy, com padding nos dois safe-area insets. Ele precisa cobrir a nav inferior (daí o z-index alto). - Uma tentativa que falha mostra um erro inline; uma tentativa cancelada não (o usuário optou por dispensar).
Modelo de estado
O estado de bloqueio é puro e somente client-side (testado por unit tests). Apenas a
preferência de habilitação do usuário persiste entre reinicializações. isLocked, backgroundedAt e a
flag de in-flight são de runtime.
isLockEffectivelyEnabled(pref, available)→available && (pref ?? true): ligado por padrão quando há biometria, sobrescrevível por uma preferência explícita do usuário.shouldLockOnForeground(backgroundedAt, now)→now - backgroundedAt >= grace.
Nenhum backend está envolvido: a verificação biométrica é local e o desbloqueio apenas alterna o estado do client. O logout reutiliza o endpoint de logout de sessão existente.
Logout
O app expõe uma linha Sair no fim do menu Perfil mobile (com estilo
destrutivo). Ela chama o logout da auth store e então faz um hard-redirect para /login (com um
clear do local-storage + fallback de sign-out do Supabase) para que todo o estado em memória e persistido
seja descartado.
Cadastro (planejado)
Após o primeiro login por OTP no nativo, perguntar uma vez: “Ativar desbloqueio por biometria?”, além de um toggle em Segurança para optar por não usar. Até lá, o core fica ligado por padrão sempre que houver biometria disponível. Acompanhado em velon-app#545.