Skip to main content

bzengage_flutter_sdk 0.7.2 copy "bzengage_flutter_sdk: ^0.7.2" to clipboard
bzengage_flutter_sdk: ^0.7.2 copied to clipboard

Official BzEngage Flutter SDK. A thin Dart layer that forwards calls to the native BzEngage Android and iOS SDKs (in-app messages, zones, push, events, consent, commerce).

Changelog #

0.7.2 — 2026-11-05 #

  • Uses the Android SDK 0.1.6 (com.bzengage:bzengage-android-sdk:0.1.6) and the iOS SDK BzEngage 0.1.5. With setInAppDebug(true), the in-app debug log now gives the actual reason a message was not shown: already shown in this session, a layout the device cannot display, or another in-app message took the slot. Display behaviour is unchanged.
  • iOS: the app's Podfile line becomes pod 'BzEngage', :podspec => 'https://bzengage.com/sdk/ios/0.1.5/BzEngage.podspec'.

0.7.1 — 2026-11-02 #

  • Uses the Android SDK 0.1.5 (com.bzengage:bzengage-android-sdk:0.1.5). Events queued while offline are no longer sent twice (or, rarely, lost) when a push notification arrives while the app is running. The iOS SDK stays at BzEngage 0.1.4 — the iOS side was not affected.
  • README: on Android, a notification tapped after only the app's screen was closed (the app process kept running) delivers its link to onPushDeeplink, not to initialPushDeeplink(); links waiting for a listener have no time limit, and the 60-second limit applies only to initialPushDeeplink().

0.7.0 — 2026-10-22 #

The SDK is now a thin layer over the native BzEngage SDKs #

This package no longer runs its own Dart implementation. Every call is forwarded to the native BzEngage Android SDK (com.bzengage:bzengage-android-sdk:0.1.4) and iOS SDK (BzEngage 0.1.4), the same way the React Native SDK works. Targeting, frequency, storage, networking, in-app rendering, push display and rules all run natively, so a Flutter app now behaves like a native app.

This is a breaking release. See the README in this package for setup.

Setup changes

  • iOS: add pod 'BzEngage', :podspec => 'https://bzengage.com/sdk/ios/0.1.4/BzEngage.podspec' and platform :ios, '15.0' to ios/Podfile. The notification service extension now uses the iOS SDK's class: import BzEngage / class NotificationService: BzEngageNotificationService {}.
  • Android: the plugin adds the BzEngage Maven repository. Apps whose settings.gradle forbids project repositories must add https://bzengage.com/sdk/android themselves.
  • The package no longer depends on http, shared_preferences, firebase_core, firebase_messaging, flutter_local_notifications, app_settings, webview_flutter, url_launcher or flutter_js. If your app uses firebase_messaging on Android, forward BzEngage pushes with BzEngageSDK.handlePushMessage() (README §11.6).
  • Data stored by 0.6.x on the device (anonymous ID, signed-in user, consent, attribution, frequency counters, offline events, seen orders) is carried over to the native SDK the first time 0.7.0 starts, so an updated app keeps the same user.

API changes (0.6.x → 0.7.0)

