one_viewr_flutter 0.2.0 copy "one_viewr_flutter: ^0.2.0" to clipboard
one_viewr_flutter: ^0.2.0 copied to clipboard

Client Flutter embarquable pour Oneviewr CRM — identify/track/registerPushToken et source d'acquisition via une clé publique non secrète. Port direct de @oneviewr/js.

example/lib/main.dart

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:one_viewr_flutter/one_viewr_flutter.dart';

/// Application de démonstration de `one_viewr_flutter`.
///
/// C'est un formulaire unique, dans l'ordre où les commandes doivent être
/// appelées (`boot` → `identify` → `track`/`registerPushToken` → `reset`) —
/// pas un bac à sable : elle sert à vérifier sur un vrai réseau ce qu'aucun
/// test avec un faux serveur ne peut vérifier, contre un vrai backend
/// Oneviewr (local ou de production).
void main() => runApp(const OneViewrExampleApp());

/// Racine de la démonstration.
class OneViewrExampleApp extends StatelessWidget {
  /// Construit l'application.
  const OneViewrExampleApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'one_viewr_flutter — démonstration',
    theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal)),
    home: const DemoPage(),
  );
}

/// Écran unique : un champ par paramètre, un bouton par commande.
class DemoPage extends StatefulWidget {
  /// Construit la page.
  const DemoPage({super.key});

  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  final TextEditingController _publicKey = TextEditingController();
  final TextEditingController _apiBase = TextEditingController();
  OneViewrEnv _env = OneViewrEnv.local;
  bool _booted = false;

  final TextEditingController _externalId = TextEditingController(
    text: 'demo-user-1',
  );
  final TextEditingController _email = TextEditingController();
  final TextEditingController _phone = TextEditingController();
  final TextEditingController _language = TextEditingController(text: 'fr');
  final TextEditingController _firstName = TextEditingController();
  final TextEditingController _lastName = TextEditingController();
  // Champs métier d'`identify` (mêmes clés que le SDK JS), tous optionnels.
  final TextEditingController _birthDate = TextEditingController();
  final TextEditingController _country = TextEditingController();
  final TextEditingController _region = TextEditingController();
  final TextEditingController _city = TextEditingController();
  final TextEditingController _district = TextEditingController();
  final TextEditingController _accountValidatedAt = TextEditingController();
  final TextEditingController _accountCreation = TextEditingController();
  final TextEditingController _plan = TextEditingController();
  final TextEditingController _traits = TextEditingController();
  final TextEditingController _hash = TextEditingController();
  bool _identified = false;

  final TextEditingController _eventName = TextEditingController(
    text: 'page_viewed',
  );
  final TextEditingController _pushToken = TextEditingController();
  final TextEditingController _screen = TextEditingController(text: 'panier');
  final TextEditingController _deepLink = TextEditingController(
    text: 'https://app.exemple.com/offre?utm_source=meta&utm_campaign=123',
  );

  final List<String> _log = <String>[];

  @override
  void dispose() {
    _publicKey.dispose();
    _apiBase.dispose();
    _externalId.dispose();
    _email.dispose();
    _phone.dispose();
    _language.dispose();
    _firstName.dispose();
    _lastName.dispose();
    _birthDate.dispose();
    _country.dispose();
    _region.dispose();
    _city.dispose();
    _district.dispose();
    _accountValidatedAt.dispose();
    _accountCreation.dispose();
    _plan.dispose();
    _traits.dispose();
    _hash.dispose();
    _eventName.dispose();
    _pushToken.dispose();
    _deepLink.dispose();
    _screen.dispose();
    super.dispose();
  }

  void _append(String message) {
    setState(() => _log.insert(0, message));
  }

  static String _emptyToNull(String value) => value.trim();

  /// Valeur saisie, ou `null` si le champ est vide : `identify` n'envoie
  /// jamais un champ vide.
  static String? _optional(TextEditingController controller) {
    final value = controller.text.trim();
    return value.isEmpty ? null : value;
  }

