Skip to Content

Push primer (soft-ask)

A push primer is a soft-ask: an in-app card that asks the user whether they want notifications before the operating system’s hard permission prompt appears. The native iOS and Android renderers present the card, and only when the user taps “Allow” do they trigger the real system prompt. If the user taps “Not right now”, the system prompt is never shown — so a decline never burns the one-shot OS permission dialog.

Operators author the primer as an in-app campaign with the render type push_primer. The native side (InAppPushPrimer.swift / InAppPushPrimer.kt) handles the card UI, the system prompt, and cooldown bookkeeping. The Dart layer in the Active Reach Flutter SDK (active_reach_sdk) is purely an observer — you subscribe to lifecycle events, check authorization state, and read or clear the cooldown.

You don’t render or trigger the primer from Dart. The native renderer shows it when the matching push_primer in-app campaign targets the user. The Dart API exists so your app can react to the outcome.

The event stream

AegisPushPrimer.instance.onEvent(...) subscribes to the six canonical events the native renderer emits. It returns a StreamSubscription<PushPrimerEvent> — call .cancel() to unsubscribe.

import 'dart:async'; import 'package:active_reach_sdk/active_reach_sdk.dart'; StreamSubscription<PushPrimerEvent>? _primerSub; void startListening() { _primerSub = AegisPushPrimer.instance.onEvent((event) { switch (event.eventName) { case PushPrimerEventName.inAppDisplayed: // soft-ask card shown break; case PushPrimerEventName.allowTapped: // user tapped "Allow" — system prompt is about to appear break; case PushPrimerEventName.permissionGranted: print('Opted in for campaign ${event.campaignId}'); break; case PushPrimerEventName.permissionDenied: // user declined the system prompt break; case PushPrimerEventName.laterTapped: // user tapped "Not right now" — cooldown starts break; case PushPrimerEventName.inAppDismissed: // card dismissed without an explicit choice break; } }); } void dispose() { _primerSub?.cancel(); }

PushPrimerEvent

Each callback receives a PushPrimerEvent:

class PushPrimerEvent { final PushPrimerEventName eventName; final String campaignId; final Map<String, dynamic> payload; }

PushPrimerEventName

The six canonical events, with the wire names the native side emits:

Dart valueWire nameMeaning
inAppDisplayedin_app.displayedThe soft-ask card was shown.
inAppDismissedin_app.dismissedThe card was dismissed without an explicit tap.
allowTappedpush_primer.allow_tappedUser tapped “Allow”; system prompt follows.
laterTappedpush_primer.later_tappedUser tapped “Not right now”; cooldown begins.
permissionGrantedpush_primer.permission_grantedThe OS permission was granted.
permissionDeniedpush_primer.permission_deniedThe OS permission was denied.

Skip the primer when already authorized

If the user has already granted notification permission, there’s no reason to show the soft-ask. Check isAlreadyAuthorized() before relying on the primer:

final authorized = await AegisPushPrimer.instance.isAlreadyAuthorized(); if (authorized) { // Already opted in — no primer needed. }

Cooldown

When a user taps “Not right now”, the native side starts a cooldown so the primer isn’t shown again immediately. You can read and clear that state from Dart — clearing is typically reserved for operator opt-in reset or debug flows.

// True while the cooldown window from the last "Not right now" is active. final cooling = await AegisPushPrimer.instance.isInCooldown(); // Reset the cooldown (operator opt-in reset / debug only). await AegisPushPrimer.instance.clearCooldown();

Platform channels

The bridge uses two channels, both already wired by the SDK:

  • MethodChannel('active_reach_sdk/push_primer')isInCooldown, clearCooldown, isAlreadyAuthorized.
  • EventChannel('active_reach_sdk/push_primer_events') — the lifecycle event stream.

The native iOS and Android renderers present both the in-app card and the system permission prompt; you don’t call these channels directly — use AegisPushPrimer.instance.

What’s next