one_viewr_flutter 0.1.0 copy "one_viewr_flutter: ^0.1.0" to clipboard
one_viewr_flutter: ^0.1.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.

one_viewr_flutter #

Client Flutter embarquable pour Oneviewr CRM : enregistrer/identifier un contact (identify), envoyer des événements de tracking (track, signupProgress pour suivre une inscription), et faire remonter un token push FCM/APNs (registerPushToken) — port direct du SDK JS public @oneviewr/js, avec la même philosophie de sécurité (une clé publique, non secrète, jamais la clé marchand X-Merchant-Key) et les patrons d'architecture d' onex_client_plugin_flutter (façade statique unique, résolution d'environnement, client REST séparé de l'état, stockage local avec repli mémoire).

Depuis la Phase 5 du canal IN_APP, ce paquet affiche aussi les messages in-app d'un Broadcast/Journey (checkInAppMessages) — voir la section dédiée plus bas et "Rupture de doctrine" ci-dessous.

Installation #

dependencies:
  one_viewr_flutter:
    path: ../one_viewr_flutter # ou une source git/pub une fois publié

Démarrage (boot) #

import 'package:one_viewr_flutter/one_viewr_flutter.dart';

await OneViewr.boot(
  publicKey: 'pk_votre_cle_publique', // REQUIS — voir "Sécurité" plus bas
  env: OneViewrEnv.prod,              // prod (défaut, https://api.oneviewr.com) | local (http://localhost:8081)
  // apiBase: '',                     // URL absolue hors table, prioritaire sur env
);

apiBase explicite > env > défaut compilé (prod) — même précédence que le SDK JS. boot doit être awaité avant tout autre appel : contrairement au web (localStorage synchrone), charger l'identité déjà persistée sur l'appareil est asynchrone ici.

identify — enregistrer/identifier un contact #

final result = await OneViewr.identify(
  externalId: 'user-12345', // l'identifiant de VOTRE système — au moins un des trois requis
  email: 'client@exemple.com',
  phone: '+221771234567',
  language: 'fr',           // REQUIS — voir plus bas
  firstName: 'Awa',
  lastName: 'Diop',
  birthDate: '1990-05-12',  // AAAA-MM-JJ — voir plus bas
  country: 'SN',            // code pays ISO à 2 lettres
  accountCreation: '2026-09-12T08:30:00Z', // création du compte, UTC ISO 8601
  traits: {'gender': 'FEMALE', 'signupSource': 'landing-page'}, // fusionné dans les attributs du contact
  hash: 'a3f9…',            // voir "Vérification d'identité" plus bas
);

