relateddigital_flutter 0.8.3
relateddigital_flutter: ^0.8.3 copied to clipboard
Related Digital Official Flutter SDK for analytics, recommendation and push notifications.
Table of Contents #
- Table of Contents
- Introduction
- Installation
- Usage
Introduction #
This library is the official Flutter SDK of Related Digital.
Requirements #
- iOS 15.0 or later
- Android API level 21 or later
Installation #
- Edit your project's
pubspec.yamlfile:
dependencies:
relateddigital_flutter: ^0.8.3
-
Run
flutter pub get -
Import the package:
import 'package:relateddigital_flutter/relateddigital_flutter.dart';
Platform-Integration #
Android #
- Add below line to your
android/build.gradlefile's bothrepositoriessections.
maven {url 'https://jitpack.io'}
- Add the following lines to the
dependenciessection inproject/build.gradle
classpath 'com.google.gms:google-services:4.3.10'
- Add the following lines to the end of
app/build.gradle
apply plugin: 'com.google.gms.google-services'
- If you are using Kotlin, add the following line to
settings.gradle.ktspluginssection.
plugins {
...
id("com.google.gms.google-services") version "4.4.0" apply false
}
-
Change your minSdkVersion to 21.
-
Change your targetSdkVersion and compileSdkVersion to 34.
-
Do not add
EuroFirebaseMessagingServiceor any othercom.google.firebase.MESSAGING_EVENTservice for Related Digital. The plugin already registerscom.relateddigital.flutter.RelatedDigitalMessagingServicethrough its own AndroidManifest. Adding a second FCM service in your app manifest will compete with it and one of the two will silently stop receiving messages. If an older README led you to declareEuroFirebaseMessagingServicein the app, remove it. -
If your app already uses
firebase_messagingor another FCM library, skip this default service and follow Using with other push providers (FCM or any other provider). -
Add below meta-data parameters in your AndroidManifest.xml
<meta-data android:name="GoogleAppAlias" android:value="google-app-alias" />
<meta-data android:name="HuaweiAppAlias" android:value="huawei-app-alias" />
<meta-data android:name="VisilabsOrganizationID" android:value="VisilabsOrganizationID" />
<meta-data android:name="VisilabsSiteID" android:value="VisilabsSiteID" />
<meta-data android:name="VisilabsSegmentURL" android:value="https://lgr.visilabs.net" />
<meta-data android:name="VisilabsDataSource" android:value="VisilabsDataSource" />
<meta-data android:name="VisilabsRealTimeURL" android:value="https://rt.visilabs.net" />
<meta-data android:name="VisilabsChannel" android:value="Android" />
<meta-data android:name="VisilabsGeofenceURL" android:value="https://s.visilabs.net/geojson" />
<meta-data android:name="VisilabsGeofenceEnabled" android:value="true" />
<!-- Parameters below are optional -->
<meta-data android:name="VisilabsRequestTimeoutInSeconds" android:value="30" />
<meta-data android:name="VisilabsRESTURL" android:value="VisilabsRESTURL" />
<meta-data android:name="VisilabsEncryptedDataSource" android:value="VisilabsEncryptedDataSource" />
<meta-data android:name="VisilabsTargetURL" android:value="https://s.visilabs.net/json" />
<meta-data android:name="VisilabsActionURL" android:value="https://s.visilabs.net/actjson" />
-
Add
google-services.jsonfile to your application’sappdirectory. -
If your app supports
HMSaddagconnect-services.jsonfile to your application’sappdirectory.
iOS #
- Change the ios platform version to 15.0 or higher in
Podfile
platform :ios, '15.0'
-
In your project directory, open the file
ios/Runner.xcworkspacewith Xcode. -
Enable
Push NotificationsandBackground Modes->Remote Notificationscapabilities.

