pilotpm_engage 0.6.0
pilotpm_engage: ^0.6.0 copied to clipboard
PilotPM Engage in-app SDK for Dart/Flutter. Event tracking (identify/track/ screen), push-token registration, in-app messages, push open/click reporting, and the Genie voice companion (a floating mic [...]
pilotpm_engage (Dart/Flutter) #
SDK for PilotPM Engage: events (identify/track/screen), push-token
registration, in-app messages, and the Genie voice companion (a floating mic
button — see below). 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 (almost) pure Dart #
The events/push/in-app core has 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. The one exception is Genie voice, which
needs a microphone and a speaker: those live behind the GenieAudioIO
interface, with record + just_audio as the default implementation.
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.
Genie voice #
A floating microphone the learner taps and speaks to. Voice only — no transcript, no text UI, no greeting on app open. Everything else (whether the button shows, what Genie knows, how it sounds, which languages) is controlled from the PilotPM workspace, so after this one integration there is no further app release to ship.
import 'package:pilotpm_engage/pilotpm_engage.dart';
import 'package:pilotpm_engage/pilotpm_engage_genie.dart';
// 1. Configure — the same call you already make for events.
await PilotPMEngage.instance.configure(
PilotPMConfiguration(apiKey: 'pk_live_…'),
);
// 2. (Optional) Personalize: hand Genie the signed-in learner's ELSA session
// token. Return null when signed out. Without this, Genie still works —
// it just cannot answer "how am I doing?".
PilotPMEngage.instance.genieIdentity(() async => session?.token);
// 3. Drop the widget into the tree, above your page (e.g. in a Stack).
Scaffold(
body: Stack(
children: [
HomePage(),
PilotPMGenieButton(workspaceSlug: 'elsa-speak'),
],
),
);
The button stays hidden until GET /api/sdk/v1/genie/config reports Genie
enabled for the workspace, and re-checks on every app resume — turning Genie
off in the workspace hides it in every installed app. It uses the workspace's
launcher icon when one is set. Position with alignment: / margin: (default
bottom-right).
If your backend already signs identity JWTs (the support-chat integration),
pass them instead: PilotPMGenieButton(identityJwtProvider: () async => jwt).
What the host app must declare (the only platform work):
- iOS
Info.plist:NSMicrophoneUsageDescription— e.g. "Talk to Genie, your learning companion." - Android
AndroidManifest.xml:<uses-permission android:name="android.permission.RECORD_AUDIO" />
Audio is recorded natively (no webview), sent to PilotPM for transcription, and never stored or logged on the device. Any Genie endpoint failure hides the button or returns it to idle — it never throws into the host.
To use your own audio stack, implement GenieAudioIO and pass it as
PilotPMGenieButton(audioIO: …); the voice loop (GenieVoiceSession) is pure
Dart and can also be driven without the widget via
PilotPMEngage.instance.genie!.createVoiceSession(audio: …).
Durable storage (recommended in production) #
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
flutter pub get
flutter analyze
flutter test