Skip to content

Commit bd07a9c

Browse files
committed
feat: Add support for Rokt event channel subscription
- Add RoktEventHandler to forward native Rokt events to Flutter. - Use an EventChannel to stream events from the native Android and iOS side. - Add lifecycle-runtime-ktx dependency to safely collect events tied to the Activity lifecycle. - Make the plugin ActivityAware to get the required Activity context. - Add iOS events API and use the callback to subscribe to events and map them to the Flutter side. - Update the example app to demonstrate listening for Rokt events.
1 parent e8b4bbb commit bd07a9c

6 files changed

Lines changed: 273 additions & 2 deletions

File tree

android/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ dependencies {
5050
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
5151
implementation 'com.mparticle:android-core:5+'
5252

53+
// Required for Rokt event subscription
54+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
55+
5356
// Required for gathering Android Advertising ID (see below)
5457
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
5558

android/src/main/kotlin/com/mparticle/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.mparticle.mparticle_flutter_sdk
22

3+
import android.app.Activity
34
import android.content.Context
45
import android.graphics.Typeface
56
import androidx.annotation.NonNull
@@ -27,14 +28,16 @@ import com.mparticle.consent.GDPRConsent
2728
import com.mparticle.rokt.CacheConfig
2829
import com.mparticle.rokt.RoktConfig
2930
import com.mparticle.rokt.RoktEmbeddedView
31+
import io.flutter.embedding.engine.plugins.activity.ActivityAware
32+
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
3033

3134
import org.json.JSONObject
3235
import kotlin.IllegalArgumentException
3336
import java.lang.ref.WeakReference
3437

3538

3639
/** MparticleFlutterSdkPlugin */
37-
class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
40+
class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
3841
/// The MethodChannel that will the communication between Flutter and native Android
3942
///
4043
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
@@ -44,6 +47,8 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
4447
private lateinit var layoutFactory: RoktLayoutFactory
4548
private var flutterAssets: FlutterPlugin.FlutterAssets? = null
4649
private var applicationContext: Context? = null
50+
private var activity: Activity? = null
51+
private var roktEventHandler: RoktEventHandler? = null
4752

4853
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
4954
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "mparticle_flutter_sdk")
@@ -55,6 +60,7 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
5560
VIEW_TYPE,
5661
layoutFactory,
5762
)
63+
roktEventHandler = RoktEventHandler(flutterPluginBinding.binaryMessenger)
5864
}
5965

6066
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
@@ -232,6 +238,22 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
232238
}
233239
}
234240