  /// `traits` saisi en JSON (objet), ou `null` si le champ est vide. Un JSON
  /// invalide fait échouer `identify` avec un message dans le journal.
  Map<String, dynamic>? _traitsValue() {
    final raw = _traits.text.trim();
    if (raw.isEmpty) return null;
    final decoded = jsonDecode(raw);
    if (decoded is! Map<String, dynamic>) {
      throw const FormatException(
        'traits doit être un objet JSON, ex. {"segment": "vip"}',
      );
    }
    return decoded;
  }

  Future<void> _run(String label, Future<void> Function() action) async {
    try {
      await action();
      _append('✅ $label');
    } on Object catch (error) {
      _append('❌ $label : $error');
    }
  }

  Future<void> _boot() => _run('boot', () async {
    await OneViewr.boot(
      publicKey: _publicKey.text.trim(),
      env: _env,
      apiBase: _apiBase.text.trim().isEmpty ? null : _apiBase.text.trim(),
      // Démonstrateur : lit le presse-papiers sur iOS pour exercer les liens
      // d'acquisition différés (iOS 16 demande alors « Autoriser le collage ? »).
      readIosClipboard: true,
      // Valeur par défaut, écrite pour être vue : enregistre l'installation
      // auprès d'Apple au premier lancement iOS. `false` si un autre SDK gère
      // les valeurs de conversion. Clés à ajouter : ios/Runner/Info.plist.
      appleAdAttribution: true,
    );
    setState(() => _booted = true);
  });

  Future<void> _identify() => _run('identify', () async {
    final result = await OneViewr.identify(
      externalId: _emptyToNull(_externalId.text).isEmpty
          ? null
          : _externalId.text.trim(),
      email: _emptyToNull(_email.text).isEmpty ? null : _email.text.trim(),
      phone: _emptyToNull(_phone.text).isEmpty ? null : _phone.text.trim(),
      language: _language.text.trim(),
      firstName: _firstName.text.trim().isEmpty ? null : _firstName.text.trim(),
      lastName: _lastName.text.trim().isEmpty ? null : _lastName.text.trim(),
      birthDate: _optional(_birthDate),
      country: _optional(_country),
      region: _optional(_region),
      city: _optional(_city),
      district: _optional(_district),
      accountValidatedAt: _optional(_accountValidatedAt),
      accountCreation: _optional(_accountCreation),
      plan: _optional(_plan),
      traits: _traitsValue(),
      hash: _optional(_hash),
    );
    setState(() => _identified = true);
    _append(
      'ℹ️ identify → externalId: ${result.externalId}, email: '
      '${result.email}, phone: ${result.phone}',
    );
  });

  Future<void> _track() => _run(
    'track(${_eventName.text})',
    () => OneViewr.track(_eventName.text.trim()),
  );

  // L'application réelle branche ici son propre paquet de géolocalisation.
  // L'exemple ne demande aucune permission et ne lit aucune position.
  Future<void> _sendTestLocation() => _run('trackLocation', () async {
    final result = await OneViewr.trackLocation(
      latitude: 4.05080,
      longitude: 9.70450,
      accuracyMeters: 18.5,
      source: 'example',
    );
    _append('   → $result');
  });

  Future<void> _sendTestBatch() => _run('trackLocations(5)', () async {
    final now = DateTime.now().toUtc();
    final result = await OneViewr.trackLocations(
      List<LocationSample>.generate(
        5,
        (index) => LocationSample(
          latitude: 4.05080 + index * 0.0001,
          longitude: 9.70450 + index * 0.0001,
          accuracyMeters: 18.5,
          occurredAt: now,
          source: 'example',
        ),
      ),
    );
    _append('   → $result');
  });

  Future<void> _registerPushToken() => _run(
    'registerPushToken',
    () => OneViewr.registerPushToken(_pushToken.text.trim()),
  );

  Future<void> _unregisterPushToken() => _run(
    'unregisterPushToken',
    () => OneViewr.unregisterPushToken(_pushToken.text.trim()),
  );

  Future<void> _reset() => _run('reset', () async {
    await OneViewr.reset();
    setState(() => _identified = false);
  });

  // Écran courant pour les messages in-app à « Écrans ciblés » : avec le
  // contexte, la vérification des messages repart aussitôt.
  Future<void> _setScreen() => _run(
    'setScreen(${_screen.text.trim()})',
    () => OneViewr.setScreen(_screen.text, context: context),
  );

