Neste guia documento a implantação de Biometria em um projeto pessoal, registrando os detalhes técnicos e as decisões tomadas durante o processo.

Esta documentação foi produzida com o auxílio do ChatGPT, a partir das alterações realizadas no código do projeto, de discussões sobre o uso de biometria com o Supabase e de consultas à documentação oficial das ferramentas envolvidas.

O objetivo é agrupar as informações relevantes a esta implementação em um único material de referência, servindo como guia para futuras reproduções ou adaptações da funcionalidade em outros projetos.

O projeto segue uma arquitetura em camadas baseada no padrão MVVM (Model–View–ViewModel), alinhada às recomendações do guia oficial de arquitetura do Flutter Team: Flutter App Architecture Guide. Essa escolha permite separar claramente infraestrutura, lógica de negócio e interface, tornando o código mais modular, testável e adaptável para futuras evoluções.

1. Dependências

A implantação da biometria no Sports se baseia em duas bibliotecas principais do Flutter:

1.1 local_auth

  • Função: fornece a interface nativa para autenticação biométrica no Android (impressão digital, reconhecimento facial) e iOS (FaceID, TouchID).
  • Porque usar:
  • É o pacote oficial do Flutter para biometria.
  • Abstrai as diferenças entre plataformas.
  • Limitações:
  • Apenas informa se a autenticação foi bem-sucedida ou não.
  • Não entrega um token ou identidade — só libera o acesso.
  • Exige que a Activity do Android seja FlutterFragmentActivity.

Instalação

dependencies:
  local_auth: ^2.3.0

Plataformas suportadas

  • Android → Impressão digital, Face Unlock (quando disponível), Iris.
  • iOS → Face ID, Touch ID.
  • Windows → suporte experimental.

1.2 flutter_secure_storage

  • Função: provê armazenamento seguro de dados no dispositivo, usando:
  • Keychain (iOS).
  • Keystore (Android).
  • Porque usar:
  • Armazena credenciais ou tokens localmente, de forma criptografada.
  • É a única forma confiável de manter dados sensíveis entre sessões.

Instalação

dependencies:
  flutter_secure_storage: ^4.2.1

Plataformas suportadas

  • Android, iOS, Linux, macOS, Windows.

Uso típico

  • Salvar credenciais do usuário após login com email/senha.
  • Recuperar essas credenciais após autenticação biométrica.

1.3 Dependências secundárias e ajustes

Além das duas principais, foram necessários alguns ajustes adicionais:

  • Android
  • Alterar MainActivity para herdar de FlutterFragmentActivity: class MainActivity : FlutterFragmentActivity()
  • Sem isso, local_auth falha com: PlatformException(no_fragment_activity, local_auth plugin requires activity to be a FragmentActivity.)
  • Linux
  • flutter_secure_storage precisa estar registrado em:
    • linux/flutter/generated_plugin_registrant.cc
    • linux/flutter/generated_plugins.cmake
  • Flutter SDK
  • O local_auth exige SDK mínimo atualizado. No Sports, o flutter foi ajustado para >=3.35.0.

1.4 Observação Importante (Supabase)

O Supabase atualmente não fornece refresh tokens reutilizáveis (GoTrue v2).

  • O campo session.refreshToken retorna apenas um identificador curto (abc123xyz), que não pode ser usado em setSession().
  • Por isso, optou-se por armazenar as credenciais do usuário (email/senha) em flutter_secure_storage, protegidas pela biometria.
  • No login biométrico → desbloqueia as credenciais → executa novamente signInWithPassword().
  • Ou seja: biometria hoje funciona como atalho seguro para login com credenciais, não como restauração de sessão via refresh token.

2. Ajustes de Plataforma

2.1 Android

2.1.1 Alteração em MainActivity

O plugin local_auth depende do uso de FragmentActivity, pois a autenticação biométrica precisa de um fragment manager para lidar com os diálogos nativos do sistema.

