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).

BzEngage Flutter SDK #

bzengage_flutter_sdk is a thin layer over the native BzEngage Android and iOS SDKs. All decisions — targeting, frequency, storage, networking, in-app rendering, push display, rules — run inside the native SDK. The Dart layer only checks values and forwards calls, so a Flutter app behaves like a native app using the same SDK (exceptions are listed under Known limitations).

The SDK is licensed to BzEngage customers for use with the BzEngage service — see the LICENSE file in this package.

Contents #

  1. Requirements & tested versions
  2. Installation
  3. Quick start
  4. Identity
  5. Events, screens, context & user properties
  6. Goals, attribution & system rules
  7. In-app messages
  8. Zones
  9. Commerce
  10. Consent
  11. Push notifications
  12. Value rules
  13. Errors
  14. Known limitations

1. Requirements & tested versions #

Flutter 3.29+ (Dart 3.7+)
Android minSdk 21, Java 17
iOS iOS 15.0+, CocoaPods

Tested builds:

Flutter Platform Status
3.44.0 Android Tested on an emulator against a live server (in-app messages, zones, push, goals, attribution, commerce, consent)
3.47 iOS Tested on the simulator against a live server (in-app messages, zones, push, goals, consent)

2. Installation #

dependencies:
  bzengage_flutter_sdk: ^0.7.2

The package is a bridge only. The native BzEngage SDKs it uses are served from https://bzengage.com/sdk (Android: Maven repository https://bzengage.com/sdk/android · iOS: podspec, see below).

Android #

  1. Add the package (above) and run flutter pub get.

  2. The plugin adds the BzEngage Maven repository to your Gradle projects. If your android/settings.gradle uses dependencyResolutionManagement with RepositoriesMode.FAIL_ON_PROJECT_REPOS or PREFER_SETTINGS, project repositories are ignored — add the repository there yourself:

    // android/settings.gradle.kts
    dependencyResolutionManagement {
      repositories {
        maven {
          url = uri("https://bzengage.com/sdk/android")
          content { includeGroup("com.bzengage") }
        }
      }
    }
    
    // android/settings.gradle (Groovy)
    dependencyResolutionManagement {
      repositories {
        maven {
          url "https://bzengage.com/sdk/android"
          content { includeGroup "com.bzengage" }
        }
      }
    }
    

    Without it the build fails with Could not find com.bzengage:bzengage-android-sdk.

  3. Stop the app and rebuild it (flutter run). A hot restart does not load new native code.

  4. For push notifications, connect Firebase — see §11.1.

If the native plugin is not registered, every call is ignored and one warning is printed: Native plugin is not registered - all calls are ignored.

iOS #

  1. Add the package (above) and run flutter pub get.

  2. In ios/Podfile, set the platform and add the BzEngage iOS SDK inside your app target. It is served from https://bzengage.com/sdk (not from the public CocoaPods trunk), so point to its podspec:

    platform :ios, '15.0'
    
    target 'Runner' do
      use_frameworks!
      pod 'BzEngage', :podspec => 'https://bzengage.com/sdk/ios/0.1.5/BzEngage.podspec'
      flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
      # …
    end
    

    If this line is missing, pod install fails with "Unable to find a specification for BzEngage". Each bzengage_flutter_sdk release names the iOS SDK version it needs — 0.7.2 needs BzEngage 0.1.5.

  3. Run cd ios && pod install.

  4. Your AppDelegate needs no BzEngage code. If you override UIApplicationDelegate methods, call super (normal Flutter behaviour) — the plugin receives the push token and notification events through it.

3. Quick start #

import 'package:bzengage_flutter_sdk/bzengage_flutter_sdk.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final sdk = await BzEngageSDK.initialize(
    baseUrl: 'https://your-bzengage-host/bzengage-api',
    apiKey: 'cpk_…',                    // public key
    inAppAllowedSchemes: const ['myapp'], // app deep link schemes in-app messages may open
  );

  // Push — on every launch, right after initialize().
  // Read a tap that opened the closed app first (needed on iOS, see §11.5).
  final openedFromPush = await sdk.initialPushDeeplink();
  await sdk.initPush();

  // Signed-in user (skip for anonymous users)
  await sdk.identify('user@example.com', properties: {'tier': 'gold'});

  runApp(MyApp(sdk: sdk, openedFromPush: openedFromPush));
}

