From 34d9c693cd982df16bce6a9be5d978342c70cb78 Mon Sep 17 00:00:00 2001 From: Thomson Thomas Date: Mon, 23 Jun 2025 23:52:25 +1000 Subject: [PATCH] feat: Add Rokt purchaseFinalized method - Add `purchaseFinalized` method to the Flutter Rokt API - Implement native calls for `purchaseFinalized` on Android and iOS - Add unit tests to verify the new functionality - Add a test helper to clear placeholders for test isolation Signed-off-by: Thomson Thomas --- .../MparticleFlutterSdkPlugin.kt | 21 +++++++ .../SwiftMparticleFlutterSdkPlugin.swift | 17 +++++- lib/mparticle_flutter_sdk.dart | 27 ++++++++ test/mparticle_flutter_sdk_test.dart | 61 +++++++++++++++++++ 4 files changed, 123 insertions(+), 3 deletions(-) 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 35de048..3b5b2e5 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 @@ -226,6 +226,7 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler { result.success(true) } "roktSelectPlacements" -> this.roktSelectPlacements(call, result) + "roktPurchaseFinalized" -> this.roktPurchaseFinalized(call, result) else -> { result.notImplemented() } @@ -750,6 +751,26 @@ class MparticleFlutterSdkPlugin: FlutterPlugin, MethodCallHandler { 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 145cc63..e77f1f9 100644 --- a/ios/Classes/SwiftMparticleFlutterSdkPlugin.swift +++ b/ios/Classes/SwiftMparticleFlutterSdkPlugin.swift @@ -527,7 +527,7 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin { } } } - + var roktConfig: MPRoktConfig? if let configMap = callArguments["config"] as? [String: Any] { roktConfig = buildRoktConfig(configMap: configMap) @@ -537,12 +537,23 @@ public class SwiftMparticleFlutterSdkPlugin: NSObject, FlutterPlugin { registerPartnerFonts(typefaces) } - MParticle.sharedInstance().rokt.selectPlacements(placementId, attributes: attributes, placements: placeholders, config: roktConfig, callbacks: callback) + 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)") } @@ -566,7 +577,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, + })); + }); + }); }