Skip to main content

lighthouse_north_client 0.2.0 copy "lighthouse_north_client: ^0.2.0" to clipboard
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. [...]

example/lib/main.dart

// App mínimo de integração com o SDK (codinome) LighthouseNorth — fatia 3.2.
//
// Demonstra o FLUXO LEGAL completo que todo app cliente deve ter:
//  1. OPT-IN explícito: botão "Ativar push" → permissão do OS → registro com
//     evidência de consentimento (a plataforma grava a trilha no contato);
//  2. heartbeat ligado junto do registro (mantém o device vivo/ativo);
//  3. OPT-OUT fácil e gratuito, sempre visível: revoga o consentimento de
//     marketing + remove o device — requisito LEGAL BLOQUEANTE do produto.
//
// A APP KEY vem do PAINEL DO TENANT (Apps > Chaves) — NUNCA hardcode no
// fonte. Preencha o campo abaixo com a sua key.
import 'package:lighthouse_north_client/lighthouse_north_client.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'LighthouseNorth SDK — exemplo',
      theme: ThemeData(
          colorSchemeSeed: const Color(0xFF2563EB), useMaterial3: true),
      home: const SdkHomePage(),
      debugShowCheckedModeBanner: false,
    );
  }
}

class SdkHomePage extends StatefulWidget {
  const SdkHomePage({super.key});

  @override
  State<SdkHomePage> createState() => _SdkHomePageState();
}

class _SdkHomePageState extends State<SdkHomePage> {
  final TextEditingController _appKey = TextEditingController();
  final TextEditingController _apiBaseUrl =
      TextEditingController(text: 'http://localhost:3000');
  final TextEditingController _contactRef = TextEditingController();

  // Desktop/web não têm os handlers nativos deste plugin — o fake é o modo
  // dev padrão lá; em android/ios o caminho nativo real é o padrão.
  bool _useFakeProvider = !kIsWeb &&
      defaultTargetPlatform != TargetPlatform.android &&
      defaultTargetPlatform != TargetPlatform.iOS;

  LighthouseNorthClient? _client;
  final InMemoryLighthouseNorthLogger _logger = InMemoryLighthouseNorthLogger();
  bool _busy = false;

  @override
  void dispose() {
    unawaited(_client?.close());
    _appKey.dispose();
    _apiBaseUrl.dispose();
    _contactRef.dispose();
    super.dispose();
  }

  static void unawaited(Future<void>? future) => future?.ignore();

  Future<LighthouseNorthClient> _ensureClient() async {
    if (_client != null) return _client!;
    if (_appKey.text.trim().isEmpty) {
      throw const InvalidAppKeyException(
        'preencha a app key do painel do tenant (Apps > Chaves)',
      );
    }
    _client = await LighthouseNorthClient.init(
      _appKey.text.trim(),
      apiBaseUrl: _apiBaseUrl.text.trim(),
      // Fake recebe a plataforma REAL do host: a API valida o binding
      // app↔plataforma e rejeita registro com plataforma trocada
      // (device_platform_not_in_app — provado no simulador iOS 21/10/2026).
      tokenProvider:
          _useFakeProvider ? FakeTokenProvider(platform: _hostPlatform) : null,
      logger: _logger,
    );
    return _client!;
  }

  /// Plataforma real do host para o provider fake reportar no registro.
  DevicePlatform get _hostPlatform {
    if (kIsWeb) return DevicePlatform.web;
    return switch (defaultTargetPlatform) {
      TargetPlatform.iOS => DevicePlatform.ios,
      _ => DevicePlatform.android,
    };
  }

  Future<void> _registerForPush() async {
    await _run(() async {
      final client = await _ensureClient();
      final reg = await client.registerForPush(
        contactRef:
            _contactRef.text.trim().isEmpty ? null : _contactRef.text.trim(),
        policyVersion: '2026-09',
      );
      await client.startHeartbeat();
      _toast(
        'Device registrado (${reg.updatedExisting ? 'atualizado' : 'novo'}) '
        '· token ${reg.maskedToken}',
      );
    });
  }

  Future<void> _optOut() async {
    await _run(() async {
      final client = await _ensureClient();
      final result = await client.optOut();
      if (result.fullyOptedOut) {
        _toast('Push desativado — consentimento revogado e device removido.');
      } else {
        // Requisito legal: falha parcial/total NÃO pode parecer sucesso.
        _toast(
          'Opt-out INCOMPLETO (${result.failures.map((f) => f.step.name).join(', ')}) '
          '— toque de novo para re-tentar.',
          error: true,
        );
      }
    });
  }