Then call sdk.showMessages() whenever a screen becomes visible — see §7.

Initialization rules #

  • Await initialize() before calling anything else. Calls made before it completes — including while it is still running — are ignored, not queued. In debug builds each ignored function prints one warning. Functions that return a value complete with a default instead (currentPersonId() → 0, getContext() → {}, showMessages() → [], sendGoal() → false, captureAttribution() → {}, callSystemRule() → null, initialPushDeeplink() → null, subscriptions.get() → null, setPushOptin() / setNightPushOptin() / subscriptions.set() → false, commerce snapshots → {}). Being called too early never throws.
  • Exceptions: the streams (onInAppDeeplink …) can be listened to before initialize(). BzEngageSDK.handlePushMessage() and BzEngageSDK.isBzEngagePush() work without initialize() (see §11.6). Zones placed before initialize() wait and load when it completes.
  • One SDK per process. Calling initialize() again with the same baseUrl and apiKey (for example when the Flutter engine is recreated) returns the same SDK and only applies the settings you pass (inAppAllowedSchemes, inAppDebug, linkHandling). A different baseUrl or apiKey throws a BzEngageError with ErrorCodes.alreadyInitializedWithDifferentKey — restart the app to change keys.
  • initialize() throws ErrorCodes.invalidArgument if baseUrl or apiKey is empty.

Configuration #

BzEngageSDK.initialize(
  baseUrl: 'https://…',              // BzEngage API base URL (required)
  apiKey: 'cpk_…',                   // public key (required)
  inAppAllowedSchemes: ['myapp'],    // app deep link schemes; if omitted, app deep links are blocked
  inAppDebug: true,                  // verbose in-app logs (Logcat / Xcode console)
  linkHandling: LinkHandling.sdk,    // who opens in-app links — default LinkHandling.sdk (see §7.2)
  projectId: null,                   // deprecated — legacy keys only, do not use
);

4. Identity #

await sdk.identify('user@example.com', properties: {'tier': 'gold', 'age': 31}); // properties optional
await sdk.logout();
final personId = await sdk.currentPersonId(); // 0 = anonymous (and before initialize())
  • Call identify() when the user signs in, logout() when they sign out.
  • identify() completes after the server answered, so currentPersonId() right after await identify(...) returns the new person. It never throws: if the request fails (no network, server error) it still completes and the person is unchanged.
  • The stored identity is restored automatically when the SDK starts. loadPersistedIdentity() still exists for backward compatibility and is safe to call, but you do not need it.

5. Events, screens, context & user properties #

// Custom event
await sdk.trackEvent('view_banner', {'banner_id': 3});

// Current screen name
await sdk.setScreen('Home');

// User attribute used by rules and audience conditions
await sdk.setUserProperty('membership', 'gold');

// App context — fills {{app:context.x}} placeholders in event properties
await sdk.setContext({'cart_total': 12000});     // replaces the whole context
await sdk.updateContext({'cart_total': 15000});  // changes only the given keys
final ctx = await sdk.getContext();

// Tell the SDK the app was opened from a push or a deep link ('push' | 'deeplink')
await sdk.markOpenedFrom('deeplink');

Report screens with Navigator #

Campaigns with a screen view condition are not shown until the SDK knows the current screen — if you never call setScreen(), they never appear. Report the route name on every navigation change, and ask for in-app messages for the new screen:

class BzEngageNavigatorObserver extends NavigatorObserver {
  BzEngageNavigatorObserver(this.sdk);
  final BzEngageSDK sdk;

  void _report(Route<dynamic>? route) {
    final name = route?.settings.name;
    if (name == null) return;
    sdk.setScreen(name);
    sdk.showMessages().catchError((Object _) => <int>[]);
  }

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => _report(route);

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => _report(previousRoute);

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => _report(newRoute);
}