  // Simule un lien profond reçu par l'application : dans une vraie
  // application, ce serait `AppLinks().uriLinkStream.listen(...)`.
  Future<void> _recordDeepLink() => _run(
    'recordDeepLink',
    () => OneViewr.recordDeepLink(Uri.parse(_deepLink.text.trim())),
  );

  // Une vraie application passerait `link.appRoute` à son routeur ; ici on
  // l'affiche dans le journal.
  Future<void> _deferredDeepLink() => _run('deferredDeepLink', () async {
    final link = await OneViewr.deferredDeepLink();
    _append(
      link == null
          ? '   → rien à remettre (installation organique, déjà remis…)'
          : '   → ${link.status.name} ${link.code ?? ''} '
                'route=${link.appRoute} params=${link.appParams}',
    );
  });

  Future<void> _resolveDeepLink() => _run('resolveDeepLink', () async {
    final link = await OneViewr.resolveDeepLink(
      Uri.parse(_deepLink.text.trim()),
    );
    _append(
      link == null
          ? '   → pas un lien d\'acquisition Oneviewr (ou serveur injoignable)'
          : '   → ${link.status.name} route=${link.appRoute} '
                'params=${link.appParams}',
    );
  });

  // Bouton de test minimal, pas une fonctionnalité complète de cet exemple :
  // il exerce `checkInAppMessages` contre un vrai backend (local ou de
  // production), la seule chose qu'aucun test avec un faux serveur ne peut
  // vérifier — le rendu WebView réel et le comportement de `url_launcher`
  // sur un vrai appareil/simulateur.
  Future<void> _checkInAppMessages() =>
      _run('checkInAppMessages', () => OneViewr.checkInAppMessages(context));

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(title: const Text('one_viewr_flutter — démonstration')),
    body: ListView(
      padding: const EdgeInsets.all(16),
      children: <Widget>[
        _SectionTitle('1. boot — obligatoire avant tout le reste'),
        _field(_publicKey, 'publicKey (pk_…, requis)'),
        DropdownButtonFormField<OneViewrEnv>(
          initialValue: _env,
          decoration: const InputDecoration(
            labelText: 'Environnement',
            border: OutlineInputBorder(),
          ),
          items: <DropdownMenuItem<OneViewrEnv>>[
            for (final env in OneViewrEnv.values)
              DropdownMenuItem<OneViewrEnv>(value: env, child: Text(env.name)),
          ],
          onChanged: (value) => setState(() => _env = value ?? _env),
        ),
        _field(_apiBase, 'apiBase (optionnel — prioritaire sur env)'),
        const SizedBox(height: 8),
        FilledButton(
          onPressed: _publicKey.text.trim().isEmpty ? null : _boot,
          child: const Text('boot()'),
        ),
        const Divider(height: 32),
        _SectionTitle('2. identify — au moins un identifiant + language'),
        _field(_externalId, 'externalId'),
        _field(_email, 'email'),
        _field(_phone, 'phone'),
        _field(_language, 'language (ISO 639-1, requis)'),
        _field(_firstName, 'firstName'),
        _field(_lastName, 'lastName'),
        const SizedBox(height: 8),
        Text(
          'Champs métier — optionnels, rangés dans les attributs du contact',
          style: Theme.of(context).textTheme.labelLarge,
        ),
        _field(_birthDate, 'birthDate (AAAA-MM-JJ, ex. 1993-02-25)'),
        _field(_country, 'country (code ISO, ex. CM)'),
        _field(_region, 'region (ex. Littoral)'),
        _field(_city, 'city (ex. Douala)'),
        _field(_district, 'district (ex. Bonamoussadi)'),
        _field(
          _accountValidatedAt,
          'accountValidatedAt (ex. 2026-09-14T10:00:00Z)',
        ),
        _field(_accountCreation, 'accountCreation (ex. 2023-12-14T10:00:00Z)'),
        _field(_plan, 'plan (ex. STANDARD)'),
        _field(
          _traits,
          'traits — autres attributs en JSON, ex. {"segment": "vip"}',
        ),
        _field(
          _hash,
          'hash — HMAC calculé par VOTRE serveur, si la vérification '
          "d'identité est activée",
        ),
        FilledButton(
          onPressed: _booted ? _identify : null,
          child: const Text('identify()'),
        ),
        const Divider(height: 32),
        _SectionTitle('3. track — nécessite un identify() réussi'),
        _field(_eventName, 'eventName'),
        FilledButton(
          onPressed: _identified ? _track : null,
          child: const Text('track()'),
        ),
        const Divider(height: 32),
        _SectionTitle('4. Positions — fournies par l’application hôte'),
        FilledButton(
          onPressed: _identified ? _sendTestLocation : null,
          child: const Text('Envoyer une position de test (Douala)'),
        ),
        OutlinedButton(
          onPressed: _identified ? _sendTestBatch : null,
          child: const Text('Envoyer un lot de 5'),
        ),
        const Divider(height: 32),
        _SectionTitle('5. registerPushToken / unregisterPushToken'),
        _field(_pushToken, 'token FCM/APNs (obtenu par votre application)'),
        Row(
          children: <Widget>[
            Expanded(
              child: FilledButton(
                onPressed: _identified ? _registerPushToken : null,
                child: const Text('registerPushToken()'),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: OutlinedButton(
                onPressed: _booted ? _unregisterPushToken : null,
                child: const Text('unregisterPushToken()'),
              ),
            ),
          ],
        ),
        const Divider(height: 32),
        _SectionTitle('6. checkInAppMessages — nécessite un identify() réussi'),
        FilledButton(
          onPressed: _identified ? _checkInAppMessages : null,
          child: const Text('checkInAppMessages()'),
        ),
        const Divider(height: 32),
        _SectionTitle('7. reset — oublie l\'identité sur CET appareil'),
        OutlinedButton(
          onPressed: _booted ? _reset : null,
          child: const Text('reset()'),
        ),
        const Divider(height: 32),
        _SectionTitle(
          '8. recordDeepLink — source d\'acquisition (dernier contact, '
          'envoyé avec le track() suivant)',
        ),
        _field(_deepLink, 'lien reçu par l\'application'),
        OutlinedButton(
          onPressed: _booted ? _recordDeepLink : null,
          child: const Text('recordDeepLink()'),
        ),
        OutlinedButton(
          onPressed: _booted ? _resolveDeepLink : null,
          child: const Text('resolveDeepLink() — où mène ce lien ?'),
        ),
        const _SectionTitle(
          '7 bis. deferredDeepLink — le lien cliqué avant l\'installation '
          '(une seule remise)',
        ),
        OutlinedButton(
          onPressed: _booted ? _deferredDeepLink : null,
          child: const Text('deferredDeepLink()'),
        ),
        const Divider(height: 32),
        _SectionTitle(
          '9. setScreen — écran courant (messages in-app à « Écrans '
          'ciblés »)',
        ),
        _field(
          _screen,
          'nom ou chemin de l\'écran, ex. panier ou /produits/42',
        ),
        OutlinedButton(
          onPressed: _booted ? _setScreen : null,
          child: const Text('setScreen()'),
        ),
        const Divider(height: 32),
        const _SectionTitle('Journal'),
        for (final line in _log)
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 2),
            child: Text(line, style: const TextStyle(fontFamily: 'monospace')),
          ),
      ],
    ),
  );

  Widget _field(TextEditingController controller, String label) => Padding(
    padding: const EdgeInsets.symmetric(vertical: 6),
    child: TextField(
      controller: controller,
      onChanged: (_) => setState(() {}),
      decoration: InputDecoration(
        labelText: label,
        border: const OutlineInputBorder(),
      ),
    ),
  );
}

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.text);

  final String text;

  @override
  Widget build(BuildContext context) => Padding(
    padding: const EdgeInsets.only(bottom: 8),
    child: Text(text, style: Theme.of(context).textTheme.titleMedium),
  );
}
0
likes
130
points
51
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Client Flutter embarquable pour Oneviewr CRM — identify/track/registerPushToken et source d'acquisition via une clé publique non secrète. Port direct de @oneviewr/js.

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

cryptography, dio, flutter, flutter_secure_storage, shared_preferences, url_launcher, webview_flutter

More

Packages that depend on one_viewr_flutter

Packages that implement one_viewr_flutter