Por padrão, o Flutter gera uma MainActivity que estende FlutterActivity. Isso causa o erro:

PlatformException(no_fragment_activity, local_auth plugin requires activity to be a FragmentActivity., null, null)
Solução

Editar o arquivo:
android/app/src/main/kotlin/<seu_pacote>/MainActivity.kt

Trocar:

import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
}

Por:

import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity : FlutterFragmentActivity()

Caso o projeto esteja em Java, o mesmo se aplica:

import io.flutter.embedding.android.FlutterFragmentActivity;

public class MainActivity extends FlutterFragmentActivity {
}

2.1.2 Permissões no AndroidManifest.xml

O local_auth geralmente não exige permissões extras, pois usa o sistema nativo.
Mas em versões antigas de Android, pode ser necessário garantir:

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

Nota: USE_FINGERPRINT é legado, mas ainda útil para Android < 10.
Em versões recentes, apenas USE_BIOMETRIC é suficiente.

2.1.3 Compatibilidade de SDK

  • No android/app/build.gradle.kts, certifique-se de usar:
  defaultConfig {
      minSdk = 23   // obrigatório para local_auth
      targetSdk = 34
  }

2.1.4 Testes em dispositivos/emuladores

  • Emuladores não suportam biometria por padrão.
  • Para testar:
  • Use adb para simular toque de digital: adb -e emu finger touch 1
  • Ou habilite Fingerprint no AVD Manager.
  • Em dispositivos reais, basta cadastrar digital/face nas configurações do sistema.

Resumindo:

  • Alterar MainActivity para FlutterFragmentActivity.
  • Garantir permissões (USE_BIOMETRIC, USE_FINGERPRINT).
  • Usar minSdk >= 23.
  • Testar em dispositivo físico para validar fluxo.

3. Service Layer

A camada de Service é responsável por lidar diretamente com as bibliotecas externas (local_auth, flutter_secure_storage) e expor uma API simples e consistente para o restante da aplicação.
Ela não deve conter lógica de negócio, apenas adaptação entre o app e o SDK/plataforma.

3.1 BiometricService

Função

Encapsular a interação com o pacote local_auth.
Isso permite que o restante da aplicação não dependa diretamente do plugin.

Métodos principais

  • isAvailable()
  • Verifica se o dispositivo suporta biometria e se há alguma biometria cadastrada.
  • Combina canCheckBiometrics e isDeviceSupported.
  • authenticate(reason)
  • Exibe o prompt nativo de autenticação biométrica.
  • Retorna true/false de acordo com o sucesso ou falha.
  • Recebe o parâmetro reason, que é exibido no diálogo nativo (ex.: “Use sua digital para continuar”).

Exemplo simplificado

import 'package:local_auth/local_auth.dart';

class BiometricService {
  final LocalAuthentication _auth = LocalAuthentication();

  Future<bool> isAvailable() async {
    try {
      final canCheck = await _auth.canCheckBiometrics;
      final supported = await _auth.isDeviceSupported();
      return canCheck && supported;
    } catch (_) {
      return false;
    }
  }

  Future<bool> authenticate({String reason = 'Authenticate to continue'}) async {
    try {
      return await _auth.authenticate(
        localizedReason: reason,
        options: const AuthenticationOptions(
          biometricOnly: true,
          stickyAuth: true,
        ),
      );
    } catch (_) {
      return false;
    }
  }
}

3.2 SecureStorageService

Função

Gerenciar o armazenamento seguro de informações sensíveis no dispositivo, usando flutter_secure_storage.

É aqui que ficam guardadas as credenciais (no caso atual, email e senha, já que o Supabase não disponibiliza refresh tokens reutilizáveis).

Métodos principais

  • saveString(key, value)
  • Salva dados criptografados no Keychain (iOS) ou Keystore (Android).
  • readString(key)
  • Recupera o valor armazenado.
  • delete(key)
  • Remove o valor armazenado.