MaterialApp(
  navigatorObservers: [BzEngageNavigatorObserver(sdk)],
  // routes need a name: RouteSettings(name: 'Cart') or named routes
);

Notes #

  • If a rule or audience condition uses a user property you never set, the condition evaluates to false — the campaign simply does not show.
  • User properties set with setUserProperty() are kept in memory only, and are cleared when the user changes (logout() or signing in as someone else). Set them again on every launch and after sign-in.
  • markOpenedFrom('push') is called automatically by the SDK for push opens; call markOpenedFrom('deeplink') yourself when the app is opened by a deep link.
  • trackEvent() and setScreen() only record. They do not display in-app messages by themselves — see §7.

6. Goals, attribution & system rules #

// Goal (conversion) not tied to an in-app message, e.g. on an order-complete screen
final sent = await sdk.sendGoal(goalId: 123, value: 9900, properties: {'plan': 'pro'}); // value and properties optional

// Click attribution from a link the app opens itself
// (wherever your app receives incoming links — for example the app_links package)
await sdk.captureAttribution(Uri.parse(incomingLink));

// System rule defined in the BzEngage console, called by its rule name
final grade = await sdk.callSystemRule('member_grade'); // String or null
  • sendGoal() returns true when the goal was sent. Each goal is sent at most once per app run; later calls return false. It throws ErrorCodes.invalidArgument if value is NaN or infinite. A goal sent this way is recorded for the goal, but is not credited to a push or in-app campaign — goals on in-app buttons are.
  • captureAttribution(uri) keeps the click parameters from the link for 30 days and attaches them to later events. Each time the app is opened with a link from your campaigns (bzen_click_id or a campaign id) it records a landing, the same way the web SDK counts every visit; the same link delivered twice in a row counts once. Links that carry only ad tags (utm_*, gclid, fbclid) are kept for attribution but not counted as a landing. It returns the click parameters — on Android only the newly captured ones ({} when they match the kept ones), on iOS the ones in this link ({} = none). Only call it for links your app opens itself — if you hand the link to a browser, the web SDK on that page counts the visit, and calling both counts the user twice.
  • callSystemRule(name) returns null when the rule does not exist, is disabled, or fails.

7. In-app messages #

7.1 Showing messages #

In-app messages are displayed only when you call showMessages(). Call it whenever a screen becomes visible (see the NavigatorObserver in §5):

try {
  final shownCampaignIds = await sdk.showMessages(); // e.g. [38199]
} on BzEngageError catch (e) {
  if (e.code == ErrorCodes.noActivity) {
    // the app was in the background — try again later
  }
}
  • Returns the IDs of the campaigns that were shown ([] if none).
  • Calling it repeatedly is safe: the SDK shows one message at a time and does not show the same campaign twice in a session.
  • One on-screen message at a time (banner or modal — highest priority first; same priority → the campaign created first). The others wait for a later showMessages() call after the current one closes, so call it again on screen changes.
  • Throws ErrorCodes.noActivity when the app has no foreground screen (in the background, or mid-transition). Retrying is up to the app.
  • For event-triggered campaigns, call trackEvent() first, then showMessages().
  • Messages are drawn natively on top of your Flutter screens. The Android back button closes the message only.
  • sdk.setInAppDebug(true) (or inAppDebug: true in initialize) prints the SDK's display decisions to Logcat / the Xcode console.

Who opens a link tapped inside an in-app message is decided by linkHandling only. Listening or not listening to the streams never changes link behavior — the streams are notifications.

