bnotify_flutter 0.1.3
bnotify_flutter: ^0.1.3 copied to clipboard
BNotify Flutter plugin wrapping the native Android and iOS SDKs for push notifications and in-app messaging.
bnotify_flutter #
Flutter plugin for BNotify push notifications and in-app messaging.
Install: bnotify_flutter: ^0.1.3 on pub.dev
Wraps the native SDKs:
- Android: JitPack
bnotifysdk1.0.6 - iOS: SPM
BNotify
Web is not included. The plugin Git repository is private (not open source). Install from pub.dev only.
| Also useful | Link |
|---|---|
| Hosted HTML guide | bnotify.github.io/sdk-docs/flutter.html |
| Console | console.bnotify.com |
| Example app | example/ in this package |
Integration guide #
Audience: Flutter developers adding BNotify to any existing app
Package:bnotify_flutter0.1.3
Platforms: Android + iOS (web is not supported)
Console: https://console.bnotify.com/
Native Android SDK (pulled in automatically): JitPackbnotifysdk1.0.6
This guide is host-app setup only: Console credentials, pubspec, Android Activity/manifest/Gradle, iOS plist/capabilities/extension, Dart calls, and how to verify. Campaigns are created and sent from the Console. This file does not describe how the plugin is implemented internally.
Table of contents #
- What you get after integration
- Prerequisites
- Integration checklist
- Step 1 — Console: create the project and apps
- Step 2 — Add the Flutter package
- Step 3 — Dart: initialize, permission, and listeners
- Step 4 — Android host setup
- Step 5 — iOS host setup
- Step 6 — In-app campaigns (
logEvent) - Step 7 — Notification tap routing
- Optional — Custom Android notification UI
- Verification and test plan
- Troubleshooting
- Production checklist
- FAQ
- Copy-paste reference
1. What you get after integration #
| Capability | Result |
|---|---|
| Device registration | Device appears in the BNotify Console after a successful initialize + permission |
| Push notifications | System notifications on Android and iOS when the Console sends a campaign |
| Background / killed app | Notifications can still arrive after the user leaves or swipes the app away (after the first successful init + config on device) |
| Notification tap | Dart BNotify.onClick with a screen value you can route on |
| In-app messages | Native in-app UI when Console campaigns match events you log. Optional route and button scope in Step 6.1 |
| Device token | Dart BNotify.onToken for debugging / your own backend if needed |
The Flutter SDK does not send campaigns by itself. Create and send messages from the BNotify Console.
2. Prerequisites #
| Requirement | Notes |
|---|---|
| Flutter 3.24+ / Dart 3.13+ | Matches the plugin pubspec.yaml |
| A real Android / iOS app (not web) | bnotify_flutter has no web implementation |
| BNotify Console account | Project + Android app + iOS app |
Android minSdk 24 |
Required |
| iOS 13.0+ | Required |
| Physical devices recommended | Emulators/simulators are weaker for push |
| Android package name / iOS bundle id | Must match the apps you register in the Console |
You will download two files from the Console (never commit real keys to a public repo):
- Android:
bnotify-config.json - iOS:
bnotify-config.plist
3. Integration checklist #
Use this as a sequential punch list. Details are in the sections below.
- In the Console, create a project and register Android and iOS apps with the exact
applicationId/ bundle identifier of this Flutter app. - Download
bnotify-config.jsonandbnotify-config.plist. - Add
bnotify_flutter: ^0.1.3inpubspec.yamlfrom pub.dev. - Declare
assets/bnotify-config.jsoninpubspec.yamland keep a copy atandroid/app/bnotify-config.json. - Add JitPack, set
minSdk24, extendBNotifyFlutterActivity, setsingleTask, switch themes to AppCompat. - Add
ios/Runner/bnotify-config.plistto the Runner target, enable Push Notifications, and add a Notification Service Extension if campaigns include images. - In Dart:
WidgetsFlutterBinding.ensureInitialized(), subscribe to streams,await BNotify.initialize(...), thenrequestPermission(). - Send a test notification from the Console. Confirm token, tray notification, tap routing, and (optional) in-app via
logEvent. To keep that in-app UI off Splash or Login, add Step 6.1. Skip Step 6.1 if the default is what you want.
4. Step 1 — Console: create the project and apps #
- Open https://console.bnotify.com/.
- Create a project (or pick an existing one).
- Register an Android app whose package name equals your Flutter
applicationId(seeandroid/app/build.gradle.kts→defaultConfig.applicationId). - Register an iOS app whose bundle id equals
ios/Runner’s bundle identifier. - Download:
bnotify-config.jsonfor Androidbnotify-config.plistfor iOS
If the package name / bundle id in those files does not match the running app, the device will not register correctly.
JSON shape (values come from the Console — do not invent them):
{
"projectId": "YOUR_PROJECT_ID",
"packageName": "com.example.my_app",
"apiKey": "YOUR_API_KEY",
"appId": "YOUR_APP_ID",
"fcmAppId": "YOUR_FCM_APP_ID",
"fcmProjectId": "YOUR_FCM_PROJECT_ID",
"fcmApiKey": "YOUR_FCM_API_KEY",
"fcmSenderId": "YOUR_FCM_SENDER_ID"
}
Keep unused Console fields if the download includes them. Do not share these files publicly.
5. Step 2 — Add the Flutter package #
Add the published package from pub.dev.
In the host app pubspec.yaml:
dependencies:
flutter:
sdk: flutter
bnotify_flutter: ^0.1.3
Then:
flutter pub get
Hosted walkthrough: https://bnotify.github.io/sdk-docs/flutter.html
Do not use a public Git dependency. The plugin source repository is private. Apps should install only from pub.dev.
If you are a BNotify maintainer with GitLab access, you may use a path dependency while developing:
dependencies:
bnotify_flutter:
path: ../bnotify_flutter
Declare the Android JSON as a Flutter asset (used by BNotify.initialize(configAsset: …)):
flutter:
assets:
- assets/bnotify-config.json
Copy the Console file to:
assets/bnotify-config.json
android/app/bnotify-config.json
ios/Runner/bnotify-config.plist
android/app/bnotify-config.json is required so Android can start BNotify when the process is created from a notification, not only after Dart runs. Copy it into Android assets on each build (Gradle snippet in Step 4).
6. Step 3 — Dart: initialize, permission, and listeners #
Call this once at startup. Subscribe to streams before initialize so you do not miss a token or a cold-start tap.
import 'package:bnotify_flutter/bnotify_flutter.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
BNotify.onToken.listen((token) {
debugPrint('BNotify token: $token');
});
BNotify.onMessage.listen((message) {
debugPrint('BNotify message: ${message.title} ${message.body}');
});
BNotify.onClick.listen((click) {
debugPrint('BNotify click screen=${click.screen}');
// Navigate using click.screen (see Step 7).
});
await BNotify.initialize(
configAsset: 'assets/bnotify-config.json',
);
runApp(const MyApp());
}
After the first frame (or from a settings screen), request notification permission:
await BNotify.requestPermission();
| API | When to call |
|---|---|
BNotify.initialize |
Once per process, after ensureInitialized |
BNotify.requestPermission |
After initialize; required on Android 13+ and iOS to show alerts |
BNotify.logEvent('…') |
When a user action should match an in-app campaign trigger |
BNotify.configureInApp |
Optional. Limit in-app UI to routes. Skip it to keep the default |
BNotify.setCurrentRoute |
Call on every navigation change when a route scope is on |
BNotify.setInAppAllowed |
Optional manual on/off for in-app UI |
BNotify.onToken |
Debug / send the token to your own server if you need it |
BNotify.onMessage |
Optional; system notifications still show when auto-notifications are on |
BNotify.onClick |
Route the user after they tap a notification |
BNotify.setAutoNotifications |
Android only; see optional custom UI |
configAsset is read on Android. iOS ignores it and uses ios/Runner/bnotify-config.plist. You still pass configAsset so one initialize call works on both platforms.
Hot reload does not re-run native Kotlin/Swift. After changing MainActivity, manifests, plist, or the plugin itself, do a full restart (flutter run) or uninstall/reinstall.
7. Step 4 — Android host setup #
Do every item in this section. Skipping the Activity or theme steps is the usual reason in-app messages never appear.
7.1 JitPack repositories #
The plugin depends on the native Android SDK from JitPack. Add JitPack in both places Flutter apps typically declare repositories.
android/settings.gradle.kts (inside pluginManagement { repositories { … } }):
maven { url = uri("https://jitpack.io") }
android/build.gradle.kts (allprojects.repositories or the equivalent in your Gradle setup):
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
7.2 minSdk 24 #
In android/app/build.gradle.kts:
defaultConfig {
applicationId = "com.example.my_app" // must match Console
minSdk = 24
}
Do not leave Flutter’s default minSdk if it is below 24.
7.3 Copy config into Android assets #
Keep android/app/bnotify-config.json (Console download). Add this to android/app/build.gradle.kts above the android { } block so the file is packaged as assets/bnotify-config.json inside the APK:
val bnotifyConfigFile = file("bnotify-config.json")
val bnotifyAssetsDir = file("src/main/assets")
tasks.register<Copy>("copyBnotifyConfigAsset") {
from(bnotifyConfigFile)
into(bnotifyAssetsDir)
onlyIf { bnotifyConfigFile.exists() }
}
tasks.matching { it.name == "preBuild" }.configureEach {
dependsOn("copyBnotifyConfigAsset")
}
Also keep assets/bnotify-config.json at the Flutter project root (same file) for configAsset.
Add android/app/src/main/assets/bnotify-config.json and android/app/bnotify-config.json to .gitignore if they contain real keys. Do not commit production credentials.
7.4 MainActivity must extend BNotifyFlutterActivity #
Replace Flutter’s default activity. File: android/app/src/main/kotlin/…/MainActivity.kt
package com.example.my_app
import com.convex.bnotify_flutter.BNotifyFlutterActivity
class MainActivity : BNotifyFlutterActivity()
Do not extend FlutterActivity or FlutterFragmentActivity. In-app campaigns are shown with AppCompat; the host activity must be BNotifyFlutterActivity.
7.5 AndroidManifest: singleTask, no empty taskAffinity #
On the launcher activity:
android:launchMode="singleTask"- Do not set
android:taskAffinity=""
Empty taskAffinity plus a notification tap creates two Recents cards.
Example activity block:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Keep INTERNET if you do not already have it:
<uses-permission android:name="android.permission.INTERNET" />
POST_NOTIFICATIONS is merged from the plugin. You still must call BNotify.requestPermission() at runtime on Android 13+.
7.6 AppCompat themes #
BNotifyFlutterActivity is an AppCompat activity. Flutter’s default Theme.Black.NoTitleBar / Material3 launch themes will crash or look wrong.
android/app/src/main/res/values/styles.xml:
<style name="LaunchTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
Night values (values-night/styles.xml): use Theme.AppCompat.DayNight.NoActionBar (or Light/Dark AppCompat) for both LaunchTheme and NormalTheme.
7.7 Optional: native Gradle config plugin #
If you prefer generated Kotlin config instead of (or in addition to) configAsset, you can apply the native Gradle plugin the same way a pure Android app does. Most Flutter apps can skip this and rely on configAsset + android/app/bnotify-config.json.
8. Step 5 — iOS host setup #
8.1 Add bnotify-config.plist #
- Copy the Console plist to
ios/Runner/bnotify-config.plist. - In Xcode, select the file → File Inspector → Target Membership → enable Runner.
- Confirm
packageNamein the plist equals the Runner bundle identifier.
8.2 Push Notifications capability #
In Xcode → Runner target → Signing & Capabilities:
- Add Push Notifications.
- Use an Apple Developer team that can issue a push-enabled provisioning profile.
- For a Notification Service Extension (below), also add App Groups on Runner and the extension, using the same group id as in the plist (
appGroupIdif present).
8.3 Swift Package Manager (recommended with current Flutter) #
If your Flutter toolchain uses Swift Package Manager for plugins:
flutter config --enable-swift-package-manager
Then flutter pub get / open ios/Runner.xcworkspace and let Flutter resolve the plugin’s iOS package (it pulls the native iOS SDK from GitLab automatically).
If you use CocoaPods only, pod install from ios/ after flutter pub get. The plugin’s podspec fetches the native iOS SDK during prepare.
Minimum deployment target: iOS 13.
8.4 AppDelegate #
A stock FlutterAppDelegate is enough. You do not need to call native BNotify APIs from AppDelegate for basic push. Keep GeneratedPluginRegistrant as Flutter generated it.
8.5 Notification Service Extension (iOS image / rich notifications) #
Apple shows title and body from the APNs alert by itself. It does not let the main Flutter app download an image and attach it while the app is in the background, killed, or the device is locked. That work has to run in a Notification Service Extension (NSE) that belongs to the same app.
bnotify_flutter does not ship this target. Every Flutter app that sends image campaigns on iOS must add it in Xcode. Android does not use an NSE. Text-only iOS campaigns keep working without one. Notification taps and BNotify.onClick / screen routing stay on the main Runner app.
Open ios/Runner.xcworkspace (not the .xcodeproj).
8.5.1 Create the extension target
- File → New → Target → Notification Service Extension.
- Name it
NotificationService. - Xcode sets the bundle id to
<Runner bundle id>.NotificationService(example: Runnercom.example.myapp→ extensioncom.example.myapp.NotificationService). - Set the extension iOS Deployment Target to 13.0.
- Use the same signing team as Runner.
8.5.2 Link the native iOS SDK to the extension only
On the NotificationService target, add this Swift package and link the product BNotify:
https://gitlab.com/hamza.hafeez/bnotifyiossdk.git
Keep the bnotify_flutter CocoaPod / Flutter plugin on the Runner target. Do not link Flutter or bnotify_flutter into the extension. The extension only needs import BNotify.
8.5.3 App Group (same id in four places)
Pick one App Group. The usual name is group. plus the Runner bundle id, for example group.com.example.myapp.
That exact string must match in all of these:
| Place | Value |
|---|---|
| Apple Developer → App IDs for Runner and the extension | App Groups capability, same group |
| Xcode → Runner → Signing & Capabilities → App Groups | Same group checked |
| Xcode → NotificationService → Signing & Capabilities → App Groups | Same group checked |
ios/Runner/bnotify-config.plist key appGroupId |
Same string |
NotificationService.swift constant |
Same string |
The Console download of bnotify-config.plist does not include appGroupId. Add it yourself on the Runner target:
<key>appGroupId</key>
<string>group.com.example.myapp</string>
Runner’s plist stays on the Runner target only. The extension reads config from the App Group after the main app has launched once.
8.5.4 NotificationService.swift
Replace the file Xcode generated:
import UserNotifications
import BNotify
/// Must match App Groups on Runner + this extension, and `appGroupId` in Runner's bnotify-config.plist.
private let bnotifyAppGroupId = "group.com.example.myapp"
final class NotificationService: UNNotificationServiceExtension {
private var contentHandler: ((UNNotificationContent) -> Void)?
private var bestAttemptContent: UNNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
bestAttemptContent = request.content
BNotifyExtensionSafe.prepareNotificationContent(
request,
appGroupId: bnotifyAppGroupId
) { [weak self] content in
guard let self, let handler = self.contentHandler else { return }
self.bestAttemptContent = content
handler(content)
self.contentHandler = nil
}
}
override func serviceExtensionTimeWillExpire() {
guard let contentHandler, let bestAttemptContent else { return }
contentHandler(bestAttemptContent)
self.contentHandler = nil
}
}
Always pass appGroupId. The extension bundle does not contain bnotify-config.plist. If you omit the argument, the SDK looks up the built-in testing group group.com.bnotify.convex.testing.BNotifyClient, which is not your app.
prepareNotificationContent downloads the image and attaches it for the collapsed thumbnail and the expanded banner. It also reports a received event when the App Group config is available.
8.5.5 Payload the extension can render
iOS starts the NSE only when the APNs payload includes aps["mutable-content"] = 1. There is no Flutter toggle for that. The BNotify sender / campaign must set it. Without it, the banner stays title and body even if the extension is installed.
Put the image on a top-level imageUrl (https), as a sibling of aps. The SDK also accepts image_url, image, mediaUrl, media_url, picture, bigPicture, and big_picture at the top level, inside aps, or under data, payload, notification, custom, or fcm_options. The value must start with http:// or https://.
PNG, JPG, JPEG, WEBP, BMP, and GIF are supported. Files larger than 10 MB are skipped. isExpanded does not block the attachment. A valid URL is enough.
{
"aps": {
"alert": { "title": "Hello", "body": "World" },
"sound": "default",
"mutable-content": 1
},
"imageUrl": "https://cdn.example.com/promo.jpg",
"notificationId": "n-123",
"token": "<device token>",
"screen": "home"
}
8.5.6 Install and verify
- After adding
appGroupId, launch the main app once so the SDK writesbnotify_config.jsoninto the App Group. - Delete the app, reinstall from Xcode onto a physical device, and allow notifications.
- Send an image campaign. The lock screen, banner, and killed/background states should show title, body, and the image.
- Tap still opens the app and emits
BNotify.onClick.
Debug builds use the development APNs environment. TestFlight and App Store builds use production. The extension follows the host app. Both targets’ provisioning profiles must include the same App Group.
In the device console, filter for the extension process and [BNotify]. A working image push logs prepareNotificationContent, a real imageUrl, aps.mutable-content: 1, and attachments=1. imageUrl: MISSING means the campaign payload has no URL the SDK recognizes. No extension log means mutable-content was not set, or the extension is not in the installed build.
8.6 Info.plist / usage strings #
You do not add a custom Dart permission plugin. iOS uses the system push prompt via BNotify.requestPermission(). If Xcode asks for a purpose string for related APIs, follow Apple’s current requirements for your extension and App Groups.
9. Step 6 — In-app campaigns (logEvent) #
- In the Console, create an in-app campaign whose trigger name matches a string you will log (for example
app_open). - In Dart, after initialize:
await BNotify.logEvent('app_open');
Call it when the matching user action happens (app open, screen view, button, purchase, and so on). The name must match the Console trigger exactly.
Android in-app UI requires Step 4.4 (BNotifyFlutterActivity) and AppCompat themes. If you skip those, logEvent will not show a campaign even when the Console is correct.
By default the native in-app UI is attached to the single host activity / root view controller, so it can appear on Splash, Login, Home, or any other Flutter route. To keep it on specific screens, or to show it only after a button tap, use the optional API below. Apps that never call it keep today's behavior. No migration.
9.1 Optional: screen-scoped and action-scoped in-app messages #
Default: do not call
configureInApporsetInAppAllowed. In-app campaigns show when the SDK matches a trigger, same as before.Opt in only if the overlay should stay off some routes, or should wait for a button.
The Console trigger is still the source of truth for which campaign matches. These methods only control when the UI may be visible. logEvent still has to use the exact trigger name.
| You want | What to call |
|---|---|
| Same as today | Nothing extra |
| Only on certain routes | configureInApp(allowedRoutes: [...]) and setCurrentRoute on every navigation change |
| Everywhere except Splash / Login | configureInApp(blockedRoutes: [...]) and setCurrentRoute |
| Only after a button | logEvent('exact_trigger') in onPressed. No route API |
| Button, and only on an allowed route | Both. logEvent matches the campaign; the UI waits until the route is allowed |
allowedRoutes and blockedRoutes are Flutter paths, not Console fields.
- Comparison is exact after the plugin trims the string, drops
?queryand#fragment, adds a leading/, and drops a trailing slash./home/and/home?tab=1both match/home. - A pattern that ends in
/*matches that path and its children./home/*matches/homeand/home/products. It does not match/homepage. - A blocked pattern wins when a path is in both lists.
- Names are case-sensitive and must be the path your router reports (
GoRouteruri.path, orRouteSettings.name). - With an allow-list, in-app UI stays off until the first
setCurrentRoute. Call it with the initial route in the same place you callconfigureInApp. - With only a block-list, an unknown route is not treated as blocked. Still pass the initial route so Splash is blocked from the first frame.
Route only (GoRouter)
await BNotify.initialize(configAsset: 'assets/bnotify-config.json');
// OPTIONAL — delete this block to keep the default
BNotify.configureInApp(allowedRoutes: [
'/home',
'/home/products',
]);
void syncInAppRoute() {
final path = _router.routerDelegate.currentConfiguration.uri.path;
BNotify.setCurrentRoute(path);
}
_router.routerDelegate.addListener(syncInAppRoute);
syncInAppRoute();
Replace /home with your own routes. Android still needs BNotifyFlutterActivity and an AppCompat theme or the dialog cannot be hosted at all.
Navigator (no GoRouter): pass RouteSettings.name from a NavigatorObserver (didPush, didPop, didReplace) and from the first route.
Block Splash and Login instead of listing every allowed screen:
BNotify.configureInApp(blockedRoutes: ['/splash', '/login']);
BNotify.setCurrentRoute('/splash'); // initial route
Action only (button)
Create an in-app campaign whose trigger is exactly home_promo_click. Do not call configureInApp.
ElevatedButton(
onPressed: () => BNotify.logEvent('home_promo_click'),
child: const Text('Show offer'),
)
The campaign shows when that event is logged and the SDK's frequency and date rules allow it. No route listener.
Both
BNotify.configureInApp(allowedRoutes: ['/home']);
BNotify.setCurrentRoute('/home');
// Console trigger name must be exactly home_promo_click
ElevatedButton(
onPressed: () => BNotify.logEvent('home_promo_click'),
child: const Text('Show offer'),
)
If this button runs while the current route is not allowed, the campaign is kept and shown once the user reaches an allowed route (still subject to the Console frequency cap). It does not show on Splash or Login.
setInAppAllowed is the same gate without a route list:
BNotify.setInAppAllowed(currentRoute == '/home');
If you also called configureInApp, the UI shows only when the manual flag is true and the route matches. setInAppAllowed(true) turns the manual flag back on.
What the gate does
- Closed gate: the campaign is queued. It is not left on screen on a disallowed route.
- Open gate: one queued campaign can show, then the existing frequency rules apply.
- The user leaves the allowed route: the overlay closes. That show already counted.
- A campaign that was only queued, and never presented, does not consume
maxPerDay. - Android SDK 1.0.6 and the current iOS SDK queue the campaign before any dialog or view controller is created. A blocked route does not leave the message on screen, and that queued campaign does not consume
maxPerDay. - The local cap is incremented when the message is presented. The server impression is a separate event about one second after the UI stays visible. Dismissing before that second does not send the server impression; the local cap still counts if the message was actually presented on an allowed screen.
Push notifications are unchanged. iOS does not need an extra extension for this. The host activity on Android is still BNotifyFlutterActivity.
10. Step 7 — Notification tap routing #
When the user taps a notification, listen to BNotify.onClick. Use click.screen (and other extras) to navigate.
final navigatorKey = GlobalKey<NavigatorState>();
void listenForClicks() {
BNotify.onClick.listen((click) {
final screen = click.screen;
if (screen == null || screen.isEmpty) return;
navigatorKey.currentState?.pushNamed(screen);
});
}
Set the screen value on the campaign in the Console so it matches a route name (or map it in a switch).
BNotifyClick also exposes notificationId, type, action, and extras if you need them.
Subscribe in main() before initialize so a tap that opened a killed app still reaches Dart.
11. Optional — Custom Android notification UI #
By default Android shows a system notification. To draw your own UI from Dart instead:
await BNotify.setAutoNotifications(false);
BNotify.onMessage.listen((message) {
// Show your own banner / dialog using message.title / message.body.
});
iOS always uses the system notification UI. setAutoNotifications has no effect there.
Leave auto-notifications on unless you have a custom in-app banner to replace the tray.
12. Verification and test plan #
Work through these on a physical device with a Debug or Release build that contains real Console files.
| # | Test | Expected |
|---|---|---|
| 1 | Cold start, initialize succeeds |
No exception; onToken eventually prints a non-empty token |
| 2 | requestPermission |
System permission dialog (Android 13+ / iOS); user allows |
| 3 | Console → device list | This install appears after token + internet |
| 4 | Console sends a push, app in foreground | Notification (iOS banner / Android tray or onMessage) |
| 5 | App in background | Tray notification appears |
| 6 | Force-stop or swipe away, then send | Notification still arrives (after first successful init with packaged Android JSON) |
| 7 | Tap the notification | App opens; onClick fires; screen matches Console |
| 8 | Recents / app switcher | One task, not two |
| 9 | logEvent with a matching in-app campaign |
In-app UI appears on Android (AppCompat host) and iOS |
| 10 | Image payload + iOS NSE | Image renders in the notification |
| 11 | App never calls configureInApp or setInAppAllowed |
In-app behavior matches older plugin versions |
| 12 | allowedRoutes: ['/home'], user is on Splash or Login |
No in-app overlay |
| 13 | User opens /home with a queued campaign |
In-app UI shows on Home |
| 14 | User leaves /home |
Overlay closes and does not stay on the next route |
| 15 | Button calls logEvent with the Console trigger |
Campaign shows when that event is logged, and only if the route gate (if you set one) allows it |
If 6 fails on Android, confirm android/app/bnotify-config.json exists and the Gradle copy task ran (android/app/src/main/assets/bnotify-config.json inside the module).
13. Troubleshooting #
| Symptom | What to check |
|---|---|
initialize throws missing config |
Android: configAsset path listed in pubspec.yaml assets, file exists. iOS: plist in Runner target. |
| No token | Permission denied; wrong package/bundle vs Console; no network; still using placeholder YOUR_API_KEY. |
| No pushes | Device not in Console; permission off; battery restrictions; you sent to the wrong app/platform. |
| Pushes only while the Flutter UI is open | Android JSON not packaged as an asset; first initialize never succeeded so nothing was saved. |
| Two Recents cards | Remove android:taskAffinity=""; use singleTask. |
| In-app never shows (Android) | MainActivity still extends FlutterActivity; themes not AppCompat; trigger name ≠ Console; need full rebuild after Activity change. If you set allowedRoutes, also confirm setCurrentRoute was called with that path. |
| In-app never shows (iOS) | Plist missing from target; campaign platform/app mismatch; logEvent name mismatch. If you set allowedRoutes, confirm setCurrentRoute matches the router path. |
| In-app shows on Splash or Login | You are on the default (no scope). Add configureInApp and call setCurrentRoute with the initial route, not only after the first navigation. |
| Crash on launch (Android theme) | LaunchTheme / NormalTheme must use Theme.AppCompat.*. |
| Permission button seems to “click” a notification | You are on an old plugin; current requestPermission only requests POST_NOTIFICATIONS. Upgrade to the latest pub.dev version. |
| iOS images missing (title and body only) | Add the Notification Service Extension in Step 5. Pass the same appGroupId into prepareNotificationContent. Confirm the push has aps.mutable-content = 1 and an https imageUrl. Reinstall on a physical device. |
Gradle cannot find bnotifysdk |
JitPack missing from settings.gradle.kts and build.gradle.kts. |
minSdk error |
Set minSdk = 24 in the app module. |
| Changes to Kotlin/Swift not visible | Hot reload is not enough. Stop the app and flutter run again. |
14. Production checklist #
- ❌ Console Android package name =
applicationId - ❌ Console iOS bundle id = Xcode Runner bundle id
- ❌ Real
bnotify-config.json/.pliston the device (not placeholders) - ❌ Those files are not in a public git repo
- ❌
bnotify_flutter: ^0.1.3from pub.dev - ❌
minSdk24, JitPack present - ❌
MainActivityextendsBNotifyFlutterActivity - ❌
launchMode="singleTask", no emptytaskAffinity - ❌ AppCompat launch/normal themes
- ❌ Android config copied into
src/main/assets - ❌ iOS Push capability, plus Notification Service Extension, shared App Group, and plist
appGroupIdwhen campaigns include images - ❌
initialize+requestPermissionin release builds - ❌
onClickrouting tested from a killed app - ❌ If in-app UI should stay off Splash / Login:
configureInApp+setCurrentRoutewired to the router, including the first route - ❌ Release signing / APNs production setup as required by Apple and Play
15. FAQ #
Is this on pub.dev?
Yes. Use bnotify_flutter: ^0.1.3 as in Step 2. The Git repository is private; do not depend on it via git:.
Do I need a custom Application class on Android?
No. Put config in android/app/bnotify-config.json and call BNotify.initialize from Dart.
Do I need Firebase / google-services.json in the Flutter app?
Use the FCM fields inside the Console bnotify-config.json. Follow the Console download; do not invent keys.
Can I use this on web?
No. Android and iOS only.
Why BNotifyFlutterActivity?
Native in-app UI requires an AppCompat host. Flutter’s default activity types do not provide that. Extending BNotifyFlutterActivity is a required host change, not an optional extra.
Does configAsset replace the iOS plist?
No. Keep both. Android uses the JSON; iOS uses the plist.
Can I initialize later than main()?
You can, but you will miss cold-start taps and possibly the first token. Prefer main() as shown.
Can I show in-app messages only on Home, or only after a button tap?
Yes. That is optional. Step 6.1 has copy-paste samples for routes only, a button only, and both. Apps that skip it keep the current behavior. The Console trigger name still has to match logEvent exactly. Android in-app UI still needs BNotifyFlutterActivity.
16. Copy-paste reference #
pubspec.yaml (host) #
dependencies:
bnotify_flutter: ^0.1.3
flutter:
assets:
- assets/bnotify-config.json
lib/main.dart #
import 'package:bnotify_flutter/bnotify_flutter.dart';
import 'package:flutter/material.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
BNotify.onClick.listen((click) {
debugPrint('screen=${click.screen}');
});
BNotify.onToken.listen(debugPrint);
await BNotify.initialize(configAsset: 'assets/bnotify-config.json');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FilledButton(
onPressed: () => BNotify.requestPermission(),
child: const Text('Enable notifications'),
),
),
),
);
}
}
MainActivity.kt #
import com.convex.bnotify_flutter.BNotifyFlutterActivity
class MainActivity : BNotifyFlutterActivity()
Files to place #
| Path | Source |
|---|---|
assets/bnotify-config.json |
Console Android download |
android/app/bnotify-config.json |
Same JSON |
ios/Runner/bnotify-config.plist |
Console iOS download |
After integration, send a test campaign from https://console.bnotify.com/.
Package example #
See example/. Replace placeholder credentials in:
example/assets/bnotify-config.jsonexample/android/app/bnotify-config.jsonexample/ios/Runner/bnotify-config.plist