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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions Example/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
10 changes: 10 additions & 0 deletions Example/mParticleExample/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIApplicationOpenURLOptionsKey,id> *)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.
Expand Down
4 changes: 2 additions & 2 deletions Kits/rokt/rokt/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 11 additions & 0 deletions Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<RoktPaymentExtension> _Nonnull)paymentExtension {
if (!paymentExtension) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ + (void)logSelectShoppableAdsEvent:(NSDictionary<NSString *, NSString *> * _Nonn

- (MPKitExecStatus *)registerPaymentExtension:(id<RoktPaymentExtension>)paymentExtension;

- (BOOL)handleURLCallback:(NSURL *)url;

- (MPKitExecStatus *)selectShoppableAdsWithIdentifier:(NSString *)identifier
attributes:(NSDictionary<NSString *, NSString *> *)attributes
config:(RoktConfig *)config
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions Kits/rokt/rokt/mParticle-Rokt.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
51 changes: 51 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIApplicationOpenURLOptionsKey,id> *)options {
if ([[MParticle sharedInstance].rokt handleURLCallback:url]) {
return YES;
}
return NO;
}
```

**Swift (SceneDelegate):**

```swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
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
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
99 changes: 97 additions & 2 deletions UnitTests/ObjCTests/MPRoktTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
Expand Down Expand Up @@ -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<MPExtensionKitProtocol>)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<MPExtensionKitProtocol>)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<MPExtensionKitProtocol>)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
2 changes: 1 addition & 1 deletion mParticle-Apple-SDK-ObjC.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion mParticle-Apple-SDK.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion mParticle-Apple-SDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
1 change: 1 addition & 0 deletions mParticle-Apple-SDK/Include/MPKitProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
config:(RoktConfig * _Nullable)config
onEvent:(void (^ _Nullable)(RoktEvent * _Nonnull))onEvent
filteredUser:(FilteredMParticleUser * _Nonnull)filteredUser;
- (BOOL)handleURLCallback:(NSURL * _Nonnull)url;

@end

Expand Down
10 changes: 10 additions & 0 deletions mParticle-Apple-SDK/Include/MPRokt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading