pilotpm_engage (Dart/Flutter)

Pure-Dart SDK for PilotPM Engage: events (identify/track/screen), push-token registration, and in-app messages. Talks to /api/sdk/v1/* with a per-workspace write-only ingest key. Full host-app wiring (dual-write beside Braze, push, in-app render widget): docs/engage-sdk/ELSA-FLUTTER-INTEGRATION.md.

Why pure Dart

No platform-channel code, so it drops into a Flutter app as just another analytics provider — ideal for dual-writing alongside Braze during a migration.

Usage

import 'package:pilotpm_engage/pilotpm_engage.dart';

// At startup (awaits identity + queue hydration):
await PilotPMEngage.instance.configure(
  PilotPMConfiguration(apiKey: 'pk_live_…'),
);

// On login:
PilotPMEngage.instance.identify('user-42', traits: {'plan': 'pro'});

// Anywhere (synchronous, fire-and-forget, fail-soft):
PilotPMEngage.instance.track('Lesson Completed', properties: {'score': 9});
PilotPMEngage.instance.screen('Home');
PilotPMEngage.instance.setUserAttributes({'streak_days': 12});

// On logout:
await PilotPMEngage.instance.reset();

Push notifications

The SDK stays pure-Dart — keep your existing FCM/APNs setup and just hand PilotPM the token you already have:

// when FCM/APNs gives you a token (and on every refresh):
await PilotPMEngage.instance.registerPushToken(token, platform: 'ios'); // or 'android'
// on logout / OS revocation:
await PilotPMEngage.instance.unregisterPushToken(token, platform: 'ios');

OS push-permission attribute (segmentable)

Report the OS notification-permission state so marketing can segment on it (profile field os_push_permission; e.g. a "push disabled" in-app campaign). This is the OS permission, not the marketing subscription state. With firebase_messaging (the ELSA wiring):

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:pilotpm_engage/pilotpm_engage.dart';

PilotPMPushPermission _mapPermission(AuthorizationStatus s) {
  switch (s) {
    case AuthorizationStatus.authorized:
      return PilotPMPushPermission.authorized;
    case AuthorizationStatus.denied:
      return PilotPMPushPermission.denied;
    case AuthorizationStatus.provisional:
      return PilotPMPushPermission.provisional;
    case AuthorizationStatus.notDetermined:
      return PilotPMPushPermission.notDetermined;
  }
}

// On login — attach it to the identify:
final settings = await FirebaseMessaging.instance.getNotificationSettings();
PilotPMEngage.instance.identify(
  userId,
  attributes: PilotPMUserAttributes(
    pushPermission: _mapPermission(settings.authorizationStatus),
  ),
);

// On change (after the permission prompt / returning from Settings) — re-send:
PilotPMEngage.instance.setPushPermission(
  _mapPermission(settings.authorizationStatus),
);

Applied server-side for identified users only; last write (by event time) wins.

In-app messages

Fetch eligible messages (the server applies targeting + impression caps + dismissal suppression) and report interactions:

final messages = await PilotPMEngage.instance
    .fetchInAppMessages(trigger: 'app_open', locale: 'vi'); // locale optional
for (final m in messages) {
  // render m.title / m.body / m.imageUrl / m.ctaLabel, then:
  await PilotPMEngage.instance
      .reportInAppEvent(m.id, PilotPMInAppEventType.impression);
  // on CTA tap: reportInAppEvent(m.id, PilotPMInAppEventType.click) + route m.ctaDeeplink
  // on close:   reportInAppEvent(m.id, PilotPMInAppEventType.dismiss)
}

A copy-paste reference render widget is in docs/engage-sdk/ELSA-FLUTTER-INTEGRATION.md.

The default store is in-memory (events batch within a session but don't survive an app kill). For durability across restarts, pass a shared_preferences-backed adapter:

class PrefsStore implements PilotPMStore {
  final SharedPreferences prefs;
  PrefsStore(this.prefs);
  @override
  Future<String?> getString(String k) async => prefs.getString(k);
  @override
  Future<void> setString(String k, String? v) async =>
      v == null ? prefs.remove(k) : prefs.setString(k, v);
}

await PilotPMEngage.instance.configure(config, store: PrefsStore(prefs));

Drop-in beside Braze (Elsa migration)

Elsa's AnalyticsService fans out to providers implementing AnalyticsProvider (setup/sendEvent/setUserId/setUserProperties/removeUserProperty). The adapter is ~15 lines:

class PilotPMProvider implements AnalyticsProvider {
  final _sdk = PilotPMEngage.instance;
  @override
  Future<void> setup() => _sdk.configure(PilotPMConfiguration(apiKey: '…'), store: …);
  @override
  void sendEvent(String name, {Map<String, dynamic>? params}) =>
      _sdk.track(name, properties: params);
  @override
  void setUserId(String userId) => _sdk.identify(userId);
  @override
  void setUserProperties(Map<String, dynamic> props) => _sdk.setUserAttributes(props);
  @override
  void removeUserProperty(String key) => _sdk.setUserAttributes({key: null});
}

Register it next to BrazeProvider and dual-write; validate parity on the PilotPM dashboard, then retire Braze.

Develop

cd sdk/flutter
dart pub get
dart analyze
dart test

Libraries

pilotpm_engage
PilotPM Engage in-app SDK for Dart/Flutter.