Tap LinkHandling.sdk (default) LinkHandling.app
Deep link The SDK opens it inside your app only — your app receives it through its normal deep link handling. If no screen in your app handles it, the message's fallback URL is used. onInAppDeeplink gets the URL. Nothing is opened. onInAppDeeplink gets the URL — your listener must navigate.
External URL http/https URLs open in the browser. Any other scheme opens only inside your app. onInAppUrlOpen gets the URL. Nothing is opened. onInAppUrlOpen gets the URL.
Custom action onInAppCustomAction gets the value. If nothing listens, Android native message buttons show a short default notice; HTML messages and iOS only log it. Same.
  • App deep link schemes must be listed in inAppAllowedSchemes, otherwise they are blocked.
  • iOS: "inside your app only" means the link's scheme must also be registered in your app's Info.plist (CFBundleURLTypes). Other schemes are not opened, and the SDK uses the fallback URL.
  • With LinkHandling.sdk, a deep link the SDK opens reaches your app like any other incoming link — if your app uses Flutter's built-in deep linking, Flutter routes it too. Use LinkHandling.app if you want to decide yourself.
  • Click tracking is done by the SDK before your listener runs — do not track these clicks yourself.

7.3 Streams #

final subs = [
  sdk.onInAppDeeplink.listen((url) => debugPrint('deeplink $url')),
  sdk.onInAppUrlOpen.listen((url) => debugPrint('url $url')),
  sdk.onInAppCustomAction.listen((value) {
    if (value == 'open_cart') navigator.pushNamed('/cart');
  }),
];

// cleanup
for (final s in subs) {
  await s.cancel();
}
Stream Value
onInAppDeeplink URL (String)
onInAppUrlOpen URL (String)
onInAppCustomAction action value (String)
onPushDeeplink URL (String) — see §11.5
  • The streams are broadcast streams and can be listened to before initialize().
  • An in-app custom action counts as handled only while onInAppCustomAction has a listener.
  • Event names are also exported as constants: BzEngageEvents.inAppDeeplink etc.

8. Zones #

A zone shows content created for a named zone in the BzEngage editor, inline in your layout.

BzenZone(
  name: 'home_top',                 // zone name set in the editor
  maxHeight: 240,                   // optional, logical pixels — HTML zones only
  placeholder: const SizedBox(),    // optional, shown while the zone has nothing to show
  padding: const EdgeInsets.all(8), // optional, applied only while content is shown
  onHeightChanged: (h) {},          // optional; 0 = nothing to show
)
  • Height comes from the SDK. The zone starts at height 0 and grows to its content height, pushing the widgets below it down. Do not put it in a fixed-height box; give it the width you want (it fills the width it gets).
  • maxHeight applies to HTML zones only; zones built with the native element editor are always sized to their content. Omit it, or pass 0 or less, for no cap.
  • onHeightChanged is called only when the height changes.
  • If the user is not in the zone's audience, or there is no content, the height stays 0 (or your placeholder is shown) — no empty gap.
  • A zone built before initialize() completes (for example on the first screen) waits and loads as soon as the SDK is ready.
  • Changing name replaces the content. Removing the widget clears the zone.
  • The zone follows your app's theme: inside a Theme with Brightness.dark, HTML zone text defaults to the dark-mode color; without a Theme it follows the phone setting.
  • Impressions, clicks and audience checks are handled by the SDK.
  • Android and iOS only. On other platforms BzenZone is an empty box, so you can keep it in shared code.

9. Commerce #

Commerce calls follow the web SDK (Bzen.commerce.*): the same GA4 event names, an accumulated cart, and duplicate-order protection.

final item = {'item_id': 'A1', 'item_name': 'T-shirt', 'price': 9900, 'quantity': 1};

await sdk.commerce.viewItem(itemId: 'A1', itemName: 'T-shirt', price: 9900);
await sdk.commerce.viewItemList(categoryId: 'tops', categoryName: 'Tops', items: [item]);
await sdk.commerce.addToCart(items: [item], value: 9900);
await sdk.commerce.removeFromCart(items: [item], value: 9900);
await sdk.commerce.addToWishlist(items: [item], value: 9900);
await sdk.commerce.beginCheckout(items: [item], value: 9900);
await sdk.commerce.purchase(
  transactionId: 'T-1001',
  items: [item],
  value: 12400,
  currency: 'KRW',
  tax: 0,
  shipping: 2500,
  extra: {'coupon': 'WELCOME'}, // any extra event properties
);
Function Event Arguments
addToCart add_to_cart items, value, currency
removeFromCart remove_from_cart items, value, currency
viewItem view_item itemId, itemName, price, currency
viewItemList view_item_list categoryId, categoryName, items
beginCheckout begin_checkout items, value, currency
purchase purchase transactionId, items, value, currency, tax, shipping
addToWishlist add_to_wishlist items, value, currency