Exemplo simplificado

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class SecureStorageService {
  final FlutterSecureStorage _storage = const FlutterSecureStorage();

  Future<void> saveString(String key, String value) async {
    await _storage.write(key: key, value: value);
  }

  Future<String?> readString(String key) async {
    return await _storage.read(key: key);
  }

  Future<void> delete(String key) async {
    await _storage.delete(key: key);
  }
}

3.3 Integração Biometria + Storage

O fluxo esperado fica assim:

  1. Após login normal, salvar as credenciais no SecureStorageService.
  2. Quando usuário optar pelo login biométrico:
  • BiometricService.authenticate() → se sucesso,
  • SecureStorageService.readString() → recupera credenciais salvas,
  • AuthService.signInWithPassword() → refaz login com Supabase.

3.4 Resumo

  • BiometricService → interação com hardware biométrico.
  • SecureStorageService → armazenamento de credenciais de forma segura.
  • Juntas, permitem que a biometria seja usada como “chave” para liberar as credenciais e reexecutar o login.

4. Repository Layer

O repositório serve como fachada para os serviços de biometria e armazenamento seguro, expondo operações de alto nível para o domínio da aplicação.
Aqui não lidamos mais com detalhes do local_auth ou do flutter_secure_storage, mas sim com funções de negócio: salvar credenciais, checar disponibilidade e autenticar.

4.1 Interface IBiometricRepository

A interface define os métodos que a aplicação pode chamar, garantindo independência da implementação.

abstract class IBiometricRepository {
  Future<void> storeBiometricData(String data);
  Future<String?> authenticateWithBiometrics(String reason);
  Future<bool> hasBiometricData();
}

4.2 Implementação BiometricRepository

Aqui ocorre a integração real entre BiometricService e SecureStorageService.

import '/data/services/biometric/biometric_service.dart';
import '/data/services/secure_storage/secure_storage_service.dart';

class BiometricRepository implements IBiometricRepository {
  final BiometricService _biometricService;
  final SecureStorageService _storageService;

  static const _tokenKey = 'biometric_token';

  BiometricRepository({
    required BiometricService biometricService,
    required SecureStorageService storageService,
  })  : _biometricService = biometricService,
        _storageService = storageService;

  @override
  Future<void> storeBiometricData(String data) async {
    await _storageService.saveString(_tokenKey, data);
  }

  @override
  Future<String?> authenticateWithBiometrics(String reason) async {
    final available = await _biometricService.isAvailable();
    if (!available) return null;

    final authenticated = await _biometricService.authenticate(reason: reason);
    if (!authenticated) return null;

    return await _storageService.readString(_tokenKey);
  }

  @override
  Future<bool> hasBiometricData() async {
    final stored = await _storageService.readString(_tokenKey);
    return stored != null;
  }
}

4.3 Responsabilidades de cada método

  • storeBiometricData(String data)
  • Executado após login normal.
  • Salva a credencial (no caso atual, email/senha ou DTO serializado) de forma segura no dispositivo.
  • authenticateWithBiometrics()
  • Primeiro valida se a biometria está disponível.
  • Executa autenticação biométrica.
  • Se sucesso, retorna a credencial previamente armazenada.
  • Essa credencial é usada para refazer o login no Supabase.
  • hasBiometricData()
  • Apenas verifica se existe algum dado salvo.
  • Útil para decidir se o botão de login biométrico deve ser exibido na tela de login.

4.4 Ponto importante (limitação Supabase)

Devido à limitação atual do Supabase (que não fornece refresh tokens reutilizáveis), a credencial armazenada não é o refresh token, mas sim o par email/senha ou um DTO com os dados de login.

  • Isso significa que o método authenticateWithBiometrics() retorna essas credenciais.
  • A camada de caso de uso (AccountManagementUseCase) então chama novamente signInWithPassword() para restaurar a sessão.

