diff --git a/android/src/main/kotlin/com/mparticle/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.kt b/android/src/main/kotlin/com/mparticle/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.kt index 9f4b23f..55e9724 100644 --- a/android/src/main/kotlin/com/mparticle/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.kt +++ b/android/src/main/kotlin/com/mparticle/mparticle_flutter_sdk/MparticleFlutterSdkPlugin.kt @@ -235,6 +235,7 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware result.success(true) } "roktSelectPlacements" -> this.roktSelectPlacements(call, result) + "roktPurchaseFinalized" -> this.roktPurchaseFinalized(call, result) else -> { result.notImplemented() } @@ -785,6 +786,26 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler, ActivityAware return builder.build() } + private fun roktPurchaseFinalized(call: MethodCall, result: Result) { + val placementId = call.argument("placementId") + val catalogItemId = call.argument("catalogItemId") + val success = call.argument("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, + ) + } + } + private fun String.toColorMode(): RoktConfig.ColorMode = when (this) { "dark" -> RoktConfig.ColorMode.DARK diff --git a/ios/Classes/SwiftMparticleFlutterSdkPlugin.swift b/ios/Classes/SwiftMparticleFlutterSdkPlugin.swift index a2e211e..e91deca 100644 --- a/ios/Classes/SwiftMparticleFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftMparticleFlutterSdkPlugin.swift @@ -529,7 +529,7 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin { } } } - + var roktConfig: MPRoktConfig? if let configMap = callArguments["config"] as? [String: Any] { roktConfig = buildRoktConfig(configMap: configMap) @@ -540,12 +540,24 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin { } roktEventHandler.subscribeToEvents(identifier: placementId) + MParticle.sharedInstance().rokt.selectPlacements(placementId, attributes: attributes, embeddedViews: placeholders, config: roktConfig, callbacks: callback) result(true) } else { print("Incorrect argument for \(call.method) iOS method") result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing placementId", details: nil)) } + case "roktPurchaseFinalized": + if let callArguments = call.arguments as? [String: Any], + let placementId = callArguments["placementId"] as? String, + let catalogItemId = callArguments["catalogItemId"] as? String, + let success = callArguments["success"] as? Bool { + MParticle.sharedInstance().rokt.purchaseFinalized(placementId, catalogItemId: catalogItemId, success: success) + result(true) + } else { + print("Incorrect argument for \(call.method) iOS method") + result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing placementId or catalogItemId or success", details: nil)) + } default: print("mParticle flutter SDK for iOS does not support \(call.method)") } @@ -569,7 +581,7 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin { private func buildRoktConfig(configMap: [String: Any]) -> MPRoktConfig? { let config = MPRoktConfig() var isConfigEmpty = true - + if let colorModeString = configMap["colorMode"] as? String { if #available(iOS 12.0, *) { isConfigEmpty = false diff --git a/lib/mparticle_flutter_sdk.dart b/lib/mparticle_flutter_sdk.dart index 895e1c9..ec79318 100644 --- a/lib/mparticle_flutter_sdk.dart +++ b/lib/mparticle_flutter_sdk.dart @@ -89,6 +89,12 @@ class MparticleFlutterSdk { _placeholders[id] = name; } + /// Clears all placeholders. Used for testing. + @visibleForTesting + void clearPlaceholders() { + _placeholders.clear(); + } + /// Logs a product commerce event with an [productActionType], a promotion commerce event with a [eventType], and an impression commerce event if neither of the prior are implemented. Future logCommerceEvent(CommerceEvent commerceEvent) async { var commerceEventMessage = { @@ -325,6 +331,27 @@ class Rokt { return await _channel.invokeMethod('roktSelectPlacements', params); } + /// Notifies Rokt that a purchase has been finalized + /// + /// Use this method to inform Rokt that a purchase has been completed or failed + /// - Parameters: + /// - placementId: The placement ID associated with the purchase + /// - catalogItemId: The catalog item ID that was purchased + /// - success: Whether the purchase was successful + /// + /// Note: This method requires iOS 15+. + Future purchaseFinalized({ + required String placementId, + required String catalogItemId, + required bool success, + }) async { + return await _channel.invokeMethod('roktPurchaseFinalized', { + 'placementId': placementId, + 'catalogItemId': catalogItemId, + 'success': success, + }); + } + Map? _roktConfigToMap({required RoktConfig? config}) { if (config == null) { return null; diff --git a/test/mparticle_flutter_sdk_test.dart b/test/mparticle_flutter_sdk_test.dart index 7f2348d..01f2e7d 100644 --- a/test/mparticle_flutter_sdk_test.dart +++ b/test/mparticle_flutter_sdk_test.dart @@ -36,6 +36,7 @@ void main() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, null); methodCall = null; + mp.clearPlaceholders(); }); group('mParticle Dart API Layer', () { @@ -295,4 +296,64 @@ void main() { ); }); }); + + group('Rokt API', () { + test('rokt select placements', () async { + final roktConfig = RoktConfig( + colorMode: ColorMode.dark, + cacheConfig: CacheConfig( + cacheDurationInSeconds: 100, + cacheAttributes: {'key1': 'value1'})); + await mp.rokt.selectPlacements( + placementId: 'placement1', + attributes: {'attr1': 'val1'}, + roktConfig: roktConfig, + fontFilePathMap: {'font1': 'path1'}); + + expect( + methodCall, + isMethodCall('roktSelectPlacements', arguments: { + 'placementId': 'placement1', + 'attributes': {'attr1': 'val1'}, + 'config': { + 'colorMode': 'dark', + 'cacheConfig': { + 'cacheDurationInSeconds': 100, + 'cacheAttributes': {'key1': 'value1'} + } + }, + 'fontFilePathMap': {'font1': 'path1'}, + })); + }); + + test('rokt select placements with placeholders', () async { + mp.attachPlaceholder(id: 1, name: "placeholder1"); + await mp.rokt.selectPlacements( + placementId: 'placement1', + ); + + expect( + methodCall, + isMethodCall('roktSelectPlacements', arguments: { + 'placementId': 'placement1', + 'attributes': null, + 'config': null, + 'fontFilePathMap': null, + 'placeholders': {1: 'placeholder1'}, + })); + }); + + test('rokt purchase finalized', () async { + await mp.rokt.purchaseFinalized( + placementId: 'placement1', catalogItemId: 'catalog1', success: true); + + expect( + methodCall, + isMethodCall('roktPurchaseFinalized', arguments: { + 'placementId': 'placement1', + 'catalogItemId': 'catalog1', + 'success': true, + })); + }); + }); }