Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,13 @@ jobs:
build-ios:
needs: test
name: Build iOS flutter app
runs-on: macos-14
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- name: Set up Xcode 16
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 16.3.0
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
Expand Down
3 changes: 3 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.mparticle:android-core:5+'

// Required for Rokt event subscription
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"

// Required for gathering Android Advertising ID (see below)
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.mparticle.mparticle_flutter_sdk

import android.app.Activity
import android.content.Context
import android.graphics.Typeface
import android.os.Build
import androidx.annotation.NonNull

import io.flutter.embedding.engine.plugins.FlutterPlugin
Expand All @@ -27,14 +29,16 @@ import com.mparticle.consent.GDPRConsent
import com.mparticle.rokt.CacheConfig
import com.mparticle.rokt.RoktConfig
import com.mparticle.rokt.RoktEmbeddedView
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding

import org.json.JSONObject
import kotlin.IllegalArgumentException
import java.lang.ref.WeakReference


/** MparticleFlutterSdkPlugin */
class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware {
/// The MethodChannel that will the communication between Flutter and native Android
///
/// This local reference serves to register the plugin with the Flutter Engine and unregister it
Expand All @@ -44,6 +48,8 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
private lateinit var layoutFactory: RoktLayoutFactory
private var flutterAssets: FlutterPlugin.FlutterAssets? = null
private var applicationContext: Context? = null
private var activity: Activity? = null
private var roktEventHandler: RoktEventHandler? = null

override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "mparticle_flutter_sdk")
Expand All @@ -55,6 +61,9 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
VIEW_TYPE,
layoutFactory,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
roktEventHandler = RoktEventHandler(flutterPluginBinding.binaryMessenger)
}
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
Expand Down Expand Up @@ -232,6 +241,22 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
}
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}

override fun onDetachedFromActivityForConfigChanges() {
activity = null
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}

override fun onDetachedFromActivity() {
activity = null
}