0.6.x 0.7.0
BzEngageSDK(baseUrl:, apiKey:, personId:, distinctId:, platform:, projectId:) await BzEngageSDK.initialize(baseUrl:, apiKey:, inAppAllowedSchemes:, inAppDebug:, linkHandling:)
sdk.personId await sdk.currentPersonId()
identify(id, properties:, anonDistinctId:) → int? identify(id, properties:) → Future<void> that completes after the server answered — read the person with await sdk.currentPersonId() right after. It never throws; on failure the person is unchanged
loadPersistedIdentity() → int? loadPersistedIdentity() → void (no longer needed — restored automatically)
setCurrentScreen(name) setScreen(name)
setRuleUserProperty(key, value) · setUserProps(map) setUserProperty(key, value)
appContext await sdk.getContext()
showAllMessages(context, onDeeplink:, onCustomEvent:) · showNextMessage(...) await sdk.showMessages() + streams onInAppDeeplink · onInAppUrlOpen · onInAppCustomAction — throws ErrorCodes.noActivity while the app is in the background (Android and iOS)
onPushDeeplink callback onPushDeeplink stream + await sdk.initialPushDeeplink()
onCustomAction onInAppCustomAction stream
commerce snapshots (synchronous) await sdk.commerce.cartSnapshot() etc.
BzenZone(name:, sdk:, placeholder:, padding:, onCtaTap:, maxHeight:) BzenZone(name:, maxHeight:, placeholder:, padding:, onHeightChanged:) — zone links arrive on the in-app streams
registerForPush() reRegisterPushToken()
sdk.commerce (CommerceApi) sdk.commerce (BzCommerce) — same functions; snapshots are now Future
setContext · updateContext · setCurrentScreen · setUserProps (synchronous) setContext · updateContext · setScreen · setUserProperty return Future<void>

New: markOpenedFrom, callSystemRule, setInAppDebug, reRegisterPushToken, BzEngageSDK.handlePushMessage, BzEngageSDK.isBzEngagePush, LinkHandling, BzEngageError / ErrorCodes.

Removed: initCustomRules, syncCustomRules, syncTriggerTemplates, enablePushAutoTracking, setPosthogApiKey, flushRuleEvents, runSystemRules, evaluateCampaignTriggers, trackRuleEvent, resolveProperties, repairAnonIdentity, capturePushLanding, fetchCampaigns, applyFrequencyGate, showMessage, fetchMessages, onEventTracked, ConsentWho, the debug… helpers, the public fields (api, ruleRuntime, ruleRepo, templateRepo, goalMemo, couponManager, triggerRuntime, channelTargetingRuntime, frequencyGuard, appOpenTracker, pushService, baseUrl, apiKey, distinctId, platform), Flutter web support, and the exported internal classes (BzEngageApi, BzAttribution, the anon_id helpers such as ensureAnonId, AppOpenTracker, the rule engine, in-app renderer and model classes). The native SDKs do this work now.

License: the SDK is licensed to BzEngage customers for use with the BzEngage service (see LICENSE). Earlier versions said MIT.

Changes in the 0.6.x line before the switch #

The entries below describe fixes made to the Dart implementation after 0.6.0 and are kept for history. In 0.7.0 that Dart code is replaced by the native SDKs.

iPhone notification buttons now appear #

A BzEngage push with action buttons used to show no buttons on iPhone. iOS only shows buttons for a registered notification category, and nothing registered one. BzEngageNotificationService now registers a category from the push's buttons (up to 3) before the notification is shown. Tapping a button opens the app and goes to that button's link, as on Android.

  • The extension now runs for every BzEngage push, not only pushes with an image. The server now marks every iPhone push so the extension runs.
  • Categories your app registered are kept.
  • Pushes from other providers get no BzEngage buttons.
  • willDeliver(content:request:) now runs for every push the extension handles, including pushes without an image. It no longer runs after the 30-second limit has passed.
  • If your app registers its own categories, read the current ones with getNotificationCategories and add yours to them. Replacing the whole list removes the buttons from BzEngage notifications already on screen.

iPhone push no longer needs Firebase — and now actually arrives #

On iOS the SDK used to register the FCM token with platform: 'apns'. The server sends apns tokens straight to Apple, so Apple rejected every one and the server switched the token off: no BzEngage push ever reached an iPhone. It also called Firebase.initializeApp() on iOS, which stops an app that has no GoogleService-Info.plist.

On iOS the SDK now uses its own native code (ios/Classes/BzenPushHub.swift) for the notification permission, the raw APNs token, the foreground banner and taps. Nothing changes on Android.

  • initPush(), registerForPush() and enablePushAutoTracking() work on iOS with no Firebase setup. The APNs entitlement (Push Notifications capability) is still required.
  • A BzEngage push that arrives while the app is open is now shown as a banner. Before this change nothing appeared.
  • Taps work while the app is open, in the background, or closed. Each tap opens onPushDeeplink and records the click and the landing once.
  • Pushes from other providers are left alone. The SDK does not show them, open them or record them.
  • If your app also uses Firebase on iOS for its own pushes, those still go to Firebase.

