reteno_plugin 1.11.1 copy "reteno_plugin: ^1.11.1" to clipboard
reteno_plugin: ^1.11.1 copied to clipboard

The Reteno Flutter Plugin for Mobile Customer Engagement and Analytics solutions

reteno_plugin #

Reteno Flutter SDK

Documentation #

Official documentation Migration guide

Installation #

1. Add package #

flutter pub add reteno_plugin

2. Initialize SDK in Flutter #

import 'package:reteno_plugin/reteno.dart';

await Reteno().initialize(
  accessKey: '<your_access_key>',
  options: RetenoInitOptions(
    lifecycleTrackingOptions: LifecycleTrackingOptions.all(),
    isDebug: false,
    deviceTokenHandlingMode: RetenoDeviceTokenHandlingMode.automatic,
  ),
);

An optional customDeviceId provider is awaited exactly once before the native initialization call on both Android and iOS. On iOS 1.11.0, initialize also verifies that native Reteno is ready before its future completes:

await Reteno().initialize(
  accessKey: '<your_access_key>',
  customDeviceId: () async => await loadStableDeviceId(),
);

The provider may return null to use the platform default device ID. A custom ID must be non-empty and stable between launches; a blank value is rejected before native initialization. Enabling it for an existing iOS installation can replace the previously stored default ID and update the device identity associated with that installation.

3. Request push permission #

final granted = await Reteno().requestPushPermission();

4. Verify integration diagnostics #

final issues = await Reteno().diagnose();
// [] means integration checks passed

iOS requirements #

  1. Add NotificationServiceExtension and follow iOS guide Step 1: https://docs.reteno.com/reference/ios#step-1-add-the-notification-service-extension
  2. The plugin already pins Reteno 2.7.3 for the app target. Add the same pod version only to each Reteno notification extension target.
  3. Configure App Group for app + extension.
  4. Do not initialize Reteno manually from AppDelegate when using this Flutter plugin. Initialization should be done via Reteno().initialize(...).
  5. For push image carousel/GIF UI on iOS, add NotificationContentExtension with RetenoCarouselNotificationViewController and categories ImageCarousel/ImageGif.

NotificationServiceExtension + App Group are required for full iOS Reteno push functionality.

iOS notification center delegate #

The plugin owns UNUserNotificationCenter.current().delegate. It installs a coordinator that handles Reteno notifications and forwards every callback to the delegate that was already installed, so application and FlutterFire handling keeps working.

The application does not need to assign that delegate at all. If it must (for another SDK), assign it before GeneratedPluginRegistrant.register(with:):

override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
    // Assign your delegate first, so Reteno can chain to it.
    UNUserNotificationCenter.current().delegate = self

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

Do not assign it after registration: that assignment replaces the Reteno coordinator and detaches Reteno from notification taps. The symptom is a push that is delivered and reported as DELIVERED, while CLICKED never arrives and onRetenoNotificationReceived never fires — delivery is reported by the notification service extension in a separate process, while the tap goes to the application delegate instead of Reteno.

Since 1.11.1 the plugin reclaims delegate ownership after registration (next main runloop turn, launch completion, initialize(), every foreground) and chains the delegate the application left behind, so the assignment order no longer breaks push tracking. A delegate that is still not owned by the plugin when Reteno().diagnose() runs is reported as NOTIFICATION_DELEGATE_OVERRIDDEN, and repeated takeovers are logged as a [Reteno] warning in the device console.

iOS token handling mode #

Choose the mode based on the notification service configured for the Reteno mobile app:

Reteno mobile app configuration iOS mode
APN for iOS, or FCM for Android + APN for iOS RetenoDeviceTokenHandlingMode.automatic
FCM-only, token managed by the Reteno Flutter plugin RetenoDeviceTokenHandlingMode.manual
FCM-only, token managed by the application RetenoDeviceTokenHandlingMode.external

In automatic mode the plugin reports the APNs device token through the native Reteno iOS SDK. In manual mode the plugin keeps APNs registration for Firebase Messaging, but reports only the FCM registration token to Reteno. In external mode the plugin does not assign a Firebase Messaging delegate, request an FCM token, or set Firebase's APNs token. The application owns the Firebase integration and supplies the FCM token through setPushToken.

Starting with 1.11.0, token source is retained until the selected mode is known: an early APNs callback is discarded for manual/external and can never be flushed as an FCM token. Reteno notification foreground, click, and action processing is chained with FlutterFire/application delegates in all three modes. manual also preserves the existing Firebase Messaging delegate.

