Skip to content

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) and AppMessages.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 (a ValueListenable) and unreadCount are 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.

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.

  1. Your server asks which push to send (with a secret isk_… key) and copies the answer’s push_data into the FCM message’s data, unchanged.
  2. When the user opens the notification, pass its data map to AppMessages.handlePush(data), from FirebaseMessaging.onMessageOpenedApp, getInitialMessage(), or your flutter_local_notifications tap handler.
  3. 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.