4.5 Resumo

O BiometricRepository encapsula a biometria + armazenamento seguro e expõe três operações simples:

  • Gravar credenciais.
  • Validar biometria e recuperar credenciais.
  • Verificar se existem dados para login biométrico.

5. Use Case / AuthService

A camada de Use Case / AuthService é onde a lógica de negócio é aplicada. Aqui conectamos o BiometricRepository com o UserAuthRepository (que fala com o Supabase).

5.1 Situação atual do Supabase (GoTrue v2, 2025)

  • O Supabase utiliza o GoTrue v2 como provedor de autenticação.
  • Diferente de versões anteriores, ele não gera refresh tokens reutilizáveis.
  • O campo session.refreshToken retorna apenas um ID curto (ex.: lcixpkoex3e2), que não funciona em auth.setSession().
  • Ao tentar usar esse valor em setSession(), o servidor retorna:
  AuthApiException(message: Invalid Refresh Token: Refresh Token Not Found)
  • Consequência: não é possível restaurar a sessão apenas com refresh token.

5.2 Implicação no login biométrico

  • Em sistemas que suportam refresh tokens, o fluxo normal seria:
  1. Guardar refresh token após login.
  2. Validar biometria.
  3. Usar refresh token em setSession() → restaurar sessão.
  • Mas como isso não é suportado pelo Supabase, foi necessário adotar outra abordagem.

5.3 Solução implementada

Fluxo de login inicial (email/senha)

  1. Usuário faz login via signInWithPassword().
  2. Supabase retorna um Session com accessToken e um refreshToken inválido para reuso.
  3. O app salva as credenciais do usuário (email e senha) no SecureStorage, criptografadas e protegidas por biometria.

Fluxo de login com biometria (segundo boot em diante)

  1. O usuário abre o app.
  2. A tela inicial verifica se existem credenciais armazenadas (hasBiometricData()).
  3. Caso positivo → chama BiometricService.authenticate().
  4. Se sucesso → o app lê as credenciais salvas.
  5. Reexecuta signInWithPassword() com Supabase, recriando a sessão.

Isso significa que o “login biométrico” é, na prática, um atalho seguro para reaproveitar email/senha sem o usuário precisar digitar novamente.

5.4 Vantagens dessa abordagem

  • Mantém a experiência do usuário fluida: basta usar biometria para logar.
  • Não depende de recursos ainda indisponíveis no Supabase.
  • Utiliza Secure Storage para proteger as credenciais.
  • Permite fallback: se biometria falhar, o usuário pode sempre digitar email/senha.

5.5 Limitações

  • Email e senha ficam armazenados localmente (mesmo que de forma criptografada).
  • Se o Supabase no futuro habilitar refresh tokens reais, será necessário refatorar essa parte do fluxo para usar setSession() em vez de signInWithPassword().
  • Em caso de alteração de senha pelo usuário, as credenciais salvas no dispositivo se tornam inválidas → exigindo login manual novamente.

5.6 Resumo

Na camada de Use Case/AuthService o papel da biometria é desbloquear credenciais salvas com segurança e reutilizá-las em um novo login no Supabase, contornando a limitação atual da plataforma.

6. UI / ViewModel

A camada de ViewModel expõe Commands que encapsulam chamadas assíncronas ao AccountManagementUseCase.
A UI não conhece detalhes de serviços nem de repositórios: ela apenas executa os Commands e reage aos seus estados (running, result, listeners).

6.1 ViewModel