birthDate : AAAA-MM-JJ (une date ISO complète, comme DateTime.toIso8601String(), est acceptée : seule la date telle qu'écrite est gardée). Elle est rangée là où la lisent l'import CSV, la fiche client et les parcours d'anniversaire (birthday). Une date mal formée lève ArgumentError avant tout appel réseau. Les autres champs métier (country, region, city, district, accountValidatedAt, accountCreation, plan) et traits vont dans les attributs du contact ; accountCreation/accountValidatedAt ne servent de date de création du contact qu'en UTC ISO 8601.

Au moins un de externalId/email/phone est requis — sinon identify échoue explicitement, côté client, sans appel réseau. Le contact résultant est mémorisé sur l'appareil (shared_preferences, avec repli mémoire si le stockage est bloqué) : les appels track/registerPushToken suivants n'ont pas besoin de répéter les identifiants.

language est obligatoire (code ISO 639-1, ex. 'fr', 'en') — ce n'est pas un champ décoratif : c'est exactement la valeur lue côté backend (JourneyActivitiesImpl#resolveContactLanguage, Contact.attributes.language) pour décider dans quelle langue un Journey envoie un message à ce contact.

track — envoyer un événement #

await OneViewr.track('page_viewed', properties: {'path': '/pricing'});
await OneViewr.track('purchase', properties: {'amount': 42}, source: 'ios_app');

Nécessite un identify préalable (dans la session courante ou une précédente, via le stockage persistant) — sinon échoue explicitement avant tout appel réseau. Un eventId (UUID v4) est généré automatiquement pour l'idempotence côté serveur.

Positions #

L'application hôte fournit elle-même les positions ; le SDK ne demande aucune permission, ne lit aucune position et ne tourne pas en arrière-plan.

final result = await OneViewr.trackLocation(
  latitude: 4.05080,
  longitude: 9.70450,
  accuracyMeters: 18.5,
  source: 'gps',
);
await OneViewr.trackLocations(samples); // lots de 100 automatiquement

Après une coupure réseau, les échantillons sont conservés au maximum 24 heures dans une file AES-GCM plafonnée à 500 éléments. La clé est conservée dans le stockage sécurisé de la plateforme et le contenu chiffré dans les préférences. Le résultat indique accepted, queued, les codes rejected et persisted. Les risques résiduels de toute file locale sont un crash avant persistance, une sauvegarde du téléphone ou un appareil partagé.

signupProgress — suivre une inscription étape par étape #

Pour voir qui commence à s'inscrire sans finir, où chacun décroche, et le relancer par un Journey :

// 1. Dès que la personne a saisi son e-mail OU son téléphone (pas encore d'id utilisateur) :
await OneViewr.identify(phone: '+237690000000', language: 'fr');

// 2. À chaque étape (attemptId : nouveau à chaque recommencement depuis zéro) :
await OneViewr.signupProgress(step: 'phone', stepNumber: 1, attemptId: attemptId);
await OneViewr.signupProgress(step: 'otp', stepNumber: 2, attemptId: attemptId,
    properties: {'method': 'sms'});

// 3. Inscription terminée : le MÊME téléphone/e-mail + votre id utilisateur, puis signup
await OneViewr.identify(externalId: 'user-12345', phone: '+237690000000', language: 'fr');
await OneViewr.track('signup');

Équivaut à track('signup_progress', properties: {'step': …, 'step_number': …, 'attempt_id': …, ...properties}) — même contrat que la commande signupProgress du SDK JS, mêmes règles que track (identify préalable). step est obligatoire (un nom stable d'une version à l'autre) ; stepNumber est son rang, à partir de 1 ; attemptId change à chaque fois que la personne recommence depuis zéro.

  • identify exige language, y compris ce premier appel fait avec le téléphone ou l'e-mail seul.
  • La fin de l'inscription, c'est track('signup') — jamais une propriété completed. Des étapes reçues après un signup ne rouvrent pas l'inscription.
  • Renvoyez le même téléphone ou e-mail à l'identify final avec votre externalId : c'est ce qui rattache votre id au contact créé pendant les étapes ; un externalId seul créerait un doublon sans l'historique.

Source d'acquisition — d'où vient cet appareil #

Activée par défaut, sans rien à appeler de plus que boot : le paquet note d'où vient l'installation, puis envoie le premier contact (fixé une fois, jamais remplacé) et le dernier contact (la dernière visite porteuse d'un paramètre marketing) dans chaque identify, et le dernier contact dans context.attribution de chaque track. Même format et mêmes règles que le SDK JS (section « Source d'acquisition » de son README) : côté Oneviewr, la fiche client, les segments et le rapport Acquisition les lisent de la même façon, qu'un contact arrive par le site ou par l'application.

Ce que le paquet relève, selon la plateforme :

Plateforme Au premier démarrage Ce qu'on en apprend
Android Play Install Referrer : le paramètre referrer du lien qui a mené à Google Play UTM du lien, gclid des campagnes d'application Google Ads (la campagne exacte est retrouvée côté serveur), installation depuis la fiche du magasin (google-play / organic), publicité Meta (campagne, ensemble de publicités et publicité déchiffrés côté serveur, voir plus bas), lien d'acquisition Oneviewr (ov_link_code)
iOS Jeton Apple AdServices, échangé auprès d'Apple ; en option (readIosClipboard), le jeton d'un lien d'acquisition Oneviewr copié dans le presse-papiers Campagne, groupe d'annonces et mot-clé Apple Search Ads ; le lien d'acquisition cliqué avant l'installation
Web L'adresse de la page, à chaque démarrage (comme le SDK JS) UTM et identifiants de clic de l'URL

Aucune permission ni consentement au suivi publicitaire (ATT) n'est demandé : ni le referrer Google Play ni le jeton AdServices n'utilisent d'identifiant publicitaire. (Seule exception, à activer, la lecture du presse-papiers iOS pour les liens d'acquisition différés : iOS 16 demande alors d'autoriser le collage — voir plus bas.) Cette recherche tourne en arrière-plan — boot ne l'attend pas — et un échec passager (service Google Play indisponible, réseau coupé) est retenté aux démarrages suivants, trois fois au plus.

Le paquet ne capte pas lui-même les liens qui ouvrent l'application : c'est le système qui les livre à votre application, et elle a déjà son moyen de les lire. Transmettez-les après boot, y compris celui qui a lancé l'application :

await OneViewr.boot(publicKey: 'pk_…');

// Paquet app_links : le flux livre aussi le lien de lancement.
AppLinks().uriLinkStream.listen(OneViewr.recordDeepLink);

Avec go_router, le lien passe par redirect (un lien relatif comme /promo?utm_source=… convient) ; les navigations internes, sans paramètre marketing, sont ignorées :

GoRouter(
  redirect: (context, state) {
    OneViewr.recordDeepLink(state.uri);
    return null;
  },
  routes: [...],
);

Pour que le système livre les liens https:// à l'application plutôt qu'au navigateur, déclarez-les une fois (configuration de l'application, pas du paquet) :

  • Android (App Links) : un intent-filter android:autoVerify="true" sur votre domaine dans AndroidManifest.xml, et le fichier /.well-known/assetlinks.json publié sur ce domaine ;
  • iOS (liens universels) : la capacité Associated Domains (applinks:votre-domaine) et le fichier /.well-known/apple-app-site-association publié sur ce domaine ;
  • un schéma propre (monapp://…) marche sans fichier sur le domaine, mais une régie ne l'accepte pas toujours comme lien d'annonce.

Les liens de vos annonces portent alors vos paramètres, par exemple https://app.boutique.com/promo?utm_source=meta&utm_campaign={{campaign.id}}. Si l'application n'est pas installée, le même lien ouvre votre site (le SDK JS en relève la source) ou mène au magasin.

Ce que fait recordDeepLink du lien : il ne lit que les paramètres utm_* et les identifiants de clic (gclid, gbraid, wbraid, fbclid, ttclid, msclkid), écarte toute valeur qui ressemble à une adresse e-mail ou à un numéro de téléphone, et garde le chemin sans la requête comme page d'arrivée (pour un schéma propre, l'hôte en fait partie : monapp://produit/42 → /produit/42). Un lien porteur de paramètres marketing devient le dernier contact ; il devient aussi le premier s'il arrive avant que celui-ci soit fixé et que l'installation ne vienne d'aucune campagne. Un lien sans paramètre marketing est ignoré. Aucun appel réseau : le dernier contact part avec le track suivant.

Un lien d'acquisition Oneviewr (https://onvw.link/AbC12345, https://go.boutique.com/l/AbC12345) est un signal à lui seul, même sans utm_* : son code est relevé et le serveur en déduit la source, le canal et la campagne. Pour savoir en plus où il mène dans l'application, voir la section suivante.

Un lien créé dans Acquisition › Liens peut mener à un écran précis de l'application (/produit/42). Deux cas, deux méthodes — ce paquet ne navigue jamais lui-même : il vous rend la destination, votre routeur décide.

1. L'application n'est pas encore installée : l'utilisateur clique, passe par le magasin, installe, ouvre. Au premier lancement :

await OneViewr.boot(publicKey: 'pk_…', readIosClipboard: true);

final link = await OneViewr.deferredDeepLink();
if (link != null && link.isResolved && link.appRoute != null) {
  router.go(link.appRoute!); // ex. /produit/42 — link.appParams, link.fallbackUrl…
}

deferredDeepLink rend null quand il n'y a rien à remettre (installation organique, lien déjà remis — il ne l'est qu'une fois —, source d'acquisition désactivée) ou quand la recherche n'est pas finie au bout de timeout (5 s par défaut ; rien n'est perdu, rappelez-la plus tard). Sinon, un DeepLink dont status vaut resolved, notFound, expired ou invalid — les échecs sont rendus tels quels pour que vous puissiez les consigner.

  • Android : le serveur ajoute ov_link_code au referrer du lien vers Google Play, que le Play Install Referrer relit au lancement. Silencieux, sans permission. Google Play seulement (ni Huawei, ni Amazon, ni installation hors magasin).
  • iOS : la page de redirection copie un jeton signé dans le presse-papiers avant d'envoyer vers l'App Store, que l'application relit au premier lancement. Désactivé par défaut (readIosClipboard: false) : depuis iOS 16, lire le presse-papiers affiche « Autoriser le collage ? », et une application qui n'utilise pas ces liens ne doit pas l'infliger à ses utilisateurs. Activez-le si vos liens mènent à l'App Store. Seul un texte de la forme exacte d'un jeton Oneviewr est gardé — tout autre contenu est ignoré et ne quitte jamais l'appareil —, et ce jeton est ensuite effacé du presse-papiers. Le jeton expire dix minutes après le clic : une installation plus tardive n'est pas rattachée au clic (statut expired).

Au mieux, jamais garanti. Refuser l'invite de collage ne se distingue pas d'un presse-papiers vide (limite d'iOS), un presse-papiers écrasé entre-temps fait perdre le jeton, et Android ne couvre que Google Play. Prévoyez toujours un parcours d'arrivée par défaut. Le serveur compte chaque résolution et son statut : le taux de résolution est visible dans les statistiques du lien.

L'attribution de l'installation ne dépend pas de cet appel : le code du lien part avec identify (premier contact) que vous appeliez deferredDeepLink ou non, et readIosClipboard suffit à l'activer sur iOS.

2. L'application est déjà installée : le lien universel / App Link l'ouvre directement avec https://boutique.onvw.link/AbC12345, que votre routeur ne connaît pas. Traduisez-le :

AppLinks().uriLinkStream.listen((uri) async {
  final link = await OneViewr.resolveDeepLink(uri);
  if (link != null && link.isResolved && link.appRoute != null) {
    router.go(link.appRoute!);
  }
});

resolveDeepLink rend null si uri n'est pas un lien d'acquisition Oneviewr (/l/{code} sur n'importe quel domaine, /{code} sur onvw.link et ses sous-domaines — un chemin nu ailleurs est une route de votre application), si le serveur n'a pas pu répondre, ou si boot(attribution: false) : rien n'est envoyé avant le consentement. Il relève aussi le lien comme source d'acquisition (comme recordDeepLink). Pour que le système livre ces liens à l'application, déclarez votre sous-domaine Oneviewr (applinks:boutique.onvw.link sur iOS, intent-filter autoVerify sur Android) — l'écran Réglages › Application mobile d'Oneviewr dit quoi renseigner.

Ordre de décision du premier contact #

  1. le lien d'acquisition Oneviewr cliqué avant l'installation, quand l'installation ne le porte pas déjà (iOS : jeton du presse-papiers) ;
  2. sinon l'installation, si elle vient d'une campagne (clic publicitaire, campagne nommée, support payant — dont le lien Oneviewr d'Android) ;
  3. sinon le premier lien marqué reçu avant la décision ;
  4. sinon l'installation depuis la fiche du magasin, sans campagne ;
  5. sinon une visite directe, datée du premier démarrage.

Tant que la décision n'est pas prise, identify n'envoie aucune source : le serveur prendrait sinon le dernier contact pour le premier, et il n'écrit le premier contact d'un contact qu'une fois. identify attend au plus trois secondes la fin de la recherche ; au-delà, il part sans, et le paquet renvoie le premier contact en arrière-plan (un identify avec l'identité mémorisée, sans nom ni attribut) dès qu'il est fixé. Ce rattrapage couvre aussi une application déjà connectée qui ne rappelle pas identify au lancement.

Désactiver (consentement) #

await OneViewr.boot(publicKey: 'pk_…', attribution: false);

Rien n'est lu, rien n'est envoyé (ni recherche du lien d'acquisition différé, ni resolveDeepLink, ni enregistrement auprès d'Apple), et ce qu'un démarrage précédent avait mémorisé est effacé — même option que le SDK JS. Rappelez boot avec attribution: true une fois le consentement donné. reset() n'efface pas la source : elle décrit l'appareil, pas la personne connectée.

Relayer un outil de mesure mobile (AppsFlyer, Adjust…) #

Si votre application mesure déjà ses installations avec un tel outil, fournissez sa réponse à la place de la recherche par défaut :

class AppsFlyerInstall implements InstallAttribution {
  @override
  Future<InstallLookup> lookup() async {
    final data = await lireAttributionAppsFlyer(); // votre code
    if (data == null) return const InstallRetryLater();
    if (data.organique) return const InstallUnknown();
    return InstallFound(AttributionTouch(
      capturedAt: data.dateDuClic,
      utmSource: data.regie,          // 'meta', 'google', 'tiktok'…
      utmMedium: 'paid_social',
      utmCampaign: data.idCampagne,   // c'est lui que le rapport Acquisition rapproche
    ));
  }
}

await OneViewr.boot(publicKey: 'pk_…', installAttribution: AppsFlyerInstall());

Rapports d'installation Apple (SKAdNetwork, AdAttributionKit) #

Sur iPhone, Apple ne dit pas à l'application quelle publicité Meta, Google, TikTok… a mené à l'installation. Il envoie plus tard (un à plusieurs jours après) un rapport d'installation signé à la régie : la campagne, rien sur la personne ni l'appareil. Il en envoie une copie au développeur de l'application si celle-ci déclare où l'envoyer — c'est ce qui permet à Oneviewr de compter, par régie et par campagne, les installations iOS venues des publicités, sans outil de mesure mobile.

Ce que fait le paquet : au premier lancement, une fois par installation et en arrière-plan (boot ne l'attend pas), il appelle la méthode de « valeur de conversion » SKAdNetwork d'Apple avec la valeur 0. Sans cet appel, Apple n'envoie aucun rapport, ni à la régie ni à vous :

iOS Appel SKAdNetwork
16.1 et plus updatePostbackConversionValue(0, coarseValue: .low, lockWindow: false)
15.4 à 16.0 updatePostbackConversionValue(0)
15.0 à 15.3 updateConversionValue(0)

Apple répercute lui-même cet appel dans AdAttributionKit (le cadre qui succède à SKAdNetwork depuis iOS 17.4) : les rapports des deux cadres sont couverts, sans que le paquet appelle ni lie AdAttributionKit.

Aucun consentement au suivi publicitaire (ATT) n'est demandé, et si aucune publicité n'a mené à l'installation, Apple ignore l'appel. Un échec est retenté aux lancements suivants, trois fois au plus. Rien n'est fait avec attribution: false, ni sur Android ou le web.

// Un autre SDK (Meta, Firebase, AppsFlyer…) gère déjà vos valeurs de conversion :
await OneViewr.boot(publicKey: 'pk_…', appleAdAttribution: false);

Passez appleAdAttribution: false si un autre SDK gère déjà les valeurs de conversion de votre application : sinon les deux se marchent dessus (ce paquet pourrait remettre à 0 une valeur posée par l'autre). Les clés ci-dessous restent utiles dans ce cas : Apple envoie la copie quel que soit le code qui a fait l'appel.

Clés Info.plist à ajouter dans votre application — sans elles, Oneviewr ne reçoit rien. Les deux sont nécessaires : chacune fait envoyer les copies d'un des deux types de rapports.

<!-- SKAdNetwork : copie des rapports d'installation gagnants. -->
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://onvw.link</string>
<!-- AdAttributionKit (iOS 17.4 et plus) : copie de ses rapports gagnants. -->
<key>AdAttributionKit</key>
<dict>
	<key>AttributionCopyEndpoint</key>
	<string>https://onvw.link</string>
	<!-- Facultatif : aussi les rapports de réengagement (publicité qui ramène
	     dans l'application quelqu'un qui l'a déjà). -->
	<key>OptInForReengagementPostbackCopies</key>
	<true/>
</dict>

Dans l'onglet Info de Xcode, ce sont « AdAttributionKit - Postback Copy URL » et « AdAttributionKit - Opt in for Reengagement Postback Copies ». Écrivez l'adresse telle quelle, sans chemin : Apple n'en garde que le domaine et envoie les rapports à https://onvw.link/.well-known/skadnetwork/report-attribution/ et https://onvw.link/.well-known/appattribution/report-attribution/. Une seule adresse pour toutes les applications : Oneviewr reconnaît la vôtre à son identifiant App Store, celui de l'adresse App Store renseignée dans Oneviewr (Application mobile & liens).

Ce qui reste impossible #

  • iOS, installations Meta, Google Ads ou TikTok, par personne : Apple ne donne rien par utilisateur — seulement les rapports d'installation, groupés par campagne et différés (voir plus haut). Ces contacts arrivent en visite directe, sauf s'ils ouvrent ensuite l'application par un lien marqué, ou si un outil de mesure mobile est relayé.
  • Android, publicités Meta sans clé enregistrée : Meta chiffre le détail du clic dans le referrer, et la clé ne doit pas vivre dans l'application. Le paquet transmet le bloc chiffré tel quel (metaInstallReferrer) ; le serveur l'ouvre avec la clé que le marchand enregistre dans Oneviewr (Paramètres › Sources publicitaires › « Installations Android venues de Meta », copiée depuis Meta for Developers › Paramètres › De base › Android › Install Referrer Decryption Key) et en tire la campagne, l'ensemble de publicités et la publicité. Sans cette clé, la source (meta) et le support payant sont connus, la campagne non.
  • Liens profonds différés (un lien qui survit à l'installation depuis le magasin) : voir deferredDeepLink plus haut — au mieux, jamais garanti. Android : Google Play seulement ; iOS : presse-papiers (invite de collage d'iOS 16, jeton valable dix minutes). Pour les liens qui ne sont pas des liens d'acquisition Oneviewr, le paquet n'a pas de mécanisme différé : seul le referrer Google Play les porte.

Mise en place côté application #

Le paquet embarque désormais du code natif (android/, ios/) : après l'avoir ajouté ou mis à jour, reconstruisez l'application (un simple hot restart ne charge pas le code natif — la recherche est alors retentée au démarrage suivant). Sur iOS, pod install (ou Swift Package Manager) ajoute les frameworks AdServices et StoreKit. Aucune permission à déclarer ; seules les clés Info.plist des rapports d'installation Apple sont à ajouter (voir plus haut).

registerPushToken — enregistrer un token push #

await OneViewr.registerPushToken(fcmToken); // platform déduite automatiquement (iOS/Android/web)

// Identifiant d'appareil imposé plutôt que celui du paquet — `FutureOr`, donc
// un Future convient tel quel, sans l'attendre vous-même :
await OneViewr.registerPushToken(
  fcmToken,
  deviceId: FirebaseInstallations.instance.getId(),
);

deviceId identifie l'appareil, pas le contact (ContactPushToken côté backend) : c'est ce qui permet à une rotation de token FCM — normale, périodique — de mettre à jour la ligne existante au lieu d'en créer une nouvelle à chaque fois.

Le laisser de côté est le cas normal : le paquet envoie alors le sien, généré une fois puis persisté, qui survit à reset() (il décrit l'appareil, pas la personne connectée dessus). Ne le renseignez que pour imposer le vôtre — identifiant de parc, device_info_plus, installation Firebase.

Son type est un FutureOr<String>? et non un String? parce que ces sources sont asynchrones : sans cela vous devriez les attendre vous-même avant d'arriver ici, donc les résoudre même lorsque l'appel échoue plus tôt sur une identité manquante. La valeur est résolue au moment de l'appel, une fois les gardes passées. Une chaîne vide vaut « pas d'identifiant » — et ne retombe pas sur celui du paquet, pour ne pas vous cacher qu'une source est vide.

Ce paquet ne gère PAS l'obtention du token lui-même : votre application doit intégrer Firebase Cloud Messaging (projet Firebase du marchand) et appeler getToken() elle-même, puis transmettre la valeur ici. Nécessite un identify préalable, comme track.

Canal de notification Android — pour un affichage prioritaire #

Le backend Oneviewr envoie android.priority: HIGH par défaut, mais ce réglage ne contrôle que l'urgence de livraison (réveiller l'appareil, contourner Doze) — pas le rang d'affichage dans le tiroir de notifications. Ce rang dépend de l'importance du canal de notification (NotificationChannel), un réglage propre à l'appareil que seule votre application peut définir en le créant — Firebase ne peut pas la relever rétroactivement une fois le canal créé.

Sans un canal explicitement créé en importance haute, une notification Oneviewr peut s'afficher sous celle d'une autre application (livraison, messagerie…) même si elle est arrivée après — l'OS ne trie pas uniquement par heure d'arrivée entre applications. Créez le canal avant la première notification, avec le même identifiant que androidChannelId (réglage Oneviewr côté Paramètres > Fournisseurs > Firebase, "default" si non personnalisé) :

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

const channel = AndroidNotificationChannel(
  'default', // doit correspondre à androidChannelId
  'Notifications',
  importance: Importance.high,
);

await FlutterLocalNotificationsPlugin()
    .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()
    ?.createNotificationChannel(channel);

unregisterPushToken — désabonner un appareil #

await OneViewr.unregisterPushToken(fcmToken);

Aucune identité requise. À appeler à la déconnexion, AVANT reset — voir l'avertissement ci-dessous.

trackNotificationClick — signaler un clic sur une notification push #

Ce paquet ne peut PAS observer lui-même qu'une notification a été tapée : votre propre intégration Firebase (onMessageOpenedApp/getInitialMessage) est le seul endroit où ce tap existe. Appelez cette méthode depuis là, avec exactement le RemoteMessage.data reçu :

FirebaseMessaging.onMessageOpenedApp.listen((message) {
  OneViewr.trackNotificationClick(message.data);
});
// Et pour un tap qui a lancé l'application depuis l'état "terminée" :
final initial = await FirebaseMessaging.instance.getInitialMessage();
if (initial != null) {
  await OneViewr.trackNotificationClick(initial.data);
}

data doit porter oneviewr_source — le marqueur posé côté serveur (FirebaseClient) sur toute notification envoyée par Oneviewr — et oneviewr_message_id. C'est ce qui permet à une application qui envoie aussi ses propres notifications, ou qui embarque d'autres SDK, d'appeler cette méthode sans condition sur chaque tap : trackNotificationClick se filtre lui-même et ne lève jamais — elle renvoie false (jamais une exception) si data n'est pas d'origine Oneviewr, si boot/identify n'ont jamais été appelés, ou si l'appel réseau échoue ; true seulement si l'accusé de réception a bien atteint le backend.

⚠️ oneviewr_message_id, jamais message_id : ce dernier est un mot réservé du protocole FCM/GCM (collision avec le champ d'identifiant du message porté par l'enveloppe elle-même) — le SDK Firebase natif (Android en particulier) supprime silencieusement toute clé data nommée exactement message_id avant même que votre application ne la reçoive. Constaté en direct : une vraie notification arrivée avec oneviewr_source intact mais sans aucune trace de l'identifiant.

reset — oublier l'identité courante #

await OneViewr.reset(); // ex. à la déconnexion de VOTRE utilisateur

⚠️ reset n'agit que sur CE terminal — il oublie l'identité stockée localement, il n'appelle jamais le backend. Sur un appareil partagé, si vous ne faites que reset() à la déconnexion, l'appareil reste abonné aux notifications push du contact précédent jusqu'à ce que quelqu'un d'autre ré-enregistre son token via registerPushToken. Pour désabonner l'appareil immédiatement, appelez unregisterPushToken(token) avant reset().

reset() referme aussi le message in-app actuellement affiché, s'il y en a un : il appartenait à l'identité qu'on vient d'oublier et n'a rien à faire devant la personne suivante. Il n'est alors pas acquitté — personne ne l'a fermé, il a été retiré de l'écran — et une vérification en cours (checkInAppMessages) s'arrête d'elle-même plutôt que d'afficher le message suivant à un utilisateur désormais déconnecté.

Le clientRef (identifiant d'appareil envoyé avec chaque réclamation de message in-app) survit à reset(), lui : il décrit le terminal, pas la personne qui l'utilise à cet instant.

checkInAppMessages — afficher les messages in-app en attente #

await OneViewr.checkInAppMessages(context); // context : un BuildContext monté

Vérifie s'il existe un message in-app en attente pour l'identité courante (canal IN_APP d'un Broadcast/Journey Oneviewr — POST /in-app-messages/pending), l'affiche (MODAL, BOTTOM_SHEET, BANNER ou FULLSCREEN, selon ce que le marchand a composé dans l'éditeur), puis recommence jusqu'à ce qu'il n'y en ait plus — plusieurs messages en attente sont affichés un par un, jamais superposés.

Nécessite un identify préalable, comme track/registerPushToken — même garde, sans file de commandes pré-boot().

Les quatre formats s'affichent sur le Navigator/Overlay racine, pas sur celui dont vient le BuildContext que vous passez : un contexte d'écran d'onglet a un Navigator local, et une route « plein écran » poussée là ne couvrirait que la zone de l'onglet — votre barre de navigation resterait par-dessus le message. FULLSCREEN est en outre posé dans un Scaffold + SafeArea, donc jamais sous la barre d'état ni sous l'indicateur d'accueil.

Une seule vérification à la fois. Appelé pendant qu'une vérification est déjà en cours, l'appel rend la main en silence au lieu d'en lancer une seconde — ce n'est pas une faute de l'appelant, l'affichage demandé est déjà en route. Ce cas est courant plutôt que théorique, puisque la méthode est justement censée être rappelée à chaque retour au premier plan (voir l'encadré ci-dessous) : sans ce verrou, deux boucles réclameraient chacune un message et empileraient deux boîtes de dialogue. Un appel réellement fautif (sans boot() ou sans identify()) continue de lever, verrou ou pas.

⚠️ Contrairement à @oneviewr/js (le SDK web), identify() n'appelle PAS checkInAppMessages() automatiquement ici. Le SDK web peut le faire parce que identify() n'a besoin d'aucun contexte de rendu ; sur Flutter, checkInAppMessages a besoin d'un BuildContext monté pour afficher la WebView (Dialog/feuille du bas), et identify() n'en reçoit pas. C'est une contrainte de la plateforme, pas un oubli — c'est à l'app hôte d'appeler checkInAppMessages(context) elle-même, à deux moments : juste après un identify() réussi, et à chaque retour au premier plan de l'application (AppLifecycleState.resumed) — sinon un message activé pendant que l'app est déjà ouverte ne sera jamais montré tant que l'app n'est pas relancée.

class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _afterIdentify(); // suppose que boot()/identify() ont déjà été appelés plus tôt
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) _afterIdentify();
  }

  Future<void> _afterIdentify() => OneViewr.checkInAppMessages(context);
}

Ce paquet gère l'affichage lui-même (WebView + Dialog/feuille du bas), contrairement à identify/track/registerPushToken qui ne font que du réseau — voir "Rupture de doctrine" ci-dessous. Le clic sur le bouton d'action du message ouvre sa destination (ctaUrl) via url_launcher — http/https/mailto/deep link personnalisé, sans configuration supplémentaire côté hôte.

Blocs interactifs, écrans ciblés et sondages #

Le plugin affiche aussi les messages « version 2 » de l'éditeur : il l'annonce au serveur à chaque demande (sdkCapabilities.compilerVersion: 2), qui ne confie jamais à une application un message qu'elle ne sait pas afficher.

Bloc Dans le message
Carrousel Diapositives précédente/suivante, en boucle, points de position
Compte à rebours Jours, heures, minutes, secondes ; à zéro, le texte de fin s'affiche
Coupon « Copier » met le code dans le presse-papiers du téléphone
Sondage Choix unique (un rond devant chaque réponse, ● une fois choisie) ou multiple (un carré, ✓) ; « Envoyer » transmet les réponses, affiche le remerciement, puis le message se ferme seul (compté comme un clic)
Barre de progression Remplie d'après la valeur du contact
Vidéo Lecture native, jamais automatique

Écrans ciblés. Un message peut n'apparaître que sur certains écrans (panier, /produits/*…). Indiquez l'écran courant ; avec un context, la vérification repart aussitôt :

GoRouter(
  redirect: (context, state) {
    OneViewr.setScreen(state.uri.path, context: context);
    return null;
  },
  routes: [...],
);

Sans setScreen, seuls les messages sans ciblage d'écran sont remis. reset() ne l'efface pas : il décrit l'application, pas la personne.

Sondages. Les réponses partent d'elles-mêmes (answerInAppSurvey, positions des choix seulement : le serveur relit question et libellés dans le message). À n'appeler vous-même que si vous affichez les messages avec showInAppMessage (paramètre onAnswer).

Sécurité du rendu #

Le JavaScript est actif dans la WebView, et le reste sous contrôle :

  1. Le HTML du marchand ne contient jamais de script : le serveur (InAppHtmlSanitizer) n'en laisse passer aucun, ni aucun attribut on* ; les liens se limitent à sept actions fixes oneviewr-inapp:*, les comportements sont des marqueurs data-oneviewr-* inertes dont chaque valeur est vérifiée, images et vidéos ne viennent que d'https.
  2. Le seul code qui tourne est celui du paquet, constant, compilé dans l'application : la mesure de hauteur et les blocs interactifs. Aucune donnée du message n'y est interpolée ; il ne touche qu'aux styles, à aria-pressed et au texte d'éléments déjà présents — jamais innerHTML, eval, une URL ou le réseau (vérifié par un test).
  3. Deux canaux seulement vers l'application (OneviewrInAppHeight, OneviewrInAppAction), et l'application revalide tout ce qui en arrive : format fermé, longueurs bornées, positions de 0 à 5. Le reste est ignoré.
  4. Seul un vrai toucher agit : un clic synthétique sur un lien oneviewr-inapp:* est neutralisé. Un sondage n'est envoyé qu'une fois par affichage.
  5. Aucune navigation hors du chargement du document et de oneviewr-inapp:cta/dismiss : la boîte d'un message ne peut jamais afficher une autre page.
  6. Encodage déclaré (<meta charset="utf-8">) : les accents ne sont jamais relus en Latin-1.

Rupture de doctrine : webview_flutter et url_launcher #

Ce paquet a longtemps été zéro dépendance UI — un pur client réseau, comme @oneviewr/js. checkInAppMessages a besoin d'afficher un HTML déjà compilé (rendu identique à l'aperçu de l'éditeur, WebView côté web comme côté Flutter — voir le plan IN_APP, Phase 0) : ce n'est ni un oubli ni une extension incrémentale anodine, c'est une décision délibérée d'élargir la surface de ce paquet au-delà du réseau pur, pour ce seul canal.

webview_flutter rend le HTML ; url_launcher résout la vraie destination du bouton d'action (ctaUrl) — jamais un href littéral dans le HTML compilé, qui ne porte que des liens internes fermés (oneviewr-inapp:cta/oneviewr-inapp:dismiss, interceptés par NavigationDelegate.onNavigationRequest, sans JavaScriptChannel ni script injecté).

Sécurité #

publicKey n'est PAS un secret — elle est conçue pour être embarquée dans une application distribuée. Générée/révoquée côté staff Oneviewr (Paramètres → Clé publique), elle donne accès à EXACTEMENT trois actions (identify/track/registerPushToken) pour le marchand concerné — jamais à la lecture de données, jamais aux routes internes.

Vérification d'identité (hash) #

Sans hash, n'importe qui connaissant/devinant l'externalId d'un utilisateur pourrait appeler identify/track/registerPushToken en son nom. Si votre application manipule des données réelles d'utilisateurs connectés, activez la vérification d'identité côté Paramètres, puis calculez hash sur votre propre serveur — jamais dans l'application :

hash = hex(HMAC-SHA256(identitySecret, externalId))

Exactement le même schéma que le hash du widget de chat et du SDK JS.

Limites connues (v1, documentées — pas des oublis) #

  • Pas de mode visiteur anonyme. identify() avec un identifiant réel est requis avant track/registerPushToken, cohérent avec le modèle d'identité du CRM.
  • track() reste synchrone vis-à-vis de son effet réseau ; les positions disposent en plus d'une file chiffrée et d'un rejeu best-effort.
  • Limitation de débit côté serveur (PublicRateLimitFilter, backend) : par IP et par clé publique. 429 en cas de dépassement.

Build & tests #

flutter pub get
flutter test
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