bzengage_flutter_sdk 0.7.1
bzengage_flutter_sdk: ^0.7.1 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 #
- Requirements & tested versions
- Installation
- Quick start
- Identity
- Events, screens, context & user properties
- Goals, attribution & system rules
- In-app messages
- Zones
- Commerce
- Consent
- Push notifications
- Value rules
- Errors
- 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.1
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 #
-
Add the package (above) and run
flutter pub get. -
The plugin adds the BzEngage Maven repository to your Gradle projects. If your
android/settings.gradleusesdependencyResolutionManagementwithRepositoriesMode.FAIL_ON_PROJECT_REPOSorPREFER_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. -
Stop the app and rebuild it (
flutter run). A hot restart does not load new native code. -
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 #
-
Add the package (above) and run
flutter pub get. -
In
ios/Podfile, set the platform and add the BzEngage iOS SDK inside your app target. It is served fromhttps://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.4/BzEngage.podspec' flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) # … endIf this line is missing,
pod installfails with "Unable to find a specification for BzEngage". Eachbzengage_flutter_sdkrelease names the iOS SDK version it needs — 0.7.1 needsBzEngage0.1.4. -
Run
cd ios && pod install. -
Your
AppDelegateneeds no BzEngage code. If you overrideUIApplicationDelegatemethods, callsuper(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 beforeinitialize().BzEngageSDK.handlePushMessage()andBzEngageSDK.isBzEngagePush()work withoutinitialize()(see §11.6). Zones placed beforeinitialize()wait and load when it completes. - One SDK per process. Calling
initialize()again with the samebaseUrlandapiKey(for example when the Flutter engine is recreated) returns the same SDK and only applies the settings you pass (inAppAllowedSchemes,inAppDebug,linkHandling). A differentbaseUrlorapiKeythrows aBzEngageErrorwithErrorCodes.alreadyInitializedWithDifferentKey— restart the app to change keys. initialize()throwsErrorCodes.invalidArgumentifbaseUrlorapiKeyis 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, socurrentPersonId()right afterawait 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; callmarkOpenedFrom('deeplink')yourself when the app is opened by a deep link.trackEvent()andsetScreen()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()returnstruewhen the goal was sent. Each goal is sent at most once per app run; later calls returnfalse. It throwsErrorCodes.invalidArgumentifvalueis 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_idor 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)returnsnullwhen 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.noActivitywhen 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, thenshowMessages(). - Messages are drawn natively on top of your Flutter screens. The Android back button closes the message only.
sdk.setInAppDebug(true)(orinAppDebug: trueininitialize) prints the SDK's display decisions to Logcat / the Xcode console.
7.2 Link handling #
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. UseLinkHandling.appif 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
onInAppCustomActionhas a listener. - Event names are also exported as constants:
BzEngageEvents.inAppDeeplinketc.
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).
maxHeightapplies to HTML zones only; zones built with the native element editor are always sized to their content. Omit it, or pass0or less, for no cap.onHeightChangedis 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
placeholderis 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
namereplaces the content. Removing the widget clears the zone. - The zone follows your app's theme: inside a
ThemewithBrightness.dark, HTML zone text defaults to the dark-mode color; without aThemeit follows the phone setting. - Impressions, clicks and audience checks are handled by the SDK.
- Android and iOS only. On other platforms
BzenZoneis 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 #
itemsis a list of GA4-style item maps:{'item_id', 'item_name', 'price', 'quantity', …}. Items are matched byitem_id(thenproduct_id,sku,id) to combine quantities; items with none of these are added as separate lines.currencydefaults to'KRW'.- Duplicate orders: a second
purchasewith the sametransactionIdis ignored — no event is sent. Without atransactionIdthere 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:
addToCartaddsvalueto the cart total;removeFromCartsubtracts it. Items with the sameitem_idhave their quantities combined; removing reduces the quantity and drops the item at 0.- Passing
cart_items(list) — optionally withcart_value— inextraofaddToCart/removeFromCartreplaces the whole cart instead of accumulating. beginCheckoutreplaces the cart items withitemsand the total withvalue— only the ones you pass; omitted ones keep their current value.purchasedoes 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 toidentify()andlogout(), so items added right afteridentify()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.
10. Consent #
// 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 returntruewhen the update was accepted by the SDK. Delivery, retry and ordering are handled natively.- An invalid code (not
marketing_,transactional_,cookie_orlegal_followed by a target) prints a warning, sends nothing and returnsfalse. get()returns the last value sent from this device (nullif 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.
-
Put
google-services.jsoninandroid/app/. -
Apply the Google services plugin. With the plugin DSL (
android/settings.gradle(.kts)andandroid/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 #
- Xcode →
Runnertarget → Signing & Capabilities → add Push Notifications. - BzEngage console → Settings → Channels & Subscriptions → Apple APNs — enter Key ID, Team ID, Bundle ID and the
.p8key, and set Environment:sandboxfor builds installed from Xcode,productionfor TestFlight / App Store. A mismatch means nothing arrives. - iOS push does not use Firebase. No
GoogleService-Info.plistis needed for BzEngage.
Images and buttons — Notification Service Extension (recommended)
-
Xcode → File → New → Target → Notification Service Extension.
-
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.4/BzEngage.podspec' end -
Replace the generated
NotificationService.swiftwith:import BzEngage class NotificationService: BzEngageNotificationService {}
- Write the extension's version numbers yourself in its
Info.plist(for example1.0.0and1).$(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_NAMEin 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
BzenAppGroupwith the group name in bothInfo.plistfiles. To mark those delivery records as Flutter, also addBzenSdkWrapper=flutterandBzenSdkWrapperVersion=0.7.1to the extension'sInfo.plist. - Older versions of this package shipped their own extension class (
import bzengage_flutter_sdk). It is gone — use the iOS SDK'sBzEngageNotificationServiceas 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();
11.5 Deep links from a tapped notification #
// 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 returnsnullbeforeinitialize()completes.- A link that arrives while nothing listens to
onPushDeeplinkis 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, andinitialPushDeeplink()returnsnull. 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 afterinitialize(). - iOS: call
initialPushDeeplink()beforeinitPush(). IfonPushDeeplinkalready has a listener wheninitPush()runs, the listener receives the tap instead andinitialPushDeeplink()returnsnull. 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 withoutinitialize()(the background handler runs in a separate isolate). It shows the notification and returnstruefor BzEngage messages,falseotherwise (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
nullso 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 ononInAppUrlOpen. (Android SDK 0.1.4 and later; earlier Android versions reported every zone link throughonInAppUrlOpenand did not deliver zone custom actions.) - Zone events (impression, click) do not carry the
sdk_wrapperproperty — other events do. - iOS — app delegate: if your app's
UIApplicationDelegateis not aFlutterAppDelegate(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/apiKeyrequires an app restart. - Calls before
initialize()completes are dropped, not queued (except stream listeners, zones andhandlePushMessage). - 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.