Explicit FCM token synchronization

Use external mode when the application already sends the FCM token to its own backend and must be the single owner of Firebase token generation:

final reteno = Reteno();

await reteno.initialize(
  accessKey: '<your_access_key>',
  options: const RetenoInitOptions(
    deviceTokenHandlingMode: RetenoDeviceTokenHandlingMode.external,
  ),
);

Future<void> syncCurrentFcmToken() async {
  // On Apple platforms, do not request an FCM token until APNs registration
  // has completed. If this is null, retry when the app resumes.
  final apnsToken = await FirebaseMessaging.instance.getAPNSToken();
  if (apnsToken == null) return;

  final token = await FirebaseMessaging.instance.getToken();
  if (token != null) {
    await reteno.setPushToken(token);
  }
}

FirebaseMessaging.instance.onTokenRefresh.listen((token) async {
  await reteno.setPushToken(token);
});

final granted = await reteno.requestPushPermission();
if (granted) await syncCurrentFcmToken();

Call setPushToken after user identification and whenever Firebase refreshes the token. Retry syncCurrentFcmToken() when the app resumes if APNs was not ready, or use the bounded retry helper in the full iOS integration guide. Pass the FCM token, not the APNs token. No native MessagingDelegate or direct Reteno native API call is needed.

setPushToken is supported on both platforms, but external token ownership is an iOS mode. Android already handles FCM tokens automatically, so an application-side synchronization there is normally redundant.

Android requirements #

  1. Enable AndroidX and complete Firebase setup from Android guide: https://docs.reteno.com/reference/android-sdk-setup
  2. minSdkVersion must be at least 21.
  3. Do not add custom FirebaseMessagingService only for Reteno. The plugin handles Reteno push service wiring internally.

Android notification grouping #

Reteno push notifications can use a constant Android group:

await Reteno().setNotificationGroupingRule(
  const RetenoNotificationGroupingRule.constant('orders'),
);

Or derive the group ID from a push payload field:

await Reteno().setNotificationGroupingRule(
  const RetenoNotificationGroupingRule.byPayloadKey(
    'orderNumber',
    fallbackGroupId: 'orders',
  ),
);

Pass null to disable grouping. The rule is stored on Android and remains available when a push arrives without a running Flutter engine. The rule sets the notification group key but does not create a group summary notification; the application remains responsible for custom summary behavior.

Android advanced setup (if your app already has custom FirebaseMessagingService) #

Use this only when you already have app-specific FCM logic and need to keep it.

Rules:

  1. Your app must have exactly one app-level MESSAGING_EVENT service.
  2. That service should inherit from RetenoFirebaseMessagingServiceBridge.
  3. onNewToken and onMessageReceived must call super.
  4. Remove plugin bridge service from manifest merge in the app module.

Manifest example:

<application xmlns:tools="http://schemas.android.com/tools">
    <service
        android:name="com.reteno.reteno_plugin.RetenoFirebaseMessagingServiceBridge"
        tools:node="remove" />

    <service
        android:name=".MyMessagingService"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
</application>

Service example:

import com.google.firebase.messaging.RemoteMessage
import com.reteno.reteno_plugin.RetenoFirebaseMessagingServiceBridge

class MyMessagingService : RetenoFirebaseMessagingServiceBridge() {
    override fun onNewToken(token: String) {
        super.onNewToken(token)
        // your token logic
    }

    override fun onMessageReceived(message: RemoteMessage) {
        super.onMessageReceived(message)
        if (!isRetenoMessage(message)) {
            // your non-Reteno message logic
        }
    }
}

Diagnose codes #

Reteno().diagnose() may return:

Code Meaning
SDK_NOT_INITIALIZED Reteno SDK has not been initialized yet
NOTIFICATIONS_DISABLED Notifications are disabled in OS settings
PUSH_PERMISSION_DENIED Notification runtime permission denied
PUSH_PERMISSION_NOT_DETERMINED iOS permission dialog not shown yet
REMOTE_NOTIFICATIONS_NOT_REGISTERED iOS permission granted but APNs registration not completed
NOTIFICATION_DELEGATE_OVERRIDDEN iOS UNUserNotificationCenter delegate is not the Reteno coordinator, so notification taps are not reported. Assign the application delegate before GeneratedPluginRegistrant.register(with:)
FCM_MESSAGING_SERVICE_MISSING Android has no active MESSAGING_EVENT handler
RETENO_MESSAGING_SERVICE_MISSING Android has no Reteno push handler among MESSAGING_EVENT services
FCM_MESSAGING_SERVICE_CONFLICT Multiple app-level MESSAGING_EVENT services detected on Android
FCM_TOKEN_MISSING Android SDK cannot get current FCM token
FCM_TOKEN_FETCH_FAILED Android failed to fetch FCM token from Firebase SDK

Push troubleshooting (Android) #

If Reteno Console says contact has no app token:

  1. Ensure Reteno().initialize(...) is called on app startup before user update calls.
  2. Request push permission (Reteno().requestPushPermission()).
  3. Call Reteno().setUserAttributes(userExternalId: ...) for the same contact you target in Console.
  4. Run Reteno().diagnose() and resolve all returned codes.
  5. Verify app has only one effective app-level MESSAGING_EVENT service (or bridge + proper merge setup).

Push notifications #

Getting initial notification #

When you instantiate the app by clicking on a push notification, you may need to get its payload. To do it, use the getInitialNotification method:

import 'package:reteno_plugin/reteno.dart';

final Reteno reteno = Reteno();

reteno.getInitialNotification().then((Map<String, dynamic>? payload) {
      // Process payload...
});

Listening for new push notifications in an open app #

When the app is open, you may need to track for new push notifications. For that, the plugin provides the onRetenoNotificationReceived stream you can listen to:

import 'package:reteno_plugin/reteno.dart';

Reteno.onRetenoNotificationReceived.listen((Map<String, dynamic> payload) {
      // Process payload...
});

Getting notification data when interacting with push #

When you want to handle interaction with received notification you can use onRetenoNotificationClicked stream, so you can listen to it and receive notification data:

import 'package:reteno_plugin/reteno.dart';

Reteno.onRetenoNotificationClicked.listen((Map<String, dynamic> payload) {
      // Process payload...
});

Push notification actions #

When you want to handle push notification actions you can use onUserNotificationAction stream, so you can listen to it and receive notification action data:

import 'package:reteno_plugin/reteno.dart';

    Reteno.onUserNotificationAction.listen((event) {
      print('$_retenoPluginLogTag: onUserNotificationAction: ${event.toString()}');
    });

Tracking user behaviour #

Track Custom Events #

Reteno SDK provides ability to track custom events.

Reteno().logEvent({required RetenoCustomEvent event});
Custom Event model:
class RetenoCustomEvent {
  RetenoCustomEvent({
    required this.eventTypeKey,
    required this.dateOccurred,
    this.forcePush = false,
    required this.parameters,
  });

  final String eventTypeKey;
  final DateTime dateOccurred;
  final List<RetenoCustomEventParameter> parameters;
  final bool forcePush;
}
  • eventTypeKey - key qualifier of the event (provided by appDeveloper, confirmed by marketing team).

  • dateOccurred - time when event occurred (Date should be in ISO8601 format)

  • parameters - list of parameters, which describe the event

  • forcePush - iOS-only feature; Please read more about it here

Parameter model containt key/value fields to describe the parameter:
class RetenoCustomEventParameter {
  RetenoCustomEventParameter(this.name, this.value);

  final String name;
  final String? value;
}
Example of usage:
final dateOccured = DateTime.now();
final event = RetenoCustomEvent(
  eventTypeKey: eventTypeName,
  dateOccurred: dateOccured,
  parameters: [
    RetenoCustomEventParameter(<parameter_name>, <parameter_value>)
  ],
  forcePush: true|false,
);

await Reteno().logEvent(event: event);

Tracking user information #

You can track the user related information using external user ID and/or user attributes. For that, you have to add userExternalId and setUserAttributes to Reteno Flutter.

Adding External User ID #

External user ID is an ID you have assigned to a user in your system (external for 'Reteno'). You can add these in Reteno Flutter using the following method:

import 'package:reteno_plugin/reteno.dart';

final reteno = Reteno();
reteno.setUserAttributes(
  userExternalId: 'USER_ID',
  user: userInfo,
);

Adding User Attributes #

User attributes define the information you collect about contacts in your user base. For example, phone number, email address, language preference, geographic location.

You can add user attributes using the following method:

import 'package:reteno_plugin/reteno.dart';

final reteno = Reteno();
reteno.setUserAttributes(
  userExternalId: 'USER_ID',
  user: userInfo,
);

Structure of the RetenoUser object:

class RetenoUser {
  RetenoUser({
    this.subscriptionKeys,
    this.userAttributes,
    this.groupNamesExclude,
    this.groupNamesInclude,
  });