private fun logEvent(call: MethodCall, result: Result) {
try {
val eventName: String? = call.argument("eventName")
Expand Down Expand Up @@ -728,6 +753,16 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler {
}

MParticle.getInstance()?.let { instance ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity?.let {
roktEventHandler?.subscribeToEvents(
events = instance.Rokt().events(placementId),
activity = it,
identifier = placementId,
)
}
}

instance.Rokt().selectPlacements(placementId, stringAttributes, null, placeHolders.takeIf { it.isNotEmpty() }, customFonts, config)
result.success(true)
} ?: result.error(TAG, "No mParticle instance exists", null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.mparticle.mparticle_flutter_sdk

import android.app.Activity
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.mparticle.RoktEvent
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque

@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class RoktEventHandler(private val messenger: BinaryMessenger) {

private val eventListeners = ConcurrentHashMap<Any?, ConcurrentLinkedDeque<EventChannel.EventSink>>()
private val eventSubscriptions = mutableMapOf<String, Job?>()

init {
setupEventChannel()
}

fun subscribeToEvents(events: Flow<RoktEvent>, identifier: String? = null, activity: Activity) {
val activeJob = eventSubscriptions[identifier.orEmpty()]?.takeIf { it.isActive }
if (activeJob != null) {
return
}
val owner = activity as? LifecycleOwner ?: return

val job = owner.lifecycleScope.launch {
owner.lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
events.collect { event ->
val params = mutableMapOf<String, String>()

params["event"] = event::class.simpleName ?: "RoktEvent"
event.placementId?.let { params["placementId"] = it }

when (event) {
is RoktEvent.InitComplete -> {
params["status"] = event.success.toString()
}
is RoktEvent.OpenUrl -> {
params["url"] = event.url
}
is RoktEvent.CartItemInstantPurchase -> {
params["cartItemId"] = event.cartItemId
params["catalogItemId"] = event.catalogItemId
params["currency"] = event.currency
params["description"] = event.description
params["linkedProductId"] = event.linkedProductId
params["totalPrice"] = event.totalPrice.toString()
params["quantity"] = event.quantity.toString()
params["unitPrice"] = event.unitPrice.toString()
}
else -> {
// No custom parameters needed for other events
}
}

identifier?.let { params["identifier"] = it }
eventListeners.values.flatten().forEach { listener -> listener.success(params) }
}
}
}
eventSubscriptions[identifier.orEmpty()] = job
}

private fun setupEventChannel() {
EventChannel(messenger, EVENT_CHANNEL_NAME).setStreamHandler(
object : EventChannel.StreamHandler {
override fun onListen(
arguments: Any?,
sink: EventChannel.EventSink?,
) {
sink?.let {
val sinks = eventListeners.getOrPut(arguments ?: "") { ConcurrentLinkedDeque() }
sinks.addLast(it)
}
}

override fun onCancel(arguments: Any?) {
val sinks = eventListeners[arguments ?: ""]
if (sinks?.isNotEmpty() == true) {
sinks.removeLast()
}
if (sinks?.isEmpty() == true) {
eventListeners.remove(arguments ?: "")
}
}
},
)
}

private val RoktEvent.placementId: String?
get() = when (this) {
is RoktEvent.FirstPositiveEngagement -> placementId
is RoktEvent.OfferEngagement -> placementId
is RoktEvent.PlacementClosed -> placementId
is RoktEvent.PlacementCompleted -> placementId
is RoktEvent.PlacementFailure -> placementId
is RoktEvent.PlacementInteractive -> placementId
is RoktEvent.PlacementReady -> placementId
is RoktEvent.PositiveEngagement -> placementId
is RoktEvent.OpenUrl -> placementId
is RoktEvent.CartItemInstantPurchase -> placementId
else -> null
}

companion object {
private const val EVENT_CHANNEL_NAME = "MPRoktEvents"
}
}
14 changes: 14 additions & 0 deletions example/lib/rokt_layouts_screen.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart';
import 'dart:io' show Platform;
import 'package:mparticle_flutter_sdk/mparticle_flutter_sdk.dart';
import 'package:mparticle_flutter_sdk/identity/identity_type.dart';
Expand All @@ -18,6 +19,7 @@ class RoktLayoutsScreen extends StatefulWidget {
class _RoktLayoutsScreenState extends State<RoktLayoutsScreen> {
final TextEditingController _placementIdController =
TextEditingController(text: 'readmorelayout');
final EventChannel roktEventChannel = EventChannel('MPRoktEvents');

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

@override
void initState() {
_receiveRoktEvent();
super.initState();
}

@override
void dispose() {
_placementIdController.dispose();
super.dispose();
}

void _receiveRoktEvent() {
roktEventChannel.receiveBroadcastStream().listen((dynamic event) {
debugPrint("rokt_event: _receiveRoktEvent $event ");
});
}

Widget buildButton(String text, VoidCallback onPressed) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
Expand Down
118 changes: 118 additions & 0 deletions ios/Classes/RoktEventHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// RoktEventHandler.swift
// rokt_sdk
//
// Copyright 2020 Rokt Pte Ltd
//
// Licensed under the Rokt Software Development Kit (SDK) Terms of Use
// Version 2.0 (the "License");
//
// You may not use this file except in compliance with the License.
//
// You may obtain a copy of the License at https://rokt.com/sdk-license-2-0/

import Foundation
import Flutter
import mParticle_Apple_SDK

class RoktEventHandler: NSObject, FlutterStreamHandler {

private var eventListeners: [String: [FlutterEventSink]] = [:]
private let eventQueue = DispatchQueue(label: "com.mparticle.rokt.event.queue")
private let EVENT_CHANNEL_NAME = "MPRoktEvents"

init(messenger: FlutterBinaryMessenger) {
super.init()
setupEventChannel(messenger: messenger)
}

private func setupEventChannel(messenger: FlutterBinaryMessenger) {
let eventChannel = FlutterEventChannel(name: EVENT_CHANNEL_NAME, binaryMessenger: messenger)
eventChannel.setStreamHandler(self)
}

func onListen(withArguments arguments: Any?, eventSink: @escaping FlutterEventSink) -> FlutterError? {
eventQueue.sync {
let key = String(describing: arguments ?? "nil")
var sinks = eventListeners[key] ?? []
sinks.append(eventSink)
eventListeners[key] = sinks
}
return nil
}

func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventQueue.sync {
let key = String(describing: arguments ?? "nil")
if var sinks = eventListeners[key], !sinks.isEmpty {
sinks.removeLast()
if sinks.isEmpty {
eventListeners.removeValue(forKey: key)
} else {
eventListeners[key] = sinks
}
}
}
return nil
}


func subscribeToEvents(identifier: String) {
MParticle.sharedInstance().rokt.events(identifier) { event in
var params: [String: String] = [:]

params["event"] = String(describing: type(of: event)).replacingOccurrences(of: "MPRokt", with: "").replacingOccurrences(of: "Event", with: "")
params["identifier"] = identifier

if let placementId = event.roktPlacementId {
params["placementId"] = placementId
}

switch event {
case let initCompleteEvent as MPRoktEvent.MPRoktInitComplete:
params["status"] = initCompleteEvent.success ? "true" : "false"
case let openUrlEvent as MPRoktEvent.MPRoktOpenUrl:
params["url"] = openUrlEvent.url
case let cartItemInstantPurchaseEvent as MPRoktEvent.MPRoktCartItemInstantPurchase:
params["cartItemId"] = cartItemInstantPurchaseEvent.cartItemId
params["catalogItemId"] = cartItemInstantPurchaseEvent.catalogItemId
params["currency"] = cartItemInstantPurchaseEvent.currency
params["description"] = cartItemInstantPurchaseEvent.description
params["linkedProductId"] = cartItemInstantPurchaseEvent.linkedProductId
params["totalPrice"] = cartItemInstantPurchaseEvent.totalPrice?.stringValue
params["quantity"] = cartItemInstantPurchaseEvent.quantity?.stringValue
params["unitPrice"] = cartItemInstantPurchaseEvent.unitPrice?.stringValue
default:
break
}

let allSinks = self.eventQueue.sync {
return Array(self.eventListeners.values.joined())
}

allSinks.forEach { listener in
DispatchQueue.main.async {
listener(params)
}
}
}
}
}

private extension MPRoktEvent {
var roktPlacementId: String? {
switch self {
case let event as MPRoktEvent.MPRoktFirstPositiveEngagement: return event.placementId
case let event as MPRoktEvent.MPRoktOfferEngagement: return event.placementId
case let event as MPRoktEvent.MPRoktPlacementClosed: return event.placementId
case let event as MPRoktEvent.MPRoktPlacementCompleted: return event.placementId
case let event as MPRoktEvent.MPRoktPlacementFailure: return event.placementId
case let event as MPRoktEvent.MPRoktPlacementInteractive: return event.placementId
case let event as MPRoktEvent.MPRoktPlacementReady: return event.placementId
case let event as MPRoktEvent.MPRoktPositiveEngagement: return event.placementId
case let event as MPRoktEvent.MPRoktOpenUrl: return event.placementId
case let event as MPRoktEvent.MPRoktCartItemInstantPurchase: return event.placementId
default: return nil
}
}
}
Loading