241+
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
242+
activity = binding.activity
243+
}
244+
245+
override fun onDetachedFromActivityForConfigChanges() {
246+
activity = null
247+
}
248+
249+
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
250+
activity = binding.activity
251+
}
252+
253+
override fun onDetachedFromActivity() {
254+
activity = null
255+
}
256+
235257
private fun logEvent(call: MethodCall, result: Result) {
236258
try {
237259
val eventName: String? = call.argument("eventName")
@@ -728,6 +750,14 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
728750
}
729751

730752
MParticle.getInstance()?.let { instance ->
753+
activity?.let {
754+
roktEventHandler?.subscribeToEvents(
755+
events = instance.Rokt().events(placementId),
756+
activity = it,
757+
identifier = placementId,
758+
)
759+
}
760+
731761
instance.Rokt().selectPlacements(placementId, stringAttributes, null, placeHolders.takeIf { it.isNotEmpty() }, customFonts, config)
732762
result.success(true)
733763
} ?: result.error(TAG, "No mParticle instance exists", null)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.mparticle.mparticle_flutter_sdk
2+
3+
import android.app.Activity
4+
import androidx.lifecycle.Lifecycle
5+
import androidx.lifecycle.LifecycleOwner
6+
import androidx.lifecycle.lifecycleScope
7+
import androidx.lifecycle.repeatOnLifecycle
8+
import com.mparticle.RoktEvent
9+
import io.flutter.plugin.common.BinaryMessenger
10+
import io.flutter.plugin.common.EventChannel
11+
import kotlinx.coroutines.Job
12+
import kotlinx.coroutines.flow.Flow
13+
import kotlinx.coroutines.launch
14+
import java.util.ArrayDeque
15+
16+
class RoktEventHandler(private val messenger: BinaryMessenger) {
17+
18+
private val eventListeners = mutableMapOf<Any?, ArrayDeque<EventChannel.EventSink>>()
19+
private val eventSubscriptions = mutableMapOf<String, Job?>()
20+
21+
init {
22+
setupEventChannel()
23+
}
24+
25+
fun subscribeToEvents(events: Flow<RoktEvent>, identifier: String? = null, activity: Activity) {
26+
val activeJob = eventSubscriptions[identifier.orEmpty()]?.takeIf { it.isActive }
27+
if (activeJob != null) {
28+
return
29+
}
30+
val owner = activity as? LifecycleOwner ?: return
31+
32+
val job = owner.lifecycleScope.launch {
33+
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
34+
events.collect { event ->
35+
val params = mutableMapOf<String, String>()
36+
37+
params["event"] = event::class.simpleName ?: "RoktEvent"
38+
event.placementId?.let { params["placementId"] = it }
39+
40+
when (event) {
41+
is RoktEvent.InitComplete -> {
42+
params["status"] = event.success.toString()
43+
}
44+
is RoktEvent.OpenUrl -> {
45+
params["url"] = event.url
46+
}
47+
is RoktEvent.CartItemInstantPurchase -> {
48+
params["cartItemId"] = event.cartItemId
49+
params["catalogItemId"] = event.catalogItemId
50+
params["currency"] = event.currency
51+
params["description"] = event.description
52+
params["linkedProductId"] = event.linkedProductId
53+
params["totalPrice"] = event.totalPrice.toString()
54+
params["quantity"] = event.quantity.toString()
55+
params["unitPrice"] = event.unitPrice.toString()
56+
}
57+
else -> {
58+
// No custom parameters needed for other events
59+
}
60+
}
61+
62+
identifier?.let { params["identifier"] = it }
63+
eventListeners.values.flatten().forEach { listener -> listener.success(params) }
64+
}
65+
}
66+
}
67+
eventSubscriptions[identifier.orEmpty()] = job
68+
}
69+
70+
private fun setupEventChannel() {
71+
EventChannel(messenger, EVENT_CHANNEL_NAME).setStreamHandler(
72+
object : EventChannel.StreamHandler {
73+
override fun onListen(
74+
arguments: Any?,
75+
sink: EventChannel.EventSink?,
76+
) {
77+
sink?.let {
78+
val sinks = eventListeners.getOrPut(arguments) { ArrayDeque() }
79+
sinks.addLast(it)
80+
}
81+
}
82+
83+
override fun onCancel(arguments: Any?) {
84+
val sinks = eventListeners[arguments]
85+
if (sinks?.isNotEmpty() == true) {
86+
sinks.removeLast()
87+
}
88+
if (sinks?.isEmpty() == true) {
89+
eventListeners.remove(arguments)
90+
}
91+
}
92+
},
93+
)
94+
}
95+
96+
private val RoktEvent.placementId: String?
97+
get() = when (this) {
98+
is RoktEvent.FirstPositiveEngagement -> placementId
99+
is RoktEvent.OfferEngagement -> placementId
100+
is RoktEvent.PlacementClosed -> placementId
101+
is RoktEvent.PlacementCompleted -> placementId
102+
is RoktEvent.PlacementFailure -> placementId
103+
is RoktEvent.PlacementInteractive -> placementId
104+
is RoktEvent.PlacementReady -> placementId
105+
is RoktEvent.PositiveEngagement -> placementId
106+
is RoktEvent.OpenUrl -> placementId
107+
is RoktEvent.CartItemInstantPurchase -> placementId
108+
else -> null
109+
}
110+
111+
companion object {
112+
private const val EVENT_CHANNEL_NAME = "MPRoktEvents"
113+
}
114+
}