Apps with an image-push extension failed to build. The new iOS push code calls UIApplication.shared, which app extensions may not use, and the Notification Service Extension links this package too. That code is now marked unavailable in extensions, so the extension builds again. The README's rich-push steps also missed two required steps, the extension's Podfile entry and the build-phase order, and both are now documented.

The iOS build was broken. pubspec.yaml declared sharedDarwinSource: true, so Flutter looked for the podspec in darwin/. It lives in ios/, so pod install failed for every iOS app. The flag is removed.

A rule that keeps running too long is now taken out of service #

evaluateTrigger has always accepted a timeoutMs, but the value was never read. Marketer-authored JavaScript ran with no limit of any kind.

Every rule door — evaluateTrigger, executeGoal, executeSystem and evaluateRaw — now goes through one guard. It times the evaluation and, after three consecutive overruns of the same rule, stops calling the engine for that rule for the rest of the run. One run inside the limit clears the count, so a single stall (a GC pause, a momentarily busy device) does not take a healthy rule out of service.

Limits: 100ms for trigger, goal and system rules; 500ms for targeting template evaluation. Both match the Android SDK. A caller may pass its own value.

This is not a hard stop. An evaluation that has already begun cannot be interrupted — flutter_js exposes no interrupt handler, and moving the engine to an Isolate would not help either, because a Dart isolate blocked inside a native call does not respond to kill. So a rule with a genuinely endless loop still blocks the app the first time it runs. What the guard removes is paying that cost again and again.

Three details keep this from misfiring:

  • Overruns are only counted once per second. In-app candidates are filtered one message at a time, so the same rule can be evaluated several times in a single pass; without this, "three consecutive" would effectively mean "once".
  • The count is kept per limit. The same rule is evaluated with a 100ms limit in one place and a 500ms one in another; sharing a counter would let the lenient place clear the strict place's strikes, and the rule would never reach three.
  • Editing the rule brings it back. The counter is keyed by the rule's code, so once a marketer fixes a runaway rule and republishes it, the next sync runs it again — without that, the fix would not take effect until the app restarted.

A rule that has been taken out of service evaluates as false, so campaigns gated on it stop being shown — deliberately, since letting them through would deliver messages to people who do not match. Every decision is logged:

[bzen] rule=rule:12 stage=eval result=drop reason=quarantine_start elapsed=2409ms limit=100ms strike=3
[bzen] rule=rule:12 stage=eval result=drop reason=quarantined

Optional parameters were added to executeGoal, executeSystem, evaluateRaw and the CustomRuleRuntime constructor. Existing calls are unaffected.

Anonymous events no longer lose their identity #

Events from a not-yet-identified user could go out with person_id: 0 and no distinct_id at all. The server then substitutes the literal string "unknown", which collapses every anonymous user in the project into a single PostHog person and stops any delivery row from being written — push open and click rates read as zero.

This happened whenever the one-time anonymous-id acquisition at startup failed (storage can be briefly unavailable while an app boots); it was never retried for the rest of that run.

The identity is now re-acquired at send time, for in-app and push lifecycle events, zone impressions and clicks, and for anything replayed from the offline queue. A logged-in user is never touched, and no value is ever invented — if storage stays unavailable the event goes out exactly as before.

Two members are added for that wiring. They are not meant to be called by host apps:

  • BzEngageSDK.repairAnonIdentity() — idempotent; returns the identity in use.
  • BzEngageApi.anonIdentityRepair — the hook the SDK installs on the client.

No existing behaviour or signature changes.

0.6.0 — 2026-08-27 #

custom action buttons now reach your app #

