Skip to content
Open
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ android/.idea/
local.properties
mparticle_flutter_sdk.iml

doc/
doc/

.build/
.swiftpm/
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Make `android-rokt-kit` a `compileOnly` plugin dependency and guard Android Rokt registration behind a runtime classpath check, matching the optional Rokt kit pattern used in react-native-mparticle.

## [2.2.0] - 2026-07-22

### Added
Comment on lines +10 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changed section is above the version header


- Add Swift Package Manager support for the iOS plugin (`ios/mparticle_flutter_sdk/Package.swift`).

### Changed

- Bump iOS CocoaPods dependency from `mParticle-Apple-SDK ~> 9.2` to `~> 9.3`.
- Bump Android Gradle dependencies to `android-core:6.0.0` and `android-rokt-kit:6.0.0`.
- **BREAKING**: Android `minSdkVersion` raised from `16` to `23` (required by Rokt SDK 6.x transitive dependencies).
- Migrate Android Rokt bridge code to Android SDK 6.0 APIs (`com.mparticle.kits`, `com.rokt.roktsdk`, `MParticle.rokt`).
- Replace deprecated `UserAttributeListener` with `TypedUserAttributeListener` on Android.

### Removed

- Android Rokt layout padding callbacks (`onMarginChanged`) — removed upstream in Android SDK 6.0.

## [2.1.0] - 2026-05-19