  Future<void> _run(Future<void> Function() action) async {
    if (_busy) return;
    setState(() => _busy = true);
    try {
      await action();
    } on PushPermissionDeniedException {
      _toast('Permissão de notificação negada — o device NÃO foi registrado.',
          error: true);
    } on FirebaseNotConfiguredException {
      _toast(
          'Host sem Firebase — configure o Firebase do app ou ative o provider fake (dev).',
          error: true);
    } on LighthouseNorthException catch (e) {
      _toast('$e', error: true);
    } finally {
      if (mounted) setState(() => _busy = false);
    }
  }

  void _toast(String message, {bool error = false}) {
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor:
            error ? Theme.of(context).colorScheme.errorContainer : null,
        duration: const Duration(seconds: 4),
      ),
    );
    setState(() {}); // atualiza painel de status/log
  }

  @override
  Widget build(BuildContext context) {
    final state = _client?.currentState;
    final events = _logger.events.reversed.take(8).toList();
    return Scaffold(
      appBar: AppBar(title: const Text('LighthouseNorth SDK — exemplo (codinome)')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _section('Configuração (do painel do tenant — nunca hardcode)'),
          TextField(
            controller: _appKey,
            decoration: const InputDecoration(
              labelText: 'App key (ak_…)',
              hintText: 'Painel do tenant → Apps → Chaves',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 8),
          TextField(
            controller: _apiBaseUrl,
            decoration: const InputDecoration(
              labelText: 'API base URL',
              helperText: 'http:// só é aceito em localhost (dev/testes)',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 8),
          TextField(
            controller: _contactRef,
            decoration: const InputDecoration(
              labelText: 'contactRef (external_id do contato — opcional)',
              helperText:
                  'Com contactRef o opt-in cria base legal push (marketing).',
              border: OutlineInputBorder(),
            ),
          ),
          SwitchListTile(
            value: _useFakeProvider,
            onChanged: (v) => setState(() => _useFakeProvider = v),
            title: const Text('Provider fake de token (dev sem Firebase)'),
            subtitle: const Text(
              'Substitui OS/Firebase por FakeTokenProvider — o resto do fluxo é real.',
            ),
          ),
          const Divider(height: 32),
          _section('Fluxo legal'),
          FilledButton.icon(
            onPressed: _busy ? null : _registerForPush,
            icon: const Icon(Icons.notifications_active_outlined),
            label: const Text('Ativar push (opt-in + consentimento)'),
          ),
          const SizedBox(height: 8),
          // OPT-OUT: sempre visível, fácil e gratuito — requisito legal.
          FilledButton.tonalIcon(
            onPressed: _busy ? null : _optOut,
            style: FilledButton.styleFrom(
              foregroundColor: Theme.of(context).colorScheme.error,
            ),
            icon: const Icon(Icons.notifications_off_outlined),
            label:
                const Text('Desativar push (opt-out — revoga consentimento)'),
          ),
          const Divider(height: 32),
          _section('Estado do SDK'),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(12),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _kv('Inicializado', _client != null ? 'sim' : 'não'),
                  _kv('Registrado',
                      _client?.isRegistered == true ? 'sim' : 'não'),
                  _kv('Heartbeat',
                      _client?.isHeartbeatRunning == true ? 'ativo' : 'parado'),
                  _kv('Token (mascarado)',
                      state?.token == null ? '—' : redactSecret(state!.token!)),
                  _kv('contactRef', state?.contactRef ?? '—'),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          _section('Eventos do SDK (log estruturado)'),
          if (events.isEmpty)
            const Text('Nenhum evento ainda — faça o opt-in.')
          else
            ...events.map(
              (e) => Text(
                '${e.$1.name.padRight(7)} ${e.$2} ${e.$3.isEmpty ? '' : e.$3}',
                style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
              ),
            ),
          const SizedBox(height: 24),
          Text(
            'Nota: nome de publicação do SDK a definir (codinome). '
            'O SDK coleta apenas platform/token/contactRef/consent (minimização LGPD) '
            'e não renderiza conteúdo de push (plain-text por construção).',
            style: Theme.of(context).textTheme.bodySmall,
          ),
        ],
      ),
    );
  }

  Widget _section(String title) => Padding(
        padding: const EdgeInsets.only(bottom: 8),
        child: Text(title, style: Theme.of(context).textTheme.titleSmall),
      );

  Widget _kv(String k, String v) => Padding(
        padding: const EdgeInsets.symmetric(vertical: 2),
        child: Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            SizedBox(
                width: 140,
                child: Text(k,
                    style: const TextStyle(fontWeight: FontWeight.w600))),
            Expanded(child: Text(v)),
          ],
        ),
      );
}
0
likes
0
points
0
downloads

Publisher

verified publisherlighthousenorth.com.br

Weekly Downloads

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. Minimização por construção: coleta apenas platform/token/contactRef/consent.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, http, meta

More

Packages that depend on lighthouse_north_client

Packages that implement lighthouse_north_client