A popup button can carry a custom action, meaning "the SDK cannot know what to do here — the app decides". It never reached the app. The renderer had no channel for it, so pressing such a button only showed a default toast. The onCustomEvent parameter looked like the way to receive it, but nothing ever called it.

Register one handler at startup and it fires from every display method:

sdk.onCustomAction = (value) {
  if (value == 'add_to_cart') { /* your app decides */ }
};

showAllMessages and showNextMessage also accept onCustomEvent now, matching onDeeplink. Previously only showMessage did — so the recommended path could not pass one at all.

Resolution order: the per-call handler, then the registered one, then the existing toast. An app that registers nothing behaves exactly as before. A handler that throws is caught; the popup stays up. Button-tap tracking is unchanged and still fires first.

showNextMessage no longer repeats within a session #

showAllMessages skipped campaigns already shown in the current app session; showNextMessage did not, so a campaign with no frequency limits reappeared on every screen change. Both now behave the same.

This changes behaviour for apps calling showNextMessage — the same message shows once per session instead of repeatedly. It also now sorts by priority and accepts onDeeplink / onCustomEvent, matching showAllMessages; the added parameters are optional and named, so existing calls compile unchanged.

logout() now clears the session record too. Without that, a campaign already shown to one user stayed suppressed for whoever logged in next on the same device.

Docs point at the path that enforces limits #

The class example recommended fetchMessages() + showMessage(), which deliberately displays whatever it is handed — the per-user frequency limits added in 0.5.0 are not applied there. An app following the example got none of them.

The example now uses showAllMessages, and every method that skips the limits says so. The README gained a section on showing messages; it previously did not mention it at all.

0.5.0 — 2026-08-26 #

In-app messages now respect their frequency limits on-device #

A campaign can carry three per-user limits — a lifetime cap, a per-day cap, and a minimum gap between showings. None of them did anything. The server has since been fixed to enforce all three at fetch time; this release adds the app-side half.

The app asks the server right before each display, so the server alone covers most cases. It cannot cover two:

  • inside the server's 5-minute response cache window — the previously fetched list is reused
  • before an impression reaches the server — the app counts at display time, the server counts when the event arrives

The SDK now keeps its own per-campaign tally and blocks in those two moments.

The app only ever blocks more, never less. A missing local tally (fresh install, new device) does not resurrect a message the server withheld. The other direction would reset every limit by reinstalling the app.

  • Tallies are per person. Switching accounts on a shared device does not inherit the previous user's cap.
  • Zone widgets are not counted. A zone banner is on screen the whole time it is mounted, so counting it would exhaust a popup's cap almost immediately.
  • Limits are read only from /inapp/messages. The same display_rules key carries something else on /inapp/campaigns, so the shape is validated and anything unexpected is treated as "no limit".
  • A backwards device clock does not lock a campaign out. Negative elapsed time (booting before NTP sync, restoring a backup) is treated as "gap satisfied" rather than "gap never elapses".
  • Campaigns without limits write nothing to storage.

New public API — DisplayRules, InAppFrequencyGuard, FrequencyBlockReason, InAppMessage.displayRules, BzEngageSDK.frequencyGuard, BzEngageSDK.applyFrequencyGate, BzEngageSDK.buildInAppEventHandler.

No host changes required. Upgrading is enough. showMessage() still displays whatever it is handed — a host that picks a message explicitly is trusted — but it now counts the impression.

Known limits. Tallies are per device, so multi-device totals are only accurate on the server. "Today" follows the device clock while the server follows the project timezone, so the two can disagree near midnight.

0.4.0 — 2026-08-18 #

App entries are now tracked automatically (app_open) #

App entry counts were always 0. On the web, posthog-js emits $pageview on its own; mobile had no equivalent, and nothing in the SDK ever reported an entry.

The receiving side already existed — the server defines app_open as the mobile counterpart of $pageview (session_start spec), and the targeting runtime counts it for visitCount. Only the emitter was missing, which is why every "visit count" targeting condition evaluated to 0.