example/lib/rokt_layouts_screen.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter/foundation.dart' show kIsWeb;
3+
import 'package:flutter/services.dart';
34
import 'dart:io' show Platform;
45
import 'package:mparticle_flutter_sdk/mparticle_flutter_sdk.dart';
56
import 'package:mparticle_flutter_sdk/identity/identity_type.dart';
@@ -18,6 +19,7 @@ class RoktLayoutsScreen extends StatefulWidget {
1819
class _RoktLayoutsScreenState extends State<RoktLayoutsScreen> {
1920
final TextEditingController _placementIdController =
2021
TextEditingController(text: 'readmorelayout');
22+
final EventChannel roktEventChannel = EventChannel('MPRoktEvents');
2123

2224
Map<String, String> _getAttributesForPlatform() {
2325
if (kIsWeb) {
@@ -71,12 +73,24 @@ class _RoktLayoutsScreenState extends State<RoktLayoutsScreen> {
7173
return '$platform-test-user-${DateTime.now().millisecondsSinceEpoch}';
7274
}
7375

76+
@override
77+
void initState() {
78+
_receiveRoktEvent();
79+
super.initState();
80+
}
81+
7482
@override
7583
void dispose() {
7684
_placementIdController.dispose();
7785
super.dispose();
7886
}
7987

88+
void _receiveRoktEvent() {
89+
roktEventChannel.receiveBroadcastStream().listen((dynamic event) {
90+
debugPrint("rokt_event: _receiveRoktEvent $event ");
91+
});
92+
}
93+
8094
Widget buildButton(String text, VoidCallback onPressed) {
8195
return Padding(
8296
padding: const EdgeInsets.symmetric(vertical: 8.0),

ios/Classes/RoktEventHandler.swift

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//
2+
// RoktEventHandler.swift
3+
// rokt_sdk
4+
//
5+
// Copyright 2020 Rokt Pte Ltd
6+
//
7+
// Licensed under the Rokt Software Development Kit (SDK) Terms of Use
8+
// Version 2.0 (the "License");
9+
//
10+
// You may not use this file except in compliance with the License.
11+
//
12+
// You may obtain a copy of the License at https://rokt.com/sdk-license-2-0/
13+
14+
import Foundation
15+
import Flutter
16+
import mParticle_Apple_SDK
17+
18+
class RoktEventHandler: NSObject, FlutterStreamHandler {
19+
20+
private var eventListeners: [String: [FlutterEventSink]] = [:]
21+
private let EVENT_CHANNEL_NAME = "MPRoktEvents"
22+
23+
init(messenger: FlutterBinaryMessenger) {
24+
super.init()
25+
setupEventChannel(messenger: messenger)
26+
}
27+
28+
private func setupEventChannel(messenger: FlutterBinaryMessenger) {
29+
let eventChannel = FlutterEventChannel(name: EVENT_CHANNEL_NAME, binaryMessenger: messenger)
30+
eventChannel.setStreamHandler(self)
31+
}
32+
33+
func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
34+
let key = String(describing: arguments ?? "nil")
35+
var sinks = eventListeners[key] ?? []
36+
sinks.append(eventSink)
37+
eventListeners[key] = sinks
38+
return nil
39+
}
40+
41+
func onCancel(withArguments arguments: Any?) -> FlutterError? {
42+
let key = String(describing: arguments ?? "nil")
43+
if var sinks = eventListeners[key], !sinks.isEmpty {
44+
sinks.removeLast()
45+
if sinks.isEmpty {
46+
eventListeners.removeValue(forKey: key)
47+
} else {
48+
eventListeners[key] = sinks
49+
}
50+
}
51+
return nil
52+
}
53+
54+
55+
func subscribeToEvents(identifier: String) {
56+
MParticle.sharedInstance().rokt.events(identifier) { event in
57+
var params: [String: String] = [:]
58+
59+
params["event"] = String(describing: type(of: event)).replacingOccurrences(of: "MPRokt", with: "").replacingOccurrences(of: "Event", with: "")
60+
params["identifier"] = identifier
61+
62+
if let placementId = event.roktPlacementId {
63+
params["placementId"] = placementId
64+
}
65+
66+
switch event {
67+
case let initCompleteEvent as MPRoktInitCompleteEvent:
68+
params["status"] = initCompleteEvent.success ? "true" : "false"
69+
case let openUrlEvent as MPRoktOpenUrlEvent:
70+
params["url"] = openUrlEvent.url
71+
case let cartItemInstantPurchaseEvent as MPRoktCartItemInstantPurchaseEvent:
72+
params["cartItemId"] = cartItemInstantPurchaseEvent.cartItemId
73+
params["catalogItemId"] = cartItemInstantPurchaseEvent.catalogItemId
74+
params["currency"] = cartItemInstantPurchaseEvent.currency
75+
params["description"] = cartItemInstantPurchaseEvent.cartItemDescription
76+
params["linkedProductId"] = cartItemInstantPurchaseEvent.linkedProductId
77+
params["totalPrice"] = cartItemInstantPurchaseEvent.totalPrice.stringValue
78+
params["quantity"] = cartItemInstantPurchaseEvent.quantity.stringValue
79+
params["unitPrice"] = cartItemInstantPurchaseEvent.unitPrice.stringValue
80+
default:
81+
break
82+
}
83+
84+
eventListeners.values.joined().forEach { listener in
85+
listener(params)
86+
}
87+
}
88+
}
89+
}
90+
91+
private extension MPRoktEvent {
92+
var roktPlacementId: String? {
93+
switch self {
94+
case let event as MPRokt.FirstPositiveEngagement: return event.placementId
95+
case let event as MPRokt.OfferEngagement: return event.placementId
96+
case let event as MPRokt.PlacementClosed: return event.placementId
97+
case let event as MPRokt.PlacementCompleted: return event.placementId
98+
case let event as MPRokt.PlacementFailure: return event.placementId
99+
case let event as MPRokt.PlacementInteractive: return event.placementId
100+
case let event as MPRokt.PlacementReady: return event.placementId
101+
case let event as MPRokt.PositiveEngagement: return event.placementId
102+
case let event as MPRokt.OpenUrl: return event.placementId
103+
case let event as MPRokt.CartItemInstantPurchase: return event.placementId
104+
default: return nil
105+
}
106+
}
107+
}

ios/Classes/SwiftMparticleFlutterSdkPlugin.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin {
88
let roktLayoutFactory: RoktLayoutFactory
99
let channel: FlutterMethodChannel
1010
let registrar: FlutterPluginRegistrar
11+
private let roktEventHandler: RoktEventHandler
1112

1213
init(messenger: FlutterBinaryMessenger, registrar: FlutterPluginRegistrar) {
1314
self.roktLayoutFactory = RoktLayoutFactory(messenger: messenger)
1415
self.registrar = registrar
1516
self.channel = FlutterMethodChannel(name: "mparticle_flutter_sdk", binaryMessenger: messenger)
17+
self.roktEventHandler = RoktEventHandler(messenger: messenger)
1618
}
1719

1820
public static func register(with registrar: FlutterPluginRegistrar) {
@@ -537,7 +539,8 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin {
537539
registerPartnerFonts(typefaces)
538540
}
539541

540-
MParticle.sharedInstance().rokt.selectPlacements(placementId, attributes: attributes, placements: placeholders, config: roktConfig, callbacks: callback)
542+
roktEventHandler.subscribeToEvents(identifier: placementId)
543+
MParticle.sharedInstance().rokt.selectPlacements(placementId, attributes: attributes, embeddedViews: placeholders, config: roktConfig, callbacks: callback)
541544
result(true)
542545
} else {
543546
print("Incorrect argument for \(call.method) iOS method")

0 commit comments

Comments
 (0)