Every function also takes extra — keys sent as additional event properties. A named argument wins over the same key in extra.

Rules #

  • items is a list of GA4-style item maps: {'item_id', 'item_name', 'price', 'quantity', …}. Items are matched by item_id (then product_id, sku, id) to combine quantities; items with none of these are added as separate lines.
  • currency defaults to 'KRW'.
  • Duplicate orders: a second purchase with the same transactionId is ignored — no event is sent. Without a transactionId there is no duplicate check. The list of seen IDs is kept on the device across app restarts and is cleared when the user changes (logout() or signing in as someone else).
  • Cart:
    • addToCart adds value to the cart total; removeFromCart subtracts it. Items with the same item_id have their quantities combined; removing reduces the quantity and drops the item at 0.
    • Passing cart_items (list) — optionally with cart_value — in extra of addToCart / removeFromCart replaces the whole cart instead of accumulating.
    • beginCheckout replaces the cart items with items and the total with value — only the ones you pass; omitted ones keep their current value.
    • purchase does not clear the cart.
  • Calls before initialize() completes are ignored (debug warning).
  • The cart is kept in memory only (as in the web SDK) — it is empty after the app restarts. Long-term cart conditions are evaluated on the server.
  • The cart and order history belong to the current user and are cleared when the user changes (logout() or signing in as someone else). Commerce calls run in the order you make them relative to identify() and logout(), so items added right after identify() belong to the new user.

Reading commerce state #

final cart = await sdk.commerce.cartSnapshot();
// {items: [...], totalValue: 9900, currency: KRW}

final last = await sdk.commerce.commerceSnapshot();
// {lastEventName, lastEventAt, lastAddToCart, lastRemoveFromCart, lastViewItem,
//  lastViewItemList, lastBeginCheckout, lastPurchase, lastAddToWishlist}

final order = await sdk.commerce.orderSnapshot();
// {lastPurchase, hasPurchased, lastTransactionId}

Before initialize() completes, each returns {}. The cart snapshot reflects the SDK's in-memory cart, so it starts empty on every launch.

// Generic consent: code = {marketing|transactional|cookie|legal}_{target}
await sdk.subscriptions.set('marketing_email', true, source: 'signup', policyVersion: '2026-01');
await sdk.subscriptions.withdraw('marketing_email', source: 'settings'); // same as set(code, false)
final last = await sdk.subscriptions.get('marketing_email'); // true | false | null

// Shortcuts
await sdk.setPushOptin(true);       // = subscriptions.set('marketing_push', …)
await sdk.setNightPushOptin(true);  // = subscriptions.set('legal_night_send', …) — night-time marketing
  • set() / withdraw() / the shortcuts return true when the update was accepted by the SDK. Delivery, retry and ordering are handled natively.
  • An invalid code (not marketing_, transactional_, cookie_ or legal_ followed by a target) prints a warning, sends nothing and returns false.
  • get() returns the last value sent from this device (null if never sent). It is not the server's consent state.
  • Operating-system notification permission is separate from marketing consent — see §11.4.

11. Push notifications #

11.1 Android — connect Firebase #

Push uses Firebase Cloud Messaging. The configuration file alone is not enough — the Google services Gradle plugin must be applied, otherwise FCM never starts and no token is registered.

  1. Put google-services.json in android/app/.

  2. Apply the Google services plugin. With the plugin DSL (android/settings.gradle(.kts) and android/app/build.gradle(.kts)):

    // android/settings.gradle.kts — plugins { … }
    id("com.google.gms.google-services") version "4.4.2" apply false
    
    // android/app/build.gradle.kts — plugins { … }
    id("com.google.gms.google-services")
    

The SDK's push receiver and notification service are merged into your app manifest automatically. Receiving, displaying and click tracking run natively — even when the app is closed. You do not need the firebase_messaging package; if your app uses it anyway, see §11.6.

11.2 iOS — APNs and the notification service extension #

  1. Xcode → Runner target → Signing & Capabilities → add Push Notifications.
  2. BzEngage console → Settings → Channels & Subscriptions → Apple APNs — enter Key ID, Team ID, Bundle ID and the .p8 key, and set Environment: sandbox for builds installed from Xcode, production for TestFlight / App Store. A mismatch means nothing arrives.
  3. iOS push does not use Firebase. No GoogleService-Info.plist is needed for BzEngage.

Images and buttons — Notification Service Extension (recommended)

  1. Xcode → File → New → Target → Notification Service Extension.

  2. In ios/Podfile, add the iOS SDK to the extension target too:

    target 'YourNotificationService' do
      use_frameworks!
      pod 'BzEngage', :podspec => 'https://bzengage.com/sdk/ios/0.1.5/BzEngage.podspec'
    end
    
  3. Replace the generated NotificationService.swift with:

    import BzEngage
    
    class NotificationService: BzEngageNotificationService {}
    
  • Write the extension's version numbers yourself in its Info.plist (for example 1.0.0 and 1). $(FLUTTER_BUILD_NAME) and $(FLUTTER_BUILD_NUMBER) are not defined for the extension target, so the version would be empty and the app fails to install ("Failed to create app extension placeholder").
  • Give the extension target a product name (PRODUCT_NAME in its build settings). If it is empty, the build fails with "Multiple commands produce … .appex".
  • Optional — count deliveries while the app is closed: add the same App Group to the app and the extension, and put the key BzenAppGroup with the group name in both Info.plist files. To mark those delivery records as Flutter, also add BzenSdkWrapper = flutter and BzenSdkWrapperVersion = 0.7.2 to the extension's Info.plist.
  • Older versions of this package shipped their own extension class (import bzengage_flutter_sdk). It is gone — use the iOS SDK's BzEngageNotificationService as above.

11.3 Turn push on — on every launch #

final sdk = await BzEngageSDK.initialize(baseUrl: 'https://…', apiKey: 'cpk_…');
await sdk.initPush();

Call initPush() right after initialize() every time the app starts (before showMessages()), not only on the first launch. A notification tap that opened a closed app is handled when initPush() runs. Push messages that arrive after install but before the first initPush() are not tracked. initPush() never shows the permission dialog.

11.4 Ask for notification permission #

Your app asks for the permission (Android 13+ and iOS) — for example with the permission_handler package. Call reRegisterPushToken() right after the answer — otherwise the server keeps treating notifications as off and campaigns skip this device.

import 'package:permission_handler/permission_handler.dart';

await Permission.notification.request();
await sdk.reRegisterPushToken();
// App running or in the background
final sub = sdk.onPushDeeplink.listen((url) => navigate(url));

// App was closed and opened by tapping the notification (cold start)
final url = await sdk.initialPushDeeplink();
if (url != null) navigate(url);
  • initialPushDeeplink() returns the link once and then clears it; it returns null before initialize() completes.
  • A link that arrives while nothing listens to onPushDeeplink is held in memory (up to 10, with no time limit) and delivered to the first listener. The same link is not delivered both ways.
  • Android — only the screen was closed: if the app's screen was closed but the app process kept running, tapping a notification delivers the link to onPushDeeplink, and initialPushDeeplink() returns null. Listen where the subscription always exists (for example in the root widget) and handle both the same way.
  • On Android the 60-second limit applies only to initialPushDeeplink(): a link that opened a closed app is ignored after 60 seconds, so call it soon after initialize().
  • iOS: call initialPushDeeplink() before initPush(). If onPushDeeplink already has a listener when initPush() runs, the listener receives the tap instead and initialPushDeeplink() returns null. Handle both the same way.

11.6 Using with firebase_messaging #

Android: when firebase_messaging is in your app, Firebase delivers every message to its service, not to BzEngage's. BzEngage notifications are then shown only if you forward them — without the code below, BzEngage pushes are silently not displayed, and token changes are not reported.

// Top-level function — runs when the app is closed or in the background
@pragma('vm:entry-point')
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
  if (BzEngageSDK.isBzEngagePush(message.data)) {
    await BzEngageSDK.handlePushMessage(message.data);
  }
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);

  final sdk = await BzEngageSDK.initialize(baseUrl: 'https://…', apiKey: 'cpk_…');
  await sdk.initPush();

  // App in the foreground
  FirebaseMessaging.onMessage.listen((message) {
    if (BzEngageSDK.isBzEngagePush(message.data)) {
      BzEngageSDK.handlePushMessage(message.data);
    }
  });
  // Token changes
  FirebaseMessaging.instance.onTokenRefresh.listen((_) => sdk.reRegisterPushToken());

  runApp(const MyApp());
}
  • isBzEngagePush(data) is a Dart-only check — it does not wake the native side for other providers' messages.
  • handlePushMessage(data) works without initialize() (the background handler runs in a separate isolate). It shows the notification and returns true for BzEngage messages, false otherwise (it never throws). Only string values are forwarded.
  • You do not need to remove BzEngage's service from your manifest.

iOS: no forwarding code is needed. The plugin hands notifications it does not own back to the delegate that had them before (for example firebase_messaging), and BzEngage notifications are not passed to Firebase. handlePushMessage() returns false on iOS.

12. Value rules #

Values you pass as properties, context, user properties or commerce payloads are cleaned in Dart before reaching the native SDK.

You pass Sent as
int, double, String, bool, null As is
List, Map with String keys Same rules applied inside
NaN, Infinity Key dropped, debug warning
DateTime Key dropped, debug warning — pass a string instead, e.g. date.toIso8601String()
Any other type, map entries with non-String keys, circular references Dropped, debug warning
  • Inside lists, a dropped value becomes null so item positions are preserved.
  • setUserProperty(key, value) with a value that would be dropped sends nothing.
  • Push data passed to handlePushMessage() keeps string values only (FCM data is always strings).

13. Errors #

Errors are thrown as BzEngageError with a code. Codes are in ErrorCodes:

ErrorCodes. code When
invalidArgument invalid_argument Bad argument — empty baseUrl/apiKey, non-finite goal value
alreadyInitializedWithDifferentKey already_initialized_with_different_key initialize() with a different baseUrl/apiKey — restart the app to change keys
noActivity no_activity showMessages() with no foreground screen
internal internal_error The native SDK threw an exception
unsupportedPlatform unsupported_platform The call is not available on this platform
notInitialized not_initialized Native-level guard; the Dart layer normally answers with defaults before this can happen

Functions that return nothing (trackEvent, identify, …) never throw — failures are printed in debug builds.

try {
  await BzEngageSDK.initialize(baseUrl: baseUrl, apiKey: apiKey);
} on BzEngageError catch (e) {
  if (e.code == ErrorCodes.alreadyInitializedWithDifferentKey) { /* … */ }
}

14. Known limitations #

  • Zones: deep links and custom actions from zone buttons and images arrive on onInAppDeeplink / onInAppCustomAction, as in in-app messages, and a deep link the app cannot open uses the fallback URL. Links tapped inside HTML zones arrive on onInAppUrlOpen. (Android SDK 0.1.4 and later; earlier Android versions reported every zone link through onInAppUrlOpen and did not deliver zone custom actions.)
  • Zone events (impression, click) do not carry the sdk_wrapper property — other events do.
  • iOS — app delegate: if your app's UIApplicationDelegate is not a FlutterAppDelegate (for example add-to-app), the push token does not reach the plugin.
  • iOS — Swift Package Manager is not supported yet; the plugin uses CocoaPods. Flutter prints a warning about plugins without Swift Package Manager support.
  • Keys cannot change at runtime. One SDK per process; changing baseUrl / apiKey requires an app restart.
  • Calls before initialize() completes are dropped, not queued (except stream listeners, zones and handlePushMessage).
  • The cart and user properties are not kept across app restarts — they live in memory, as in the web SDK (see Commerce and §5).
  • Push messages before the first initPush() after install are not tracked.
  • shutdown() is for tests only and is not recommended in production apps.
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