No SignInViewModel, os métodos relacionados à biometria são transformados em Commands:

  • checkBiometricAvailability → executa _accountManagementUseCase.canUseBiometrics().
  • Indica se o dispositivo suporta biometria.
  • Em caso de falha, retorna CanNotUseBiometricsException.
  • checkBiometricData → executa _accountManagementUseCase.hasBiometricData().
  • Verifica se existem credenciais salvas no SecureStorage.
  • Caso não haja, retorna CanNotUseBiometricsException.
  • biometricLogin → executa _accountManagementUseCase.loginWithBiometrics(String reason).
  • Solicita autenticação biométrica.
  • Se sucesso → recupera credenciais armazenadas e refaz login com Supabase.
  • Se falha → expõe a exceção à UI.
  • storeBiometricData → executa _accountManagementUseCase.storeBiometricData(String data).
  • Responsável por salvar credenciais após login manual com email/senha.

Dessa forma, o ViewModel é apenas uma ponte de orquestração, sem lógica de autenticação.

6.2 UI

Na SignInView, a biometria é integrada de forma condicional:

  • Escuta dos Commands
  • addListener em checkBiometricAvailability, checkBiometricData e biometricLogin.
  • Atualiza variáveis locais (_biometricAvailable, _hasBiometricData) conforme resultados.
  • Renderização do botão biométrico
  • Apenas exibido se:
    • Dispositivo suporta biometria.
    • Há credenciais armazenadas.
  • Caso contrário → ícone esmaecido ou oculto.
  • Execução do login biométrico
  • Ao toque no botão → biometricLogin.execute("Autenticar com biometria...").
  • Enquanto running == true → exibe CircularProgressIndicator.
  • Ao concluir:
    • Sucesso → redireciona para Routes.events.
    • Falha → apresenta SnackBar ou BottomSheetMessage com motivo (cancelamento, ausência de sessão, erro genérico).

6.3 Resumo

  • ViewModel:
  • Expõe apenas Commands, encapsulando casos de uso (loginWithBiometrics, storeBiometricData, etc.).
  • Não contém lógica de UI, apenas traduz resultados em Result<void>.
  • UI:
  • Decide quando exibir o botão de biometria.
  • Executa os Commands e reage a seus estados (running, Success, Failure).
  • Fornece feedback claro ao usuário (loading, sucesso, falha).

Assim, a biometria no Sports segue fielmente o padrão Command + MVVM, mantendo separação entre infraestrutura, lógica de negócio e interface.

7. Conclusões

A biometria foi integrada ao Sports como um recurso de conveniência e segurança adicional, mas seu papel exato depende do contexto da aplicação.

  1. Fallback obrigatório
  • O login com email/senha deve permanecer disponível.
  • A biometria deve ser vista como atalho seguro, não como substituto único de autenticação.
  1. Logout e credenciais armazenadas
  • No Sports, o logout encerra a sessão atual, mas pode manter credenciais no SecureStorage para permitir login biométrico subsequente.
  • Esse comportamento é aceitável em apps onde a conveniência pesa mais que a segurança absoluta.
  • Em aplicações que lidam com dados sensíveis (ex.: bancos, saúde), o logout deve limpar totalmente o SecureStorage, impedindo qualquer reentrada sem autenticação completa.
  1. Limitação do Supabase
  • O Supabase (GoTrue v2, 2025) ainda não fornece refresh tokens reutilizáveis.
  • O campo session.refreshToken retorna apenas um identificador curto, que não funciona em auth.setSession().
  • Assim, a biometria hoje desbloqueia credenciais (email/senha) e refaz o login, em vez de restaurar sessão.
  1. Perspectiva futura
  • Quando o Supabase liberar refresh tokens persistentes, será possível simplificar:
    • Guardar apenas o refresh token.
    • Restaurar a sessão via auth.setSession(refreshToken).
  • Isso permitirá maior segurança, sem precisar armazenar email/senha no dispositivo.

A biometria no Sports é hoje uma solução prática, que aumenta a conveniência do login e protege as credenciais no dispositivo.
Entretanto, a estratégia de logout e armazenamento deve sempre ser ajustada ao nível de sensibilidade dos dados da aplicação: para apps comuns, manter credenciais pode ser aceitável; para apps críticos, o correto é limpar tudo no logout.