### Added
Expand Down
31 changes: 31 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,34 @@ await MparticleFlutterSdk.getInstance().then((mp) => mp?.rokt.selectShoppableAds
Rokt event delivery now uses explicit subscription by identifier through `Rokt.events(...)`. Call `events(identifier, ...)` before `selectPlacements(...)` or `selectShoppableAds(...)` for that identifier.

The Rokt payment extension (for example `RoktPaymentExtension`) is **not** proxied through Dart. Integrators must add the pod and register it directly from native Swift/Objective-C in the host app (for example `ios/Runner/AppDelegate.swift`), after `MParticle.sharedInstance().start(with:)`.

## Migrating Android apps to mParticle Android SDK 6.0.0

The Dart API is unchanged. Existing calls to `Rokt.selectPlacements`, `Rokt.events`, and `Rokt.purchaseFinalized` continue to use the same Flutter signatures and event channel names.

### Android dependencies

Android apps must include matching 6.x mParticle artifacts:

```gradle
implementation "com.mparticle:android-core:6.0.0"
implementation "com.mparticle:android-rokt-kit:6.0.0"
```

`RoktEmbeddedView` and other Rokt bridge types moved out of `android-core` in SDK 6.0. They now live in `android-rokt-kit` under `com.mparticle.kits`. Apps that only add `android-core` will compile for non-Rokt usage, but Rokt method channel calls and `RoktLayout` platform views require `android-rokt-kit` on the app classpath.

The Flutter plugin uses `compileOnly` for `android-rokt-kit` and registers Rokt platform views only when `com.mparticle.kits.RoktEmbeddedView` is present at runtime.

Apps that include `android-rokt-kit` `6.0.0` should build with `compileSdk` 35+ and Android Gradle Plugin 8.6+.

### Android Rokt API changes handled by the plugin

| SDK 5.x | SDK 6.0 |
| --- | --- |
| `com.mparticle.rokt.RoktEmbeddedView` | `com.mparticle.kits.RoktEmbeddedView` |
| `MParticle.getInstance()?.Rokt()` | `MParticle.getInstance()?.rokt` |
| `com.mparticle.rokt.RoktConfig` | `com.rokt.roktsdk.RoktConfig` |
| `com.mparticle.RoktEvent` | `com.rokt.roktsdk.RoktEvent` |
| `UserAttributeListener` | `TypedUserAttributeListener` |

`selectShoppableAds` remains a no-op on Android in this release.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ To install mParticle on an Android platform:

```groovy
dependencies {
implementation 'com.mparticle:android-core:5.79.0'
// Required only if you use Rokt APIs from Flutter
implementation 'com.mparticle:android-rokt-kit:5.79.0'
implementation 'com.mparticle:android-core:6.0.0'

// Required if you use Rokt APIs or RoktLayout from Flutter.
// RoktEmbeddedView and related types live in android-rokt-kit, not android-core.
implementation 'com.mparticle:android-rokt-kit:6.0.0'

// Required for gathering Android Advertising ID (see below)
implementation 'com.google.android.gms:play-services-ads-identifier:16.0.0'
Expand All @@ -56,6 +58,8 @@ dependencies {
}
```

> **Note:** The Flutter plugin compiles against `android-rokt-kit` but does not force it as a transitive dependency. Apps that use Rokt must add `android-rokt-kit` explicitly, matching the version of `android-core`. Without it, non-Rokt mParticle APIs continue to work, but Rokt method channel calls return an error.

2. Grab your mParticle key and secret from [your workspace's dashboard](https://app.mparticle.com/setup/inputs/apps) and construct an `MParticleOptions` object.

3. Call `start` from the `onCreate` method of your app's `Application` class. It's crucial that the SDK be started here for proper session management. If you don't already have an `Application` class, create it and then specify its fully-qualified name in the `<application>` tag of your app's `AndroidManifest.xml`.
Expand Down Expand Up @@ -140,7 +144,7 @@ To install mParticle on an iOS platform:
2. Install the SDK using CocoaPods:

```bash
$ # Update your Podfile to depend on 'mParticle-Apple-SDK' version 9.2.0 or later
$ # Update your Podfile to depend on 'mParticle-Apple-SDK' version 9.3.1 or later
$ pod install
```

Expand Down
7 changes: 4 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ group 'com.mparticle.mparticle_flutter_sdk'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.9.22'
ext.kotlin_version = '2.1.20'
repositories {
google()
mavenCentral()
Expand Down Expand Up @@ -41,14 +41,15 @@ android {
}

defaultConfig {
minSdkVersion 16
minSdkVersion 23
compileSdkVersion 34
}
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'com.mparticle:android-core:5.79.0'
implementation 'com.mparticle:android-core:6.0.0'
compileOnly 'com.mparticle:android-rokt-kit:6.0.0'

// Required for Rokt event subscription
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ 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 @@ -20,22 +18,17 @@ import com.mparticle.identity.IdentityHttpResponse
import com.mparticle.identity.MParticleUser
import com.mparticle.MParticle
import com.mparticle.MPEvent
import com.mparticle.UserAttributeListener
import com.mparticle.TypedUserAttributeListener
import com.mparticle.WrapperSdk
import com.mparticle.commerce.*
import com.mparticle.consent.CCPAConsent
import com.mparticle.consent.ConsentState
import com.mparticle.consent.GDPRConsent
import com.mparticle.internal.Logger
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 */
Expand All @@ -46,25 +39,18 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
/// when the Flutter Engine is detached from the Activity
private lateinit var channel: MethodChannel
private val TAG = "MparticleFlutterSdkPlugin"
private lateinit var layoutFactory: RoktLayoutFactory
private var roktDelegate: RoktPluginDelegate? = null
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")
channel.setMethodCallHandler(this)
layoutFactory = RoktLayoutFactory(flutterPluginBinding.binaryMessenger)
flutterAssets = flutterPluginBinding.flutterAssets
applicationContext = flutterPluginBinding.applicationContext
flutterPluginBinding.platformViewRegistry.registerViewFactory(
VIEW_TYPE,
layoutFactory,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
roktEventHandler = RoktEventHandler(flutterPluginBinding.binaryMessenger)
}
roktDelegate = RoktKitAvailability.createDelegate(flutterPluginBinding.binaryMessenger)
roktDelegate?.registerPlatformView(flutterPluginBinding.platformViewRegistry)
}

override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
Expand Down Expand Up @@ -111,12 +97,13 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
} ?: result.error(TAG, "Missing attributeKey", null)
}
"getUserAttributes" -> this.getUser(call, result)?.let {
it.getUserAttributes(object : UserAttributeListener {
it.getUserAttributes(object : TypedUserAttributeListener {
override fun onUserAttributesReceived(
userAttributes: Map<String, String>?,
userAttributeLists: Map<String, List<String>>?, mpid: Long?
userAttributes: Map<String, Any?>,
userAttributeLists: Map<String, List<String?>?>,
mpid: Long,
) {
result.success(sanitizeMapToString(userAttributes))
result.success(sanitizeMapToString(userAttributes.mapValues { (_, value) -> value?.toString() }))
}
})
Unit
Expand Down Expand Up @@ -235,10 +222,18 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
setSdkVersion()
result.success(true)
}
"roktSubscribeToEvents" -> this.roktSubscribeToEvents(call, result)
"roktSelectPlacements" -> this.roktSelectPlacements(call, result)
"roktSelectShoppableAds" -> this.roktSelectShoppableAds(call, result)
"roktPurchaseFinalized" -> this.roktPurchaseFinalized(call, result)
"roktSubscribeToEvents" -> this.handleRoktCall(call, result) { delegate ->
delegate.subscribeToEvents(call, result, activity)
}
"roktSelectPlacements" -> this.handleRoktCall(call, result) { delegate ->
delegate.selectPlacements(call, result, applicationContext, flutterAssets)
}
"roktSelectShoppableAds" -> this.handleRoktCall(call, result) { delegate ->
delegate.selectShoppableAds(call, result)
}
"roktPurchaseFinalized" -> this.handleRoktCall(call, result) { delegate ->
delegate.purchaseFinalized(call, result)
}
else -> {
result.notImplemented()
}
Expand Down Expand Up @@ -720,119 +715,19 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
} ?: result.error(TAG, "No mParticle instance exists", null)
}