app_open now fires on app entry — cold start and return from background — with no host app changes. Upgrading is enough.

  • 30-minute session gap. A brief exit (checking a notification) does not count as a new entry; being away for 30+ minutes does. Matches GA4's default and the session_start semantics the server already maps this event onto. The stamp is persisted, so killing and relaunching the app is judged by the same rule as a background return.
  • entry_type property — cold (process launch) or warm (background resume), so launches and sessions can be separated in analysis later without a new SDK release.
  • Held back until identity is known. personId starts at 0, and loadPersistedIdentity() is host-invoked — firing immediately would attribute entries to person 0. The entry is emitted once identify() or loadPersistedIdentity() resolves a person, and dropped if neither ever does.
  • Failures (network, storage) never propagate — analytics cannot break app startup or backgrounding.

Existing lifecycle observers are untouched; this one registers from the constructor so it works regardless of which init* the host calls.

0.3.0 — 2026-08-12 #

Zone height — 500px cap removed (breaking-ish behaviour change) #

BzenZone measured its content height in the WebView but discarded any value above 500px, leaving the 200px default. A 600px zone was therefore clipped to 200px with no error anywhere — and it looked fine in the admin editor, which made it very hard to diagnose.

Root cause of the cap: the wrapper HTML had no doctype, so the WebView rendered in quirks mode. There, a height:100% (common in content copied from popup templates) resolves against the viewport even when the parent is auto — so the very first measurement came back as the full screen height. The 500px cap was a guard against that bogus value, and it took correct content down with it.

  • Wrapper now starts with <!DOCTYPE html> (standards mode) and forces html, body { height: auto; min-height: 0 } — a height:100% zone now measures its content (21px in our test) instead of the screen (844px).
  • Height is measured with getBoundingClientRect() and tracked by ResizeObserver, so late-loading images and dynamic content are followed. Repeated identical values are no longer posted.
  • The cap is now the app's choice: BzenZone(maxHeight: ...). Default is no limit — the zone takes its content height. Only absurd values (>10000px, i.e. a measurement error) are discarded.

Upgrade note: zones that were silently clipped to 200px will now take their real height. If you relied on the clipping to bound your layout, pass maxHeight.

0.1.0 — 2026-05-14 (initial public release) #

Distribution model #

  • Adopted industry-standard Free SDK + Paid Backend pattern (matches Braze, OneSignal, Iterable, Customer.io, CleverTap, Airship, MoEngage, Insider — confirmed via 2026-05 industry survey).
  • License: MIT. Source is fully open; the hosted BzEngage platform is the paid component.

Package contents #

  • Extract BzEngageSDK from the test app (bzengage-test-ios-app/lib/) into a standalone Flutter package. Test app now depends on this package.
  • iOS BzEngageNotificationService base class under ios/Classes/ — customer apps subclass it inside their NSE target to enable rich push (image attach) with zero boilerplate.
  • BzEngageFlutterSdkPlugin stubs (iOS Swift + Android Kotlin) so Flutter recognises the package as a plugin and resolves its Cocoapod at build time.
  • Public API surfaces via package:bzengage_flutter_sdk/bzengage_flutter_sdk.dart.

Path forward #

  • Phase 1 (now): SDK lives inside the bzengage monorepo for development convenience; test app uses path: dependency.
  • Phase 2 (pre-customer): split into standalone GitHub repo + publish to pub.dev as a verified publisher.
  • Phase 3 (production): add iOS native (Swift) + Android native (Kotlin) + React Native SDKs to cover non-Flutter customers.
0
likes
140
points
519
downloads

Documentation

API reference

Publisher

verified publisherbzengage.com

Weekly Downloads

Official BzEngage Flutter SDK. A thin Dart layer that forwards calls to the native BzEngage Android and iOS SDKs (in-app messages, zones, push, events, consent, commerce).

Homepage

License

unknown (license)

Dependencies

flutter

More

Packages that depend on bzengage_flutter_sdk

Packages that implement bzengage_flutter_sdk