lighthouse_north_client 0.2.0
lighthouse_north_client: ^0.2.0 copied to clipboard
SDK Flutter oficial para integrar o app do cliente à plataforma Lighthouse North de disparo push: registro de device token com evidência de consentimento (LGPD), heartbeat e opt-out fácil e gratuito. [...]
client-flutter — SDK Flutter da plataforma Lighthouse North (CODINOME) #
Codinome: o NOME DE PUBLICAÇÃO deste pacote (pub.dev) é PENDENTE de decisão do dono.
client_flutteré o nome interno neutro do monorepo (ADR-13); renomear no futuro é uma operação mecânica. Enquanto isso,publish_to: none.
SDK Flutter que integra o app do cliente final à plataforma Lighthouse North de disparo push. A plataforma cuida do disparo e da segmentação; o SDK coleta o mínimo e oferece opt-out fácil e gratuito.
Compromissos deste SDK (contrato de produto — fatia 3.2) #
- Minimização LGPD por construção. O SDK coleta e envia
EXATAMENTE
platform,token,contactRefeconsent— nada mais. Sem IP custom, sem User-Agent extra, sem identificadores de propaganda, sem qualquer dado do app hospedeiro. O User-Agent é fixo (lighthouse-north-client-flutter/0.1.0 (dart;os)) e identifica apenas o SDK. - Plain-text por construção. O SDK NÃO renderiza conteúdo de push — a exibição é entregue ao sistema operacional. Não há WebView/HTML no caminho (deliberação do gate de segurança 3.1).
- Opt-out fácil e gratuito (requisito legal BLOQUEANTE):
optOut()nunca lança exceção de rede, é best-effort com resultado explícito, e a UI do app DEVE oferecê-lo sempre (ver exemplo/). - Cobrança honesta: se o usuário revogar a permissão de notificação nas
Configurações do OS, o SDK remove o device da plataforma automaticamente
(
onPermissionRevoked). - Zero segredos em log: token e app key só aparecem mascarados
(
fcm-...cdef).
Instalação #
Ainda não publicado no pub.dev (nome pendente). Use via path/git:
dependencies:
client_flutter:
path: ../packages/client-flutter # ou git: https://github.com/SystemOverclock/MsgBoy
Requisitos: Flutter/Dart 3 (null-sound). Dependências do pacote: apenas
http (+ Flutter SDK). O SDK não embute Firebase — ver abaixo.
Integração passo a passo #
1. Obtenha a app key no painel do tenant #
Painel Lighthouse North → Apps → Chaves. O material tem formato ak_… e é a
credencial PÚBLICA do device flow (distinta das API keys pk_…). Ela é
configuração do app cliente: nunca hardcoded no fonte — use
--dart-define, config de ambiente ou o backend do seu app.
2. Inicialize #
import 'package:client_flutter/client_flutter.dart';
final client = await LighthouseNorthClient.init(
appKey, // do painel — NUNCA hardcoded
apiBaseUrl: 'https://api.seu-ambiente.lighthouse', // default: ver doc da API
);
Regras de apiBaseUrl: TLS-only — https:// sempre; http:// só é
aceito em localhost/127.0.0.1/[::1] para dev/testes (erro tipado
InsecureBaseUrlException caso contrário). Se um heartbeat estava ativo,
o init retoma o timer e faz um refresh imediato (last_seen honesto).
3. Opt-in de push (permissão + token + consentimento) #
Chame a partir de uma ação explícita do usuário (botão/check de "quero receber notificações") — o consentimento precisa ser explícito (LGPD art. 7º):
try {
final reg = await client.registerForPush(
contactRef: 'id-do-contato-no-seu-sistema', // external_id — opcional
policyVersion: '2026-09', // versão da política exibida
);
await client.startHeartbeat(); // 24h com jitter ±10% (sobrevive a restart)
} on PushPermissionDeniedException {
// usuário negou no OS: device NÃO registrado — trate e siga (não é erro)
}
O que o fluxo faz: (1) pede a permissão de notificação do OS; (2) obtém o
token push (Firebase do SEU app); (3) registra na plataforma com a evidência
do consentimento (consent {granted: true, capturedAt, policyVersion} — a
plataforma grava a trilha no contato quando há contactRef); (4) persiste
localmente o mínimo (token/contactRef) para heartbeat e opt-out.
contactRef: se você quer push de MARKETING atrelado ao contato, passe o
external_id dele. Sem contactRef, o device registra sem base legal de
marketing (apto a transacional futuro).
4. Opt-out — OBRIGATÓRIO na UI do seu app #
final result = await client.optOut();
if (!result.fullyOptedOut) {
// Falha de rede: em falha TOTAL (nada concluído) o SDK preservou o estado —
// re-tentar optOut() conclui. Em falha PARCIAL do unregister (consent já
// revogado) o estado foi limpo: re-tentar é noop; o device remanescente
// segue no servidor apenas para transacional até o expurgo (90d sem
// refresh). Nunca mostre "desativado" se não foi.
}
optOut() é best-effort em ordem deliberada: (1) revoga a base de marketing
(consent granted=false + contactRef); (2) remove o device
(unregister). Nunca lança exceção de rede — o resultado reporta cada passo
(consentRevoked / unregistered / stateCleared / failures). Em falha
TOTAL o SDK preserva o estado local mínimo para permitir a re-tentativa —
descartar o token descartaria a capacidade de concluir o opt-out.
5. Revogação da permissão no OS (permission watcher) #
client.onPermissionRevoked.listen((event) {
// O SDK JÁ removeu o device da plataforma (cobrança honesta).
// Aqui você decide a UX: explicar, pedir reativação etc.
});
Token via firebase_messaging do hospedeiro (recomendado) #
O SDK não embute nem inicializa Firebase — ele lê o token do Firebase do
seu app. No Android isso é automático (o handler nativo detecta o SDK do
host). Se o seu app já usa o pacote firebase_messaging, o caminho mais
simples em ambas as plataformas é fornecer o token via provider próprio:
class HostFirebaseTokenProvider implements PushTokenProvider {
@override
Future<bool> requestPermission() async {
final settings = await FirebaseMessaging.instance.requestPermission();
return settings.authorizationStatus == AuthorizationStatus.authorized;
}
@override
Future<String> getToken() => FirebaseMessaging.instance.getToken()
.then((t) => t ?? throw PushTokenUnavailableException('token nulo'));
@override
DevicePlatform resolvePlatform() => DevicePlatform.android; // ou ios
}
await LighthouseNorthClient.init(appKey, tokenProvider: HostFirebaseTokenProvider());
Sem Firebase no host: getToken lança FirebaseNotConfiguredException
(erro claro e tipado). Em dev/testes, injete FakeTokenProvider().
Requisitos por plataforma (3.3 — nativos elevados) #
| Plataforma | Permissão | Token | Storage |
|---|---|---|---|
| Android | ✅ POST_NOTIFICATIONS (13+; <13 concedida por padrão) |
✅ FCM do host (sem Firebase no host → erro tipado) | Android Keystore AES-256-GCM (3.3; migra o plaintext da 3.2 e o remove) |
| iOS | ✅ UNUserNotificationCenter |
✅ FCM do host detectado em runtime (3.3, sem dependência no pod); sem Firebase → erro tipado + provider Dart documentado | Keychain (3.3; migra o UserDefaults plaintext e o remove) |
| Web | — | 3.4 (push VAPID) | 3.4 |
| Desktop (dev) | — | FakeTokenProvider |
InMemoryDeviceStateStore |
A detecção de revogação de permissão acontece na volta ao primeiro plano
(lifecycle) — ver docs/NATIVE_CONTRACT.md.
Rotação de token FCM (3.3 — onNewToken → refresh automático) #
O canal lighthouse_north_client agora recebe onNewToken do lado nativo e o SDK
executa a rotação AUTOMATICAMENTE: registra o token NOVO (upsert,
SEM consent — rotação não é ato de consentimento (@gate QA 3.3: reafirmar granted=true ressuscitaria consentimento revogado no intervalo do optOut parcial; a base vigente no servidor é a autoridade)) + unregister best-effort do antigo (evita push duplicado até o
expurgo de 90d) + atualiza o estado local. O resultado chega na stream
onTokenRotated (UX/logging); hosts com firebase_messaging próprio podem
chamar handleTokenRotation(token) manualmente. No Android, um host com
service nativo próprio encaminha com:
override fun onNewToken(token: String) {
LighthouseNorthClientPlugin.dispatchNewToken(applicationContext, token)
}
Segurança e comportamento de rede #
- TLS-only (exceção documentada: loopback para dev/testes);
- timeout de 10s por tentativa;
- retry com backoff exponencial (500ms → 1s → 2s) + jitter ±25%, máx. 3
tentativas, apenas em operações idempotentes (as 3 rotas de devices) e
apenas em
408 | 429 | 5xx; - User-Agent próprio fixo; nenhum header do app hospedeiro;
- token/app key NUNCA logados inteiros (máscara
redactSecret); - log estruturado opcional (
Lighthouse NorthLogger), silencioso por padrão.
Erros tipados (resumo) #
Todos os erros do SDK estendem Lighthouse NorthException (hierarquia sealed):
| Erro | Quando |
|---|---|
PushPermissionDeniedException |
usuário negou a permissão (fluxo normal — não é falha) |
FirebaseNotConfiguredException |
host sem Firebase (configure o host ou use provider/fake) |
UnauthorizedAppKeyException |
app key rejeitada (401) — confira o painel |
Lighthouse NorthApiException |
erro HTTP da API (statusCode/errorCode no contexto) |
Lighthouse NorthNetworkException |
rede/timeout após as 3 tentativas |
InsecureBaseUrlException / InvalidBaseUrlException |
base URL http fora de localhost / malformada |
InvalidAppKeyException |
app key evidentemente inválida no init |
Lighthouse NorthInvalidInputException |
contactRef 1..200 / token 16..4096 (validação local) |
Lighthouse NorthUnsupportedPlatformException |
plataforma sem caminho nativo nesta versão |
LighthouseNorthClientClosedException / Lighthouse NorthNotInitializedException |
ciclo de vida |
Testes e fakeabilidade #
Toda dependência é injetável em LighthouseNorthClient.init: httpClient,
tokenProvider (FakeTokenProvider), stateStore (InMemory…),
logger, clock (FakeClock), scheduler (FakeScheduler — tempo
virtual para heartbeat/backoff), random (Fixed… — jitter determinístico).
cd packages/client-flutter
flutter analyze # No issues found
flutter test # 87 testes verdes (76 da 3.2 + 11 da 3.3: rotação/version check)
O app example/ demonstra o fluxo legal completo (opt-in + opt-out
obrigatório na UI) — rode com a API local (docker compose up -d pg redis api) e uma app key real do painel.
Contrato nativo e roadmap 3.3 #
docs/NATIVE_CONTRACT.md— spec canônica do canallighthouse_north_clientpara os SDKs nativos Kotlin/Swift da fatia 3.3 (interface, fluxos, erros, storage).- 3.3 ENTREGUE nos nativos embutidos: 1ª compilação real (example APK no
emulador — a pendência "nativos não compilados" da 3.2 está fechada),
storage cifrado (Keystore/Keychain), rotação
onNewToken→ refresh automático, getToken iOS via Firebase do host detectado em runtime, correções de API do embedding (ActivityAware/lifecycle 2.7.0) que só a compilação real revelou. - Ainda 3.4+: Firebase/APNs do projeto Lighthouse North (ação do dono), web push VAPID, observação contínua de permissão. Build iOS pendente de macOS (sem Xcode nesta máquina — o Swift foi escrito e revisado, não compilado).
Licença #
MIT (ver LICENSE).