private fun roktSelectPlacements(call: MethodCall, result: Result) {
try {
val placementId: String? = call.argument("placementId")
val attributes: Map<String, Any?>? = call.argument("attributes")
val placeHolders: MutableMap<String, WeakReference<RoktEmbeddedView>> = mutableMapOf()
val configMap = call.argument<HashMap<String, Any>>("config")
val config = configMap?.let { buildRoktConfig(it) }
val customFonts = call.argument<HashMap<String, String>>("fontFilePathMap")
.orEmpty()
.mapNotNull { (key, fontPath) ->
applicationContext?.assets?.let { assets ->
flutterAssets?.getAssetFilePathByName(fontPath)?.let { assetPath ->
runCatching {
key to WeakReference(Typeface.createFromAsset(assets, assetPath))
}.getOrNull()
}
}
}
.toMap()

call.argument<HashMap<Int, String>>("placeholders")?.entries?.forEach { entry ->
layoutFactory.nativeViews[entry.key]?.let { view ->
placeHolders[entry.value] = WeakReference(view)
}
}

if (placementId == null) {
result.error(TAG, "Missing placementId", null)
return
}

val stringAttributes: MutableMap<String, String> = mutableMapOf()
attributes?.forEach { (key, value) ->
stringAttributes[key] = value?.toString() ?: ""
}

MParticle.getInstance()?.let { instance ->
instance.Rokt().selectPlacements(placementId, stringAttributes, null, placeHolders.takeIf { it.isNotEmpty() }, customFonts, config)
result.success(true)
} ?: result.error(TAG, "No mParticle instance exists", null)
} catch (e: Exception) {
result.error(TAG, e.localizedMessage, null)
}
}

private fun roktSubscribeToEvents(call: MethodCall, result: Result) {
val identifier = call.argument<String>("identifier")
if (identifier.isNullOrBlank()) {
result.error(TAG, "Missing identifier", null)
private fun handleRoktCall(
call: MethodCall,
result: Result,
handler: (RoktPluginDelegate) -> Unit,
) {
val delegate = roktDelegate
if (delegate == null) {
result.error(TAG, RoktKitAvailability.REQUIRED_MESSAGE, null)
return
}

MParticle.getInstance()?.let { instance ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity?.let { currentActivity ->
roktEventHandler?.subscribeToEvents(
events = instance.Rokt().events(identifier),
activity = currentActivity,
identifier = identifier,
)
}
}
result.success(true)
} ?: result.error(TAG, "No mParticle instance exists", null)
}

private fun buildRoktConfig(configMap: Map<String, Any>): RoktConfig {
val builder = RoktConfig.Builder()
(configMap["colorMode"] as? String)?.let {
builder.colorMode(it.toColorMode())
}
(configMap["cacheConfig"] as? Map<String, Any>)?.let { cacheConfig ->
val cacheDurationInSeconds = cacheConfig["cacheDurationInSeconds"] as? Int ?: 0
val cacheAttributes = cacheConfig["cacheAttributes"] as? Map<String, String> ?: null
builder.cacheConfig(CacheConfig(cacheDurationInSeconds.toLong(), cacheAttributes))
}

return builder.build()
}

private fun roktPurchaseFinalized(call: MethodCall, result: Result) {
val placementId = call.argument<String>("placementId")
val catalogItemId = call.argument<String>("catalogItemId")
val success = call.argument<Boolean>("success") ?: true
if (placementId != null && catalogItemId != null) {
MParticle.getInstance()?.Rokt()?.purchaseFinalized(
placementId = placementId,
catalogItemId = catalogItemId,
status = success,
)
result.success("Success")
} else {
result.error(
"INVALID_PARAMS",
"placementId and catalogItemId are required",
null,
)
}
handler(delegate)
}

private fun roktSelectShoppableAds(call: MethodCall, result: Result) {
// Parity with RN bridge: Android API is exposed but not implemented yet.
Logger.warning("selectShoppableAds is not yet supported on Android")
result.success(true)
}

private fun String.toColorMode(): RoktConfig.ColorMode =
when (this) {
"dark" -> RoktConfig.ColorMode.DARK
"light" -> RoktConfig.ColorMode.LIGHT
else -> RoktConfig.ColorMode.SYSTEM
}

private fun ConvertIdentityHttpResponseToString(response: IdentityHttpResponse?): String {
val map = mutableMapOf<String, Any?>()

Expand Down Expand Up @@ -1005,8 +900,4 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware
instance.setWrapperSdk(WrapperSdk.WrapperFlutter, "")
}
}

companion object {
private const val VIEW_TYPE = "rokt_sdk.rokt.com/rokt_layout"
}
}
Loading
Loading