diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b3e75ba3..6ad1cb4d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,29 @@ For each release, **Core** (main SDK) changes are listed first, followed by **Ki ## [Unreleased] +### Core + +#### Added + +- Add `MPRokt.handleURLCallback:` for forwarding Afterpay/PayPal redirect URLs to the registered Rokt payment extension. Call from `application:openURL:options:` (AppDelegate) or `scene:openURLContexts:` / `.onOpenURL` (Scene/SwiftUI). + +#### Changed + +- Bump minimum `RoktContracts` to 2.0.0 (adds `PaymentMethodType.paypal` and totals on `PaymentPreparation`). + +### Kits + +#### Rokt + +##### Changed + +- Bump minimum `Rokt-Widget` to 5.1.0 (adds Afterpay payment support). +- Bump minimum `RoktContracts` to 2.0.0. + +##### Added + +- Pass through `handleURLCallback:` to `Rokt.handleURLCallback(with:)` on the Rokt SDK. + ## [9.0.1] - 2026-04-22 ### Core diff --git a/Example/Podfile b/Example/Podfile index 4bfc05d59..773e3fb4e 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -10,15 +10,15 @@ target 'mParticleExample' do pod 'mParticle-Apple-Media-SDK/mParticleMediaNoLocation', '~> 1.7.0' - # Rokt-Widget pulls in RoktUXHelper (~> 0.8) transitively. This explicit line is only to resolve it + # Rokt-Widget pulls in RoktUXHelper (~> 0.10) transitively. This explicit line is only to resolve it # from Git (tag) instead of CocoaPods trunk when CDN/trunk is unreliable — remove it if trunk works. - pod 'RoktUXHelper', :git => 'https://github.com/ROKT/rokt-ux-helper-ios.git', :tag => '0.8.3' + pod 'RoktUXHelper', :git => 'https://github.com/ROKT/rokt-ux-helper-ios.git', :tag => '0.10.4' - # Rokt iOS SDK 5.x (aligned with Kits/rokt/rokt/Package.swift). Use Git branch; not on CocoaPods trunk. - pod 'Rokt-Widget', :git => 'https://github.com/ROKT/rokt-sdk-ios.git', :branch => 'workstation/5.0.0' + # Rokt iOS SDK 5.1.x (aligned with Kits/rokt/rokt/Package.swift). Use Git tag; not on CocoaPods trunk. + pod 'Rokt-Widget', :git => 'https://github.com/ROKT/rokt-sdk-ios.git', :tag => '5.1.0' # Local Rokt kit — uses the same local mParticle-Apple-SDK above (single resolved copy via :path). - # RoktContracts resolves from CocoaPods trunk per podspecs (~> 0.1). + # RoktContracts resolves from CocoaPods trunk per podspecs (~> 2.0). pod 'mParticle-Rokt', :path => '../Kits/rokt/rokt' #pod 'PLCrashReporter', '~> 1.11.1' #pod 'mParticle-UrbanAirship', :path => '../../mparticle-apple-integration-urbanairship' diff --git a/Example/mParticleExample/AppDelegate.m b/Example/mParticleExample/AppDelegate.m index 767afcda8..78ab13c47 100644 --- a/Example/mParticleExample/AppDelegate.m +++ b/Example/mParticleExample/AppDelegate.m @@ -86,6 +86,16 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( } +// Forward redirect URLs (e.g. Afterpay/PayPal) to Rokt so registered payment extensions can complete the payment flow. +// If your app uses UIScene, call handleURLCallback(with:) from scene:openURLContexts: / .onOpenURL instead. +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + if ([[MParticle sharedInstance].rokt handleURLCallback:url]) { + return YES; + } + return NO; +} + + - (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. diff --git a/Kits/rokt/rokt/Package.swift b/Kits/rokt/rokt/Package.swift index c199ae92b..166d89a63 100644 --- a/Kits/rokt/rokt/Package.swift +++ b/Kits/rokt/rokt/Package.swift @@ -32,11 +32,11 @@ let package = Package( mParticleAppleSDK, .package( url: "https://github.com/ROKT/rokt-sdk-ios", - .upToNextMajor(from: "5.0.0") + .upToNextMajor(from: "5.1.0") ), .package( url: "https://github.com/ROKT/rokt-contracts-apple.git", - .upToNextMajor(from: "0.1.3") + .upToNextMajor(from: "2.0.0") ), .package( url: "https://github.com/erikdoe/ocmock", diff --git a/Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m b/Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m index 15565af9a..d6084b96f 100644 --- a/Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m +++ b/Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m @@ -149,6 +149,17 @@ - (MPKitExecStatus *)selectPlacementsWithIdentifier:(NSString * _Nullable)identi return [[MPKitExecStatus alloc] initWithSDKCode:[[self class] kitCode] returnCode:MPKitReturnCodeSuccess]; } +/// Forwards a redirect URL to the Rokt SDK so registered payment extensions (Afterpay, PayPal) can claim it. +/// - Parameter url: The URL received by the host app's URL handler. +/// - Returns: YES if a registered payment extension claimed the URL. +- (BOOL)handleURLCallback:(NSURL * _Nonnull)url { + if (!url) { + return NO; + } + [MPKitRokt MPLog:[NSString stringWithFormat:@"Rokt Kit handleURLCallback: %@", url]]; + return [Rokt handleURLCallbackWith:url]; +} + /// Forwards to Rokt Shoppable payment registration. When kit \c configuration includes \c stripePublishableKey (mParticle kit settings), it is passed to Rokt as \c stripeKey in the registration config. - (MPKitExecStatus *)registerPaymentExtension:(id _Nonnull)paymentExtension { if (!paymentExtension) { diff --git a/Kits/rokt/rokt/Tests/mParticle-RoktObjCTests/mParticle_RoktTests.m b/Kits/rokt/rokt/Tests/mParticle-RoktObjCTests/mParticle_RoktTests.m index f03ee4c71..7444b5358 100644 --- a/Kits/rokt/rokt/Tests/mParticle-RoktObjCTests/mParticle_RoktTests.m +++ b/Kits/rokt/rokt/Tests/mParticle-RoktObjCTests/mParticle_RoktTests.m @@ -43,6 +43,8 @@ + (void)logSelectShoppableAdsEvent:(NSDictionary * _Nonn - (MPKitExecStatus *)registerPaymentExtension:(id)paymentExtension; +- (BOOL)handleURLCallback:(NSURL *)url; + - (MPKitExecStatus *)selectShoppableAdsWithIdentifier:(NSString *)identifier attributes:(NSDictionary *)attributes config:(RoktConfig *)config @@ -1106,6 +1108,43 @@ - (void)testRegisterPaymentExtensionForwardsToRoktWithConfigurationStripeKey { [mockRoktSDK stopMocking]; } +- (void)testHandleURLCallbackForwardsToRoktAndReturnsYES { + id mockRoktSDK = OCMClassMock([Rokt class]); + NSURL *url = [NSURL URLWithString:@"myapp://afterpay-redirect?token=xyz"]; + OCMExpect([mockRoktSDK handleURLCallbackWith:url]).andReturn(YES); + + BOOL handled = [self.kitInstance handleURLCallback:url]; + + XCTAssertTrue(handled); + OCMVerifyAll(mockRoktSDK); + [mockRoktSDK stopMocking]; +} + +- (void)testHandleURLCallbackForwardsToRoktAndReturnsNO { + id mockRoktSDK = OCMClassMock([Rokt class]); + NSURL *url = [NSURL URLWithString:@"myapp://unrelated"]; + OCMExpect([mockRoktSDK handleURLCallbackWith:url]).andReturn(NO); + + BOOL handled = [self.kitInstance handleURLCallback:url]; + + XCTAssertFalse(handled); + OCMVerifyAll(mockRoktSDK); + [mockRoktSDK stopMocking]; +} + +- (void)testHandleURLCallbackNilURLReturnsNOWithoutForwarding { + id mockRoktSDK = OCMClassMock([Rokt class]); + OCMReject([mockRoktSDK handleURLCallbackWith:OCMOCK_ANY]); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + BOOL handled = [self.kitInstance handleURLCallback:nil]; +#pragma clang diagnostic pop + + XCTAssertFalse(handled); + [mockRoktSDK stopMocking]; +} + - (void)testSelectShoppableAdsInvokesRoktAndLogsEvent { id mockRoktSDK = OCMClassMock([Rokt class]); OCMExpect([mockRoktSDK selectShoppableAdsWithIdentifier:@"ShopView" diff --git a/Kits/rokt/rokt/mParticle-Rokt.podspec b/Kits/rokt/rokt/mParticle-Rokt.podspec index ec62ee961..a356e75d0 100644 --- a/Kits/rokt/rokt/mParticle-Rokt.podspec +++ b/Kits/rokt/rokt/mParticle-Rokt.podspec @@ -14,6 +14,6 @@ Pod::Spec.new do |s| s.ios.source_files = 'Sources/mParticle-Rokt/**/*.{h,m}', 'Sources/mParticle-Rokt-Swift/**/*.swift' s.ios.resource_bundles = { 'mParticle-Rokt-Privacy' => ['Sources/mParticle-Rokt/PrivacyInfo.xcprivacy'] } s.ios.dependency 'mParticle-Apple-SDK', '~> 9.0' - s.ios.dependency 'RoktContracts', '~> 0.1' - s.ios.dependency 'Rokt-Widget', '~> 5.0' + s.ios.dependency 'RoktContracts', '~> 2.0' + s.ios.dependency 'Rokt-Widget', '~> 5.1' end diff --git a/MIGRATING.md b/MIGRATING.md index f560f99a1..394cc6605 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -4,6 +4,57 @@ This document provides migration guidance for breaking changes in the mParticle Apple SDK. +## Migrating from 9.0.x to 9.1.0 + +### Rokt SDK 5.1.0 + RoktContracts 2.0.0 + +SDK 9.1.0 bumps the Rokt dependencies to pick up Afterpay payment support shipped in Rokt SDK 5.1.0 and PayPal / payment‑preparation totals shipped in RoktContracts 2.0.0. The dependency minimums moved from: + +- `Rokt-Widget` `~> 5.0` → `~> 5.1` +- `RoktContracts` `~> 0.1` → `~> 2.0` + +If your `Podfile` pins `RoktContracts '~> 0.1'` transitively, loosen it to `'~> 2.0'` so CocoaPods can resolve the new minimum. SPM users using the mParticle manifest inherit the new bound automatically. + +The payment extension package previously published at `https://github.com/ROKT/rokt-stripe-payment-extension-ios` has been **renamed to `https://github.com/ROKT/rokt-payment-extension-ios`** (the old name implied Stripe‑only, but it now supports Afterpay and PayPal). Update any `Package.swift` / `Podfile` references to the new URL. mParticle does not depend on this package directly — it is pulled in by partners alongside `mParticle-Rokt`. + +### New `MPRokt.handleURLCallback:` API + +Redirect‑based payment flows (Afterpay, PayPal) send users into a web view and then back into your app via a registered URL scheme. To complete the round trip, forward the incoming URL to Rokt via `MPRokt.handleURLCallback:`. Call it from your URL handler and return the `BOOL` it produces so the system knows the URL was handled. + +**Objective‑C (AppDelegate):** + +```objective-c +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + if ([[MParticle sharedInstance].rokt handleURLCallback:url]) { + return YES; + } + return NO; +} +``` + +**Swift (SceneDelegate):** + +```swift +func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + for urlContext in URLContexts { + _ = MParticle.sharedInstance().rokt.handleURLCallback(with: urlContext.url) + } +} +``` + +**Swift (SwiftUI):** + +```swift +WindowGroup { + ContentView() + .onOpenURL { url in + _ = MParticle.sharedInstance().rokt.handleURLCallback(with: url) + } +} +``` + +Your `Info.plist` setup is unchanged — register the URL scheme you pass to your `RoktPaymentExtension` initializer under `CFBundleURLTypes` exactly as in a direct‑Rokt integration. + ## Migrating from versions < 9.0.0 ### Removed AppDelegateProxy diff --git a/Package.swift b/Package.swift index 9eb906725..e742a6f8f 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/ROKT/rokt-contracts-apple.git", - .upToNextMajor(from: "0.1.0") + .upToNextMajor(from: "2.0.0") ) ], targets: [ diff --git a/UnitTests/ObjCTests/MPRoktTests.m b/UnitTests/ObjCTests/MPRoktTests.m index 1af6b9141..98d69ab6b 100644 --- a/UnitTests/ObjCTests/MPRoktTests.m +++ b/UnitTests/ObjCTests/MPRoktTests.m @@ -14,16 +14,23 @@ // Rokt kit identifier for testing static NSNumber * const kTestRoktKitId = @181; -// Test helper class that simulates a kit with getSessionId method +// Test helper class that simulates a kit with getSessionId and handleURLCallback methods @interface MPRoktTestKitInstance : NSObject @property (nonatomic, copy) NSString *sessionIdToReturn; +@property (nonatomic, assign) BOOL handleURLCallbackReturn; +@property (nonatomic, strong) NSURL *lastHandleURLCallbackURL; - (NSString *)getSessionId; +- (BOOL)handleURLCallback:(NSURL *)url; @end @implementation MPRoktTestKitInstance - (NSString *)getSessionId { return self.sessionIdToReturn; } +- (BOOL)handleURLCallback:(NSURL *)url { + self.lastHandleURLCallbackURL = url; + return self.handleURLCallbackReturn; +} @end @interface MPRokt () @@ -1168,4 +1175,92 @@ - (void)testGetSessionIdReturnsNilWhenKitInstanceIsNil { XCTAssertNil(result, @"Should return nil when kit wrapper instance is nil"); } -@end +#pragma mark - handleURLCallback Tests + +- (void)testHandleURLCallbackReturnsYESWhenKitClaimsURL { + MParticle *instance = [MParticle sharedInstance]; + self.mockInstance = OCMPartialMock(instance); + self.mockContainer = OCMClassMock([MPKitContainer_PRIVATE class]); + [[[self.mockInstance stub] andReturn:self.mockContainer] kitContainer_PRIVATE]; + [[[self.mockInstance stub] andReturn:self.mockInstance] sharedInstance]; + + id mockKitRegister = OCMProtocolMock(@protocol(MPExtensionKitProtocol)); + OCMStub([(id)mockKitRegister code]).andReturn(kTestRoktKitId); + + MPRoktTestKitInstance *kitInstance = [[MPRoktTestKitInstance alloc] init]; + kitInstance.handleURLCallbackReturn = YES; + OCMStub([mockKitRegister wrapperInstance]).andReturn(kitInstance); + + OCMStub([self.mockContainer activeKitsRegistry]).andReturn(@[mockKitRegister]); + + NSURL *url = [NSURL URLWithString:@"myapp://afterpay-redirect?token=abc"]; + BOOL result = [self.rokt handleURLCallback:url]; + + XCTAssertTrue(result, @"Should return YES when the kit claims the URL"); + XCTAssertEqualObjects(kitInstance.lastHandleURLCallbackURL, url, @"Kit should have received the URL"); +} + +- (void)testHandleURLCallbackReturnsNOWhenKitDoesNotClaimURL { + MParticle *instance = [MParticle sharedInstance]; + self.mockInstance = OCMPartialMock(instance); + self.mockContainer = OCMClassMock([MPKitContainer_PRIVATE class]); + [[[self.mockInstance stub] andReturn:self.mockContainer] kitContainer_PRIVATE]; + [[[self.mockInstance stub] andReturn:self.mockInstance] sharedInstance]; + + id mockKitRegister = OCMProtocolMock(@protocol(MPExtensionKitProtocol)); + OCMStub([(id)mockKitRegister code]).andReturn(kTestRoktKitId); + + MPRoktTestKitInstance *kitInstance = [[MPRoktTestKitInstance alloc] init]; + kitInstance.handleURLCallbackReturn = NO; + OCMStub([mockKitRegister wrapperInstance]).andReturn(kitInstance); + + OCMStub([self.mockContainer activeKitsRegistry]).andReturn(@[mockKitRegister]); + + NSURL *url = [NSURL URLWithString:@"myapp://unrelated"]; + BOOL result = [self.rokt handleURLCallback:url]; + + XCTAssertFalse(result, @"Should return NO when the kit does not claim the URL"); +} + +- (void)testHandleURLCallbackReturnsNOWhenNoActiveKits { + MParticle *instance = [MParticle sharedInstance]; + self.mockInstance = OCMPartialMock(instance); + self.mockContainer = OCMClassMock([MPKitContainer_PRIVATE class]); + [[[self.mockInstance stub] andReturn:self.mockContainer] kitContainer_PRIVATE]; + [[[self.mockInstance stub] andReturn:self.mockInstance] sharedInstance]; + + OCMStub([self.mockContainer activeKitsRegistry]).andReturn(@[]); + + NSURL *url = [NSURL URLWithString:@"myapp://afterpay-redirect"]; + BOOL result = [self.rokt handleURLCallback:url]; + + XCTAssertFalse(result, @"Should return NO when no kits are active"); +} + +- (void)testHandleURLCallbackReturnsNOWhenRoktKitNotRegistered { + MParticle *instance = [MParticle sharedInstance]; + self.mockInstance = OCMPartialMock(instance); + self.mockContainer = OCMClassMock([MPKitContainer_PRIVATE class]); + [[[self.mockInstance stub] andReturn:self.mockContainer] kitContainer_PRIVATE]; + [[[self.mockInstance stub] andReturn:self.mockInstance] sharedInstance]; + + // A non-Rokt kit is registered + id mockKitRegister = OCMProtocolMock(@protocol(MPExtensionKitProtocol)); + OCMStub([(id)mockKitRegister code]).andReturn(@999); + OCMStub([self.mockContainer activeKitsRegistry]).andReturn(@[mockKitRegister]); + + NSURL *url = [NSURL URLWithString:@"myapp://afterpay-redirect"]; + BOOL result = [self.rokt handleURLCallback:url]; + + XCTAssertFalse(result, @"Should return NO when the Rokt Kit is not registered"); +} + +- (void)testHandleURLCallbackReturnsNOForNilURL { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + BOOL result = [self.rokt handleURLCallback:nil]; +#pragma clang diagnostic pop + XCTAssertFalse(result, @"Should return NO when url is nil"); +} + +@end diff --git a/mParticle-Apple-SDK-ObjC.podspec b/mParticle-Apple-SDK-ObjC.podspec index 3fde13c8e..177dd6ee5 100644 --- a/mParticle-Apple-SDK-ObjC.podspec +++ b/mParticle-Apple-SDK-ObjC.podspec @@ -35,6 +35,6 @@ Pod::Spec.new do |s| mp.source_files = 'mParticle-Apple-SDK/**/*.{h,m}' mp.resource_bundles = {'mParticle-Privacy' => ['PrivacyInfo.xcprivacy']} mp.dependency 'mParticle-Apple-SDK-Swift' - mp.dependency 'RoktContracts', '~> 0.1' + mp.dependency 'RoktContracts', '~> 2.0' end end diff --git a/mParticle-Apple-SDK.podspec b/mParticle-Apple-SDK.podspec index 55f9aecbe..48ef7b39d 100644 --- a/mParticle-Apple-SDK.podspec +++ b/mParticle-Apple-SDK.podspec @@ -22,5 +22,5 @@ Pod::Spec.new do |s| s.source_files = 'MParticle/Sources/mParticle_Apple_SDK/**/*.swift' s.dependency 'mParticle-Apple-SDK-ObjC', s.version.to_s - s.dependency 'RoktContracts', '~> 0.1' + s.dependency 'RoktContracts', '~> 2.0' end diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index 39cbf0b88..053fc4088 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -2178,7 +2178,7 @@ repositoryURL = "https://github.com/ROKT/rokt-contracts-apple.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 0.1.0; + minimumVersion = 2.0.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/mParticle-Apple-SDK/Include/MPKitProtocol.h b/mParticle-Apple-SDK/Include/MPKitProtocol.h index e7b00ab4c..3e00e2cdc 100644 --- a/mParticle-Apple-SDK/Include/MPKitProtocol.h +++ b/mParticle-Apple-SDK/Include/MPKitProtocol.h @@ -153,6 +153,7 @@ config:(RoktConfig * _Nullable)config onEvent:(void (^ _Nullable)(RoktEvent * _Nonnull))onEvent filteredUser:(FilteredMParticleUser * _Nonnull)filteredUser; +- (BOOL)handleURLCallback:(NSURL * _Nonnull)url; @end diff --git a/mParticle-Apple-SDK/Include/MPRokt.h b/mParticle-Apple-SDK/Include/MPRokt.h index 43d8dbb7a..e6400f718 100644 --- a/mParticle-Apple-SDK/Include/MPRokt.h +++ b/mParticle-Apple-SDK/Include/MPRokt.h @@ -129,5 +129,15 @@ config:(RoktConfig * _Nullable)config onEvent:(void (^ _Nullable)(RoktEvent * _Nonnull))onEvent; +/** + * Forwards a redirect URL (e.g. Afterpay, PayPal) to the registered Rokt payment extension(s). + * Call from your AppDelegate's application:openURL:options: or SceneDelegate's scene:openURLContexts: + * (SwiftUI: onOpenURL) so Rokt can complete redirect-based payment flows. + * + * @param url The URL received by your app. + * @return YES if a registered payment extension claimed the URL; NO otherwise. + */ +- (BOOL)handleURLCallback:(NSURL * _Nonnull)url NS_SWIFT_NAME(handleURLCallback(with:)); + @end diff --git a/mParticle-Apple-SDK/MPRokt.m b/mParticle-Apple-SDK/MPRokt.m index a7a7ae134..a5c867751 100644 --- a/mParticle-Apple-SDK/MPRokt.m +++ b/mParticle-Apple-SDK/MPRokt.m @@ -362,6 +362,47 @@ - (void)selectShoppableAds:(NSString * _Nonnull)identifier }]; } +/// Forwards a redirect URL (e.g. Afterpay, PayPal) to the registered Rokt payment extension(s) via the Rokt Kit. +/// Uses a synchronous kit lookup (not \c forwardSDKCall) because the caller relies on the BOOL return value. +/// - Parameter url: The URL received by the app's URL handler. +/// - Returns: YES if a registered payment extension claimed the URL; NO otherwise (including when the Rokt Kit is not registered). +- (BOOL)handleURLCallback:(NSURL * _Nonnull)url { + MPILogDebug(@"MPRokt handleURLCallback called - url: %@", url); + if (!url) { + return NO; + } + + NSArray> *activeKits = [[MParticle sharedInstance].kitContainer_PRIVATE activeKitsRegistry]; + if (!activeKits || activeKits.count == 0) { + MPILogDebug(@"MPRokt handleURLCallback - no active kits found"); + return NO; + } + + for (id kitRegister in activeKits) { + if ([kitRegister.code integerValue] == kMPRoktKitId) { + id kitInstance = kitRegister.wrapperInstance; + SEL sel = @selector(handleURLCallback:); + if (kitInstance && [kitInstance respondsToSelector:sel]) { + NSMethodSignature *signature = [kitInstance methodSignatureForSelector:sel]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setSelector:sel]; + [invocation setTarget:kitInstance]; + [invocation setArgument:&url atIndex:2]; + [invocation invoke]; + BOOL handled = NO; + [invocation getReturnValue:&handled]; + MPILogDebug(@"MPRokt handleURLCallback returning: %@", handled ? @"YES" : @"NO"); + return handled; + } + MPILogDebug(@"MPRokt handleURLCallback - kit found but doesn't respond to handleURLCallback:"); + return NO; + } + } + + MPILogDebug(@"MPRokt handleURLCallback - Rokt Kit not found in active kits"); + return NO; +} + #pragma mark - Private Helper Methods /// Applies dashboard placement attribute key mapping, then sets each non-sandbox key on the user.