  final UserAttributes? userAttributes;
  final List<String>? subscriptionKeys;
  final List<String>? groupNamesInclude;
  final List<String>? groupNamesExclude;
}

class UserAttributes {
  UserAttributes({
    this.lastName,
    this.firstName,
    this.address,
    this.email,
    this.fields,
    this.languageCode,
    this.marketId,
    this.phone,
    this.timeZone,
  });
  final String? phone;
  final String? email;
  final String? firstName;
  final String? lastName;
  final String? languageCode;
  final String? marketId;
  final String? timeZone;
  final Address? address;
  final List<UserCustomField>? fields;
}

class Address {
  Address({
    this.address,
    this.postcode,
    this.region,
    this.town,
  });
  final String? region;
  final String? town;
  final String? address;
  final String? postcode;
}

class UserCustomField {
  UserCustomField({
    required this.key,
    this.value,
  });
  final String key;
  final String? value;
}

Anonymous User Attributes #

Available for reteno_plugin starting from version 1.1.0 Reteno plugin allows tracking anonymous user attributes (no externalUserId required). To set user attributes without externalUserId use method setAnonymousUserAttributes():

Reteno.setAnonymousUserAttributes(AnonymousUserAttributes attributes);

**AnonymousUserAttributes** model:

```dart
class AnonymousUserAttributes {
  AnonymousUserAttributes({
    this.lastName,
    this.firstName,
    this.address,
    this.fields,
    this.languageCode,
    this.marketId,
    this.timeZone,
  });
  final String? firstName;
  final String? lastName;
  final String? languageCode;
  final String? marketId;
  final String? timeZone;
  final Address? address;
  final List<UserCustomField>? fields;
}

Note: you can't provide anonymous user attributes with phone or/and email. For that purpose use setUserAttributes() method with externalUserId

Note

LanguageCode

The tags for identifying language must be in compliance with RFC 5646. The primary language subtag must comply with the ISO 639-1 format. Example: de-AT.

TimeZone

The time zone format must comply with TZ database. Example: Europe/Kyiv.

MarketId

Max length is 64 characters. Allowed characters are latin letters, digits, -, and _. Pass an empty string to clear the value; null values are ignored by the native SDK.

Note In versions before 1.2.2 you can not use sentry_fltter due to sentry iOS versions mismatch. If you want to use sentry_flutter in your application. Please use flutter_plugin : 1.2.2 and sentry_flutter: 7.8.0

In-App Messages #

Pausing and Resuming In-App Messages #

You can pause and resume in-app messages using the following methods:

// Pause in-app messages
Reteno.pauseInAppMessages(true);

// Resume in-app messages
Reteno.pauseInAppMessages(false);

Listening for In-App Message Status Changes #

  Reteno.onInAppMessageStatusChanged.listen((status) {
      switch (status) {
        case InAppShouldBeDisplayed():
          print('In-app should be displayed');
        case InAppIsDisplayed():
          print('In-app is displayed');
        case InAppShouldBeClosed(:final action):
          print('In-app should be closed $action');
        case InAppIsClosed(:final action):
          print('In-app is closed $action');
        case InAppReceivedError(:final errorMessage):
          print('In-app error: $errorMessage');
      }
    });

You can personalize the user experience and increase sales by adding recommendations of your goods and services to an app.

  final recommendations = await Reteno.getRecommendations(
    recomenedationVariantId: 'r1107v1482',
    productIds: ['240-LV09', '24-WG080'],
    categoryId: 'Default Category/Training/Video Download',
    filters: [RetenoRecomendationFilter(name: 'filter_name', values: ['filter_value'])],
    fields: ['productId', 'name', 'descr', 'imageUrl', 'price'],
  );

Reteno App Inbox #

Downloading new messages #

final messages = await Reteno.appInbox.getAppInboxMessages();

Marking message as opened #

await Reteno.appInbox.markAsOpened(message.id);

Marking all messages as opened #

await Reteno.appInbox.markAllMessagesAsOpened();

Obtaining messages count #

final count = await Reteno.appInbox.getAppInboxMessagesCount();

Subscribing to messages count changes #

Reteno.appInbox.onMessagesCountChanged.listen((count) {
  print(count);
});

License #

MIT

3
likes
140
points
1.91k
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

The Reteno Flutter Plugin for Mobile Customer Engagement and Analytics solutions

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on reteno_plugin

Packages that implement reteno_plugin