Flutter
nida_inapp shows banners, popups, full-screen messages and an inbox, and handles push opens,
track and consent. Every message is drawn with Flutter widgets. There is no native UI and no
WebView, so callbacks behave the same on Android and iOS, and a message runs in its own direction
inside a Directionality. Impressions, clicks and dismissals are reported, queued on the device and
retried. Nothing the SDK does throws into the app.
Install
Not published yet
The package isn’t on pub.dev yet: it comes with your account, under the name below.
dependencies:
nida_inapp: ^0.1.0
Use it
final navigatorKey = GlobalKey<NavigatorState>();
AppMessages.configure(AppMessagesOptions(
appKey: 'ipk_…', // the app → In-app keys (publishable)
apiUrl: 'https://<your in-app host>',
placements: ['home_top', 'home_popup', 'inbox'], // every placement the app shows, at most 10
profilesUrl: 'https://<your profiles host>', // for track and consent
profilesKey: 'pk_…',
onOpenUrl: (url, message) => myRouter.open(url), // the SDK opens no link itself
onPushOpened: (messageId, placement) => myRouter.openOffers(),
));
await AppMessages.identify(user.id); // your own opaque id; nothing shows before it
await AppMessages.setConsent('in_app', true); // strict: without it, nothing is decided
runApp(AppMessagesScope(navigatorKey: navigatorKey, child: MyApp(navigatorKey: navigatorKey)));
// where the app wants them:
const MessageBanner(placement: 'home_top'); // builds nothing until a banner is ready
const MessageInbox(placement: 'inbox'); // a list; put it on its own screen
const AppMessagesDebugView(); // the install id, for test devices
One decide per session
A session starts at configure, on identify of another user, and on coming back to the
foreground after sessionTimeout (30 minutes). Each makes one decide for every configured
placement, and the widgets serve from its answer, so showing a banner on ten screens is still one
call.
setConsent('in_app', true)andAppMessages.refresh()decide once more on purpose.- Each install may decide 5 times in a burst, then once every 3 minutes. When asked to wait, the SDK keeps the last decision until it may decide again. Offline also keeps it.
- Each decide’s token is kept on the device for its user and sent back on the next decide, so the service can reuse its choices. A different user never sends another user’s token.
Images
A message’s image is loaded into Flutter’s image cache before the message counts as ready, and kept there while it can show. An image that fails (offline, a 404, 10 seconds without an answer) means that message is skipped this session: it is never shown broken.
The SDK keeps no disk cache of its own. To get images that survive a restart, pass a disk-caching
provider as imageProvider.
Popups and full-screen messages
A ready popup (a dialog) or full-screen message waits until an AppMessagesScope is mounted, its
navigator exists, and the app is in the foreground. It is never dropped for being early. Only one
shows at a time; when it closes, the next one comes. A popup shown this session isn’t shown again
this session. A newer decide replaces a waiting popup for its placement, or withdraws it.
The inbox
Every inbox card a decide hands the user is kept on the device, newest first, for 30 days, at most 50 cards.
- Opening a card marks it read and reports a click. Deleting one reports a dismissal and removes it. Read state is local to the device.
- The inbox belongs to one user: identifying someone else, or
reset(), empties it. - A card whose image isn’t cached (offline) is left out of the list until it is.
AppMessages.inbox(aValueListenable) andunreadCountare there for a badge, or for an inbox of your own.
Drawing messages yourself
Use AppMessages.decide() (the ready messages, deciding first if the session hasn’t),
AppMessages.messages (a stream of them) and messageFor. Report what happened with
reportImpression, reportClick and reportDismiss(messageId). Pass presentPopups: false so the
scope leaves popups to you.
Reporting
- An impression is reported when the message is actually laid out, a click from its call to action
(the link goes to your
onOpenUrl), a dismissal from closing it. Each is reported once per message per session. - Each event gets its own id when it happens, so a resent batch counts once.
- Events are queued on the device (at most 500, for 7 days) and sent in batches of 50. Offline, or when asked to wait, they’re kept and sent later.
Track and consent
AppMessages.track('cart_viewed', {'items': 2}) and AppMessages.setConsent('in_app', true) go to
the user’s profile with profilesKey. Consent is strict: a user who never granted in_app
gets nothing from a decide, even for a message meant for everyone.
Push
The SDK never registers a push handler, so it can’t fight firebase_messaging or
flutter_local_notifications for one. It owns no notification channel and shows no notification.
- Your server asks which push to send (with a secret
isk_…key) and copies the answer’spush_datainto the FCM message’sdata, unchanged. - When the user opens the notification, pass its data map to
AppMessages.handlePush(data), fromFirebaseMessaging.onMessageOpenedApp,getInitialMessage(), or yourflutter_local_notificationstap handler. - For a Nida push, the SDK reports the click, calls
onPushOpened(messageId, placement)and returns true. Any other push returns false and is left alone.
A push handed over before configure (a launch from the notification) waits for it. With nobody
identified, you still get the callback, but no click is reported.
Test devices
AppMessagesDebugView shows the install id. Add the tester’s user id and that install id in
Test devices. A test device gets drafts marked for test devices, each with a small “Test” badge
(AppMessage.test). The tester grants in_app consent like anyone.
Language
Decides ask for en or ar (by default, the device’s language). An app with its own language
switch calls AppMessages.setLanguage('ar'), which decides once more in that language. A message’s
own texts run in its direction; the SDK’s few words, such as Close and Test, follow the app’s
locale.