-
In Xcode, add a new Notification Service Extension target and name it NotificationService. NotificationServiceExtension allows your iOS application to receive rich notifications with images, videos, and badges. It's also required for Related Digital's analytics features and to store and access notification payloads of the last 30 days.
-
In your podfile, add below section and then run
pod install.
target 'NotificationService' do
use_frameworks!
pod 'Euromsg'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'No'
end
end
end
-
Set NotificationService target's deployment target to iOS 11.
-
Replace NotificationService.swift file content with the code below.
import UserNotifications
import Euromsg
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
Euromsg.configure(appAlias: "ios-app-alias", enableLog: true)
Euromsg.didReceive(bestAttemptContent, withContentHandler: contentHandler)
}
override func serviceExtensionTimeWillExpire() {
guard let contentHandler = self.contentHandler else {
return;
}
guard let bestAttemptContent = self.bestAttemptContent else {
return;
}
contentHandler(bestAttemptContent)
}
}
If the app also uses Firebase Cloud Messaging or another push SDK, do not use this extension as-is. Follow Using with other push providers (FCM or any other provider) so non-Related Digital payloads are forwarded to the other provider.
-
Enable
App GroupsCapability for your targets. App Groups allow your app to execute code when a notification is recieved, even if your app is not active. This is required for Related Digital's analytics features and to store and access notification payloads of the last 30 days.- In your Main App Target go to
Signing & Capabilities > All. - Click
+ Capabilityif you do not have App Groups in your app yet. - Select App Groups.
- Under App Groups click the
+button. - Set the
App Groupscontainer to begroup.BUNDLE_ID.relateddigitalwhereBUNDLE_IDis the same as set inBundle Identifier. - Press OK.
- In the NotificationServiceExtension Target
- Go to
Signing & Capabilities > All - Click
+ Capabilityif you do not have App Groups in your app yet. - Select App Groups
- In the NotificationContentExtension Target go to
Signing & Capabilities> All`. - Click
+ Capability. - Select App Groups
- Under App Groups click the
+button. - Set the
App Groupscontainer to begroup.BUNDLE_ID.relateddigitalwhereBUNDLE_IDis the same as your Main App TargetBundle Identifier. Do Not IncludeNotificationServiceExtensionandNotificationContentExtension. - Press OK
- In your Main App Target go to


- If you want to use AdvertisingTrackingID with isIDFAEnabled parameter (see Usage below), you need to add this key to your Info.plist file for iOS 14 and above.
<key>NSUserTrackingUsageDescription</key>
<string>We use advertising identifier!</string>
Troubleshooting
Xcode Build Cycle Error
If you encounter this error:
Error (Xcode): Cycle inside Runner; building could produce unreliable results.
Cause: This occurs when build phases are in the wrong order, creating a circular dependency:
- "Thin Binary" script runs first
- "Embed Foundation Extensions" runs after
Solution: Reorder the Runner build phases:
- Open your Xcode project
- Select the Runner target
- Go to Build Phases
- Reorder the phases so that:
- Embed Foundation Extensions comes first
- Thin Binary comes after
Usage #
Initializing #
Import the library
import 'package:relateddigital_flutter/relateddigital_flutter.dart';
import 'package:relateddigital_flutter/request_models.dart';
import 'package:relateddigital_flutter/response_models.dart';
Initialize the library
final RelateddigitalFlutter relatedDigitalPlugin = RelateddigitalFlutter();
@override
void initState() {
super.initState();
initLib();
}
Future<void> initLib() async {
var initRequest = RDInitRequestModel(
appAlias: Platform.isIOS ? 'ios-app-alias' : 'android-app-alias',
huaweiAppAlias: 'huawei-app-alias', // pass empty String if your app does not support HMS
androidPushIntent: 'com.test.MainActivity', // Android only
organizationId: 'ORG_ID',
siteId: 'SITE_ID',
dataSource: 'DATA_SOURCE',
maxGeofenceCount: 20, // iOS only
geofenceEnabled: true,
inAppNotificationsEnabled: true,
logEnabled: true,
isIDFAEnabled: true, // iOS only
useNotificationLargeIcon: true,
androidIconName: 'ic_launcher',
);
await relatedDigitalPlugin.init(initRequest, _readNotificationCallback);
}
void _readNotificationCallback(dynamic result) {
print(result);
}
Push Notifications #
Requesting Permission & Retrieving Token #
Add the lines below to request push notification permission and retrieve token.
IOS
- Instead of having to prompt the user for permission to send them push notifications, your app can request provisional authorization. In order to enable provisional authorization, you should set
isProvisionalparameter ofrequestPermissionmethod totrue.
String token = '-';
void _getTokenCallback(RDTokenResponseModel result) {
if(result != null && result.deviceToken != null && result.deviceToken.isNotEmpty) {
setState(() {
token = result.deviceToken;
});
}
else {
setState(() {
token = 'Token not retrieved';
});
}
}
Future<void> requestPermission() async {
await relatedDigitalPlugin.requestPermission(_getTokenCallback, isProvisional: true);
}
Carousel Push Notifications #
To be able to receive push notifications with carousel, follow the steps below.
IOS
- In Xcode, add a new Notification Content Extension target and name it NotificationContent.
- In your podfile, add below section and then run
pod install.
target 'NotificationContent' do
use_frameworks!
pod 'Euromsg'
end
- Set NotificationContent target's deployment target to iOS 11.
- Delete MainInterface.storyboard and NotificationContent.swift files. Then create a swift file named EMNotificationViewController.swift under the NotificationContent folder.
- Replace EMNotificationViewController.swift file content with the code below.
import UIKit
import UserNotifications
import UserNotificationsUI
import Euromsg
@objc(EMNotificationViewController)
class EMNotificationViewController: UIViewController, UNNotificationContentExtension {
let carouselView = EMNotificationCarousel.initView()
var completion: ((_ url: URL?, _ bestAttemptContent: UNMutableNotificationContent?) -> Void)?
var notificationRequestIdentifier = ""
func didReceive(_ notification: UNNotification) {
notificationRequestIdentifier = notification.request.identifier
Euromsg.configure(appAlias: "relateddigital-flutter-example-ios", launchOptions: nil, enableLog: true)
carouselView.didReceive(notification)
}
func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {
carouselView.didReceive(response, completionHandler: completion)
}
override func loadView() {
completion = { [weak self] url, bestAttemptContent in
if let identifier = self?.notificationRequestIdentifier {
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [identifier])
UNUserNotificationCenter.current().getDeliveredNotifications(completionHandler: { notifications in
bestAttemptContent?.badge = NSNumber(value: notifications.count)
})
}
if let url = url {
if #available(iOSApplicationExtension 12.0, *) {
self?.extensionContext?.dismissNotificationContentExtension()
}
self?.extensionContext?.open(url)
} else {
if #available(iOSApplicationExtension 12.0, *) {
self?.extensionContext?.performNotificationDefaultAction()
}
}
}
carouselView.completion = completion
carouselView.delegate = self
self.view = carouselView
}
}
/**
Add if you want to track which carousel element has been selected
*/
extension EMNotificationViewController: CarouselDelegate {
func selectedItem(_ element: EMMessage.Element) {
// Add your work...
print("Selected element is => \(element)")
}
}
- In your NotificationContent/Info.plist add below section
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>UNNotificationExtensionCategory</key>
<string>carousel</string>
<key>UNNotificationExtensionDefaultContentHidden</key>
<false />
<key>UNNotificationExtensionInitialContentSizeRatio</key>
<real>1</real>
<key>UNNotificationExtensionUserInteractionEnabled</key>
<true />
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.content-extension</string>
<key>NSExtensionPrincipalClass</key>
<string>NotificationContent.EMNotificationViewController</string>
</dict>
Set Push Permit #
You can only call setNotificationPermission method to enable or disable push notifications for the application.
relatedDigitalPlugin.setNotificationPermission(true);
Get-Payload-List #
You can access payload list of last 30 days if you have completed iOS NotificationServiceExtension and App Groups setup. Using getPushMessages method you can access these payloads
PayloadListResponse payloadListResponse = await relatedDigitalPlugin.getPushMessages();
Using with other push providers (FCM or any other provider) #
Use this section when the app already has its own push stack (firebase_messaging, a custom FirebaseMessagingService, or similar) and Related Digital push.
Android delivers com.google.firebase.MESSAGING_EVENT to one FirebaseMessagingService. iOS allows one UNUserNotificationCenterDelegate. Intent-filter priority does not make two FCM services work together; it only picks a winner. The supported integration is: your app owns the FCM service / notification delegate, and forwards Related Digital payloads to this SDK.
Do not call both relatedDigitalPlugin.requestPermission and another SDK's permission API for the same prompt. Request permission from one place; both stacks can still receive the resulting APNs/FCM token.
If you only use Related Digital push, skip this section and keep the default Android service from Android.
Send Related Digital campaigns with the token from requestPermission (iOS: APNs hex, Android: FCM). Send Firebase Console tests with the FCM token from FirebaseMessaging.instance.getToken() — never paste the iOS APNs hex into Firebase.
This SDK renders Related Digital rich push (image, carousel). Foreground FCM banners and FCM images are your existing FCM code; the helper does not draw them.
Android
-
Add
xmlns:tools="http://schemas.android.com/tools"to the<manifest>tag if it is not already there. -
Remove the plugin FCM service so your service is the only one that receives messages. If you extend
FlutterFirebaseMessagingService, also remove FlutterFire's default service so you do not end up with two services again.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application>
<service
android:name="com.relateddigital.flutter.RelatedDigitalMessagingService"
tools:node="remove" />
<!-- Only if your router extends FlutterFirebaseMessagingService -->
<service
android:name="io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService"
tools:node="remove" />
<service
android:name=".RouterFCMService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
</application>
</manifest>
- Create
RouterFCMServicenext toMainActivity. Related Digital messages must be handled natively (carousel / image). Other messages go to your existing provider. The Java sample is the same API in Kotlin.
firebase_messaging (recommended when you already use FlutterFire):
package com.yourapp;
import androidx.annotation.NonNull;
import com.google.firebase.messaging.RemoteMessage;
import com.relateddigital.flutter.RelatedDigitalFCMHelper;
import io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingService;
public class RouterFCMService extends FlutterFirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
if (RelatedDigitalFCMHelper.isRelatedDigitalMessage(remoteMessage)) {
RelatedDigitalFCMHelper.onMessageReceived(this, remoteMessage);
return;
}
super.onMessageReceived(remoteMessage);
}
@Override
public void onNewToken(@NonNull String token) {
RelatedDigitalFCMHelper.onNewToken(this, token);
super.onNewToken(token);
}
}
Your own FirebaseMessagingService (no FlutterFire service):
package com.yourapp;
import androidx.annotation.NonNull;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.relateddigital.flutter.RelatedDigitalFCMHelper;
public class RouterFCMService extends FirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
if (RelatedDigitalFCMHelper.isRelatedDigitalMessage(remoteMessage)) {
RelatedDigitalFCMHelper.onMessageReceived(this, remoteMessage);
return;
}
// Existing FCM handling
}
@Override
public void onNewToken(@NonNull String token) {
RelatedDigitalFCMHelper.onNewToken(this, token);
// Existing token handling
}
}
Any other provider in the same router: keep a single service. After the Related Digital check, handle that provider’s payloads with its own API. Do not register a second FirebaseMessagingService next to this router. Always call RelatedDigitalFCMHelper.onNewToken in onNewToken in addition to the other provider’s token callback. The Android FCM token is shared; Related Digital and your own backend both subscribe to the same token.
iOS
If the host never assigns UNUserNotificationCenter.current().delegate, the plugin assigns itself so Related Digital-only apps keep receiving taps and foreground presentation. Flutter does not do this automatically.
If your app uses its own UNUserNotificationCenterDelegate (AppDelegate, firebase_messaging, or any other provider), set that delegate before super.application(...) so the plugin does not take it over.
super.userNotificationCenter(...) forwards the same call to every Flutter plugin in the chain (this SDK and firebase_messaging). This plugin already runs handlePush for Related Digital taps and always calls completionHandler. Call handlePush in AppDelegate only if you do not call super; calling both sends open tracking twice.
Pass a first-wins wrapper into super. Both plugins complete the same handler; iOS allows that only once. If you call completionHandler yourself, do not also call super for that invocation.
Native methods (use handlePush only when super is not called):
RelatedDigitalPushHandler.isRelatedDigitalPayload(_:)RelatedDigitalPushHandler.handlePush(_:)— click / open tracking (Euromsg.handlePush)RelatedDigitalPushHandler.registerToken(_:)— APNsDatafromdidRegisterForRemoteNotificationsWithDeviceToken, not the FCM string
On iOS, Related Digital uses the APNs device token. Firebase uses a different FCM token mapped from the same APNs token. Both can be registered. If you disable Firebase swizzling (FirebaseAppDelegateProxyEnabled = NO), set Messaging.messaging().apnsToken yourself. Put the APNs hex in the Related Digital panel and the FCM string in Firebase Console.
AppDelegate.swift when you own the notification delegate:
import UIKit
import Flutter
import UserNotifications
import relateddigital_flutter
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
UNUserNotificationCenter.current().delegate = self // before super, so the plugin does not take over
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
RelatedDigitalPushHandler.registerToken(deviceToken) // APNs Data only, not Firebase getToken()
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}
override func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
// Do not call handlePush here: the plugin does it for RD payloads when super runs.
super.userNotificationCenter(center, didReceive: response,
withCompletionHandler: firstCall(completionHandler))
}
override func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
super.userNotificationCenter(center, willPresent: notification,
withCompletionHandler: firstCall(completionHandler))
}
private func firstCall<T>(_ handler: @escaping (T) -> Void) -> (T) -> Void {
var done = false
return { value in
guard !done else { return }
done = true
handler(value)
}
}
private func firstCall(_ handler: @escaping () -> Void) -> () -> Void {
var done = false
return {
guard !done else { return }
done = true
handler()
}
}
}
If import relateddigital_flutter is not available from Runner, set Runner IPHONEOS_DEPLOYMENT_TARGET to 15.0 (or add pod 'Euromsg' to Runner and call Euromsg.handlePush(pushDictionary:) / Euromsg.registerToken(tokenData:)). Only forward when userInfo["emPushSp"] or userInfo["pushId"] is present, and then complete the handler yourself without super.
Call super in didRegisterForRemoteNotificationsWithDeviceToken so other Flutter plugins still receive the APNs token. For didReceive / willPresent, either super plus the first-wins wrapper above, or complete once yourself with no super — not both.
Notification Service Extension (rich push + other-provider images):
import UserNotifications
import Euromsg
import FirebaseMessaging
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else { return }
var userInfo = bestAttemptContent.userInfo
if userInfo["emPushSp"] != nil || userInfo["pushId"] != nil {
userInfo.removeValue(forKey: "fcm_options")
bestAttemptContent.userInfo = userInfo
Euromsg.configure(appAlias: "ios-app-alias", enableLog: true)
Euromsg.didReceive(bestAttemptContent, withContentHandler: contentHandler)
} else {
Messaging.serviceExtension().populateNotificationContent(bestAttemptContent,
withContentHandler: contentHandler)
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
Add pod 'Firebase/Messaging' (or the version your app already uses) to the NotificationService target next to pod 'Euromsg'. For any other provider, use that SDK’s NSE helper in the else branch instead of Messaging.serviceExtension().
If you only use Related Digital push, keep the default NotificationService example (Euromsg.didReceive for every payload). Do not add Firebase to the extension unless another provider needs it.
Carousel still needs the Notification Content Extension; the NSE else branch only attaches the other provider’s images.
Check: one Android MESSAGING_EVENT service and one iOS notification delegate; RD payloads (emPushSp / pushId) go to this SDK, everything else to your FCM or other-provider code; iOS RMC uses APNs hex, Firebase uses FCM token.
Data Collection #
Related Digital uses events to collect data from mobile applications. The developer needs to implement the methods provided by SDK. customEvent is a generic method to track user events. customEvent takes 2 parameters: pageName and properties.
- pageName : The current page of your application. If your event is not related to a page view, you should pass a value related to the event. If you pass an empty String the event would be considered invalid and discarded.
- parameters : A collection of key/value pairs related to the event. If your event does not have additional data apart from page name, passing an empty Map acceptable.
Some of the most common events:
Sign Up #
String userId = 'userId';
// optional
Map<String, String> properties = {
'OM.b_sgnp':'1'
};
await relatedDigitalPlugin.signUp(userId, properties: properties);
Login #
String userId = 'userId';
// optional
Map<String, String> properties = {
'OM.b_login':'1'
};
await relatedDigitalPlugin.login(userId, properties: properties);
Page View #
Use the following implementation of customEvent method to record the page name the visitor is currently viewing. You may add extra parameters to properties Map or you may leave it empty.
String pageName = 'Page Name';
Map<String, String> parameters = {};
await relatedDigitalPlugin.customEvent(pageName, parameters);
Product View #
Use the following implementation of customEvent when the user displays a product in the mobile app.
String pageName = 'Product View';
Map<String, String> parameters = {
'OM.pv' : productCode,
'OM.pn' : productName,
'OM.ppr' : productPrice,
'OM.pv.1' : productBrand,
'OM.inv': inventory // Number of items in stock
};
relatedDigitalPlugin.customEvent(pageName, parameters);
Add to Cart #
Use the following implementation of customEvent when the user adds items to the cart or removes.
String pageName = 'Cart';
Map<String, String> parameters = {
'OM.pbid' : basketID,
'OM.pb' : 'Product1 Code;Product2 Code',
'OM.pu' : 'Product1 Quantity;Product2 Quantity',
'OM.ppr' : 'Product1 Price*Product1 Quantity;Product2 Price*Product2 Quantity'
};
relatedDigitalPlugin.customEvent(pageName, parameters);
Product Purchase #
Use the following implementation of customEvent when the user buys one or more items.
String pageName = 'Purchase';
Map<String, String> parameters = {
'OM.tid' : transactionID,
'OM.pp' : 'Product1 Code;Product2 Code',
'OM.pu' : 'Product1 Quantity;Product2 Quantity',
'OM.ppr' : 'Product1 Price*Product1 Quantity;Product2 Price*Product2 Quantity',
'OM.exVisitorID' : userId
};
relatedDigitalPlugin.customEvent(pageName, parameters);
Product Category Page View #
When the user views a category list page, use the following implementation of customEvent.
String pageName = 'Category View';
Map<String, String> parameters = {
'OM.clist': '12345',
};
relatedDigitalPlugin.customEvent(pageName, parameters);
In App Search #
If the mobile app has a search functionality available, use the following implementation of customEvent.
String pageName = 'In App Search';
Map<String, String> parameters = {
'OM.OSS': searchKeyword,
'OM.OSSR': searchResult.length,
};
relatedDigitalPlugin.customEvent(pageName, parameters);
Banner Click #
You can monitor banner click data using the following implementation of customEvent.
String pageName = 'Banner Click';
Map<String, String> parameters = {
'OM.OSB': 'Banner Name/Banner Code',
};
relatedDigitalPlugin.customEvent(pageName, parameters);;
Add To Favorites #
When the user adds a product to their favorites, use the following implementation of customEvent.
String pageName = 'Add To Favorites';
Map<String, String> parameters = {
'OM.pf' : productCode,
'OM.pfu' : '1',
};
relatedDigitalPlugin.customEvent(pageName, parameters);
Remove from Favorites #
When the user removes a product from their favorites, use the following implementation of customEvent.
String pageName = 'Add To Favorites';
Map<String, String> parameters = {
'OM.pf' : productCode,
'OM.pfu' : '-1',
};
relatedDigitalPlugin.customEvent(pageName, parameters);
Logout #
To remove all the user related data from local storage, use the method below.
await relatedDigitalPlugin.logout();
Targeting Actions #
In-App Messaging #
In-app messages are notifications to your users when they are directly active in your mobile app. To enable In-App Messaging feature you need to set the value of inAppNotificationsEnabled parameter to true when calling init to initialize the SDK.
The existence of a relevant in-app message for an event controlled by after each customEvent call. You can create and customize your in-app messages on https://intelligence.relateddigital.com/#Target/TargetingAction/TAList page of RMC administration panel.
There are 9 types of in-app messages:
| Pop-up - Image, Header, Text & Button | Mini-icon&text | Full Screen-image |
|---|---|---|
![]() |
![]() |
![]() |
| Full Screen-image&button | Pop-up - Image, Header, Text & Button | Pop-up-Survey |
![]() |
![]() |
![]() |
| Pop-up - NPS with Text & Button | Native Alert & Actionsheet | NPS with numbers |
![]() |
![]() |
![]() |
Story #
Follow the step below to add a countdown to your stories.
iOS
Add below lines to your project target's Build Phases->Copy Bundle Resources section. Select Create folder references when prompted.
Pods/VisilabsIOS/Sources/TargetingAction/Story/Views/timerView/timerView.xib
Android
No special installation required
Usage
To add story view to your app, import RDStoryView and use as below:
import 'package:relateddigital_flutter/rd_story_view.dart';
...
...
RDStoryView(
actionId: '975', // Story actionid created from panel
relatedDigitalPlugin: widget.relatedDigitalPlugin,
onItemClick: (Map<String, String> result) {
print(result);
},
onRequestResult: (Map<String, String> result) {
// isAvailable, height, width — fired after the campaign response
print(result);
}
)
App-Banner #
iOS Android
No special installation required
Usage
To add app banner view to your app, import RDBannerView and use as below:
import 'package:relateddigital_flutter/rd_banner_view.dart';
...
...
RDBannerView(
key: null,
relatedDigitalPlugin: widget.relatedDigitalPlugin,
properties: {'key1': 'value1', 'key2': 'value2'},
onItemClick: (Map<String, String> result) {
print("onItemClick: $result");
},
onRequestResult: (Map<String, String> result) {
print("onRequestResult: $result");
},
)
Geofencing #
IOS
- Add the following keys to your Info.plist file:
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need access to your location for better user experience.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need access to your location for better user experience.</string>
- Add
locationto UIBackgroundModes in your Info.plist file (also enable Background fetch and Location updates in Xcode's Signing & Capabilities tab):
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>location</string>
<string>remote-notification</string>
</array>
- When initializing the plugin, set geofenceEnabled to true. Also provide a number for maxGeofenceCount parameter (max. 20 supported):
var initRequest = RDInitRequestModel(
// ...
geofenceEnabled: true,
maxGeofenceCount: 20,
);
- After initialization, request location permission from the user. See Location Permission section below.
Android
- Add below permissions in your AndroidManifest.xml
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
- Add below service and receivers in your AndroidManifest.xml, within the
<application></application>tags:
<service
android:name="com.visilabs.gps.geofence.GeofenceTransitionsIntentService"
android:enabled="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver android:name="com.visilabs.gps.geofence.VisilabsAlarm" android:exported="false"/>
<receiver
android:name="com.visilabs.gps.geofence.GeofenceBroadcastReceiver"
android:enabled="true"
android:exported="false"/>
- After initialization, request location permission from the user. See Location Permission section below.
Location Permission #
Location permission must be requested after the SDK is initialized (i.e. after relatedDigitalPlugin.init(...) has completed) and geofencing is enabled. Calling it before initialization has no effect.
Use requestLocationPermission to show the system location permission dialog to the user. The SDK handles the permission flow automatically:
iOS behavior:
- If the user has not yet made a choice (
notDetermined) → shows the When In Use permission dialog. - If the user has already granted When In Use (
authorizedWhenInUse) → shows the Always Allow upgrade dialog. - If permission is already at Always or was permanently denied → no dialog is shown.
Android behavior:
- If
ACCESS_FINE_LOCATIONorACCESS_COARSE_LOCATIONis not granted → requests both permissions. - If both are granted and the device runs Android 10 (Q) or higher → requests
ACCESS_BACKGROUND_LOCATION(required for geofence triggers while the app is in the background). - If all permissions are already granted → no dialog is shown.
After calling requestLocationPermission, the current permission status is automatically sent to Related Digital servers via sendLocationPermission.
// Call this after init() completes, for example in a button handler
// or after a brief delay to avoid showing the dialog immediately on app launch.
await relatedDigitalPlugin.requestLocationPermission();
Note: On Android 10+, the system requires the user to grant foreground location permission before the background location dialog is shown. The SDK handles this in two separate steps automatically.
If you only want to report the current permission status to Related Digital servers without showing any dialog (e.g. on each app launch to keep the server state in sync), use sendLocationPermission instead:
await relatedDigitalPlugin.sendLocationPermission();
Recommendation #
Use getRecommendations method as below to retrieve product recommendations. This method takes mandatory zoneId and productCode parameters with optional filters parameter.
import 'package:relateddigital_flutter/recommendation_filter.dart';
Future<void> getRecommendations() async {
String zoneId = '6';
String productCode = '';
// optional
Map<String, Object> filter = {
RDRecommendationFilter.attribute: RDRecommendationFilterAttribute.PRODUCTNAME,
RDRecommendationFilter.filterType: RDRecommendationFilterType.like,
RDRecommendationFilter.value: null
};
List filters = [
filter
];
// optional
Map<String, String> properties = {
'OM.cat':'84'
};
List result = await widget.relatedDigitalPlugin.getRecommendations(zoneId, productCode);
// List result = await relatedDigitalPlugin.getRecommendations(zoneId, productCode, properties:properties, filters: filters);
print(result.toString());
}
Recommendation Response
{
"recommendations": [
{
"attr1": "420494",
"attr10": "",
"attr2": "",
"attr3": "",
"attr4": "",
"attr5": "",
"attr6": "",
"attr7": "",
"attr8": "",
"attr9": "",
"brand": "Related Digital",
"code": "1159092",
"comment": 0,
"cur": "TRY",
"dcur": "TRY",
"dest_url": "https://relateddigital.com/example-product?OM.zn=You%20Viewed-w60&OM.zpc=1159092",
"discount": 0,
"dprice": 5.25,
"freeshipping": false,
"img": "https://cdn.relateddigital.com/example.png",
"price": 5.25,
"qs": "OM.zn=You Viewed-w60&OM.zpc=1159092",
"rating": 0,
"samedayshipping": false,
"title": "Titiz TP-115 Yeşil Ayakkabı",
"variants2": [
{
"color": "Gümüş Rengi",
"colors": [
{
"size": "STANDART",
"product_id": 12345,
"cart_id": "56789",
"stock": 123
}
]
}
]
}
],
"title": "Display You Viewed"
}
After clicking on each product, run the following function with the qs parameter. This parameter is included in the response returned from the widget.relatedDigitalPlugin.getRecommendations function.
widget.relatedDigitalPlugin.trackRecommendationClick(qs);
App Tracking #
(Android Only) Use sendTheListOfAppsInstalled method to track installed apps on android.
await widget.relatedDigitalPlugin.sendTheListOfAppsInstalled();
Add one of the sections below to your AndroidManifest.xml in order to use this feature.
Option 1
<manifest package="com.example.myApp">
<queries>
<package android:name="com.example.app1" />
<package android:name="com.example.app2" />
</queries>
</manifest>
Option 2
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />








