MobileBloqueio e biometria

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

GatilhoComportamento
Cold startSe há uma sessão e a biometria está disponível, o app abre bloqueado.
Background → foregroundBloqueia 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.
InatividadeBloqueia 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] sobre bg-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.