Skip to content
Closed
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
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ This document will help you:

- Install the mParticle SDK using [CocoaPods](https://cocoapods.org/?q=mparticle) or [Carthage](https://github.com/Carthage/Carthage)
- Add any desired [kits](#currently-supported-kits)
- Control kit startup with [consent](#consent)
- Initialize the mParticle SDK

## Get the SDK
Expand Down Expand Up @@ -137,6 +138,56 @@ Several integrations require additional client-side add-on libraries called "kit
| [UserLeap](https://github.com/UserLeap/userleap-mparticle-ios-kit) | ✓ | ✓ |
| [Wootric](https://github.com/mparticle-integrations/mparticle-apple-integration-wootric) | ✓ | |

## Consent

The mParticle Apple SDK supports gating **integration kit** initialization until the host app has collected user consent.

- `hasConsent` defaults to `false`
- When `hasConsent` is `false`, **kits will not initialize/start**
- The core mParticle SDK continues to initialize and function normally

### Swift

Start kits immediately:

```swift
let options = MParticleOptions(key: "<<<App Key Here>>>", secret: "<<<App Secret Here>>>")
options.hasConsent = true
MParticle.sharedInstance().start(with: options)
```

Defer kit startup until consent is collected:

```swift
let options = MParticleOptions(key: "<<<App Key Here>>>", secret: "<<<App Secret Here>>>")
// options.hasConsent is false by default
MParticle.sharedInstance().start(with: options)

// Later, when the user has granted consent:
MParticle.sharedInstance().hasConsent = true
```

### Objective-C

Start kits immediately:

```objective-c
MParticleOptions *options = [MParticleOptions optionsWithKey:@"<<<App Key Here>>>" secret:@"<<<App Secret Here>>>"];
options.hasConsent = YES;
[[MParticle sharedInstance] startWithOptions:options];
```

Defer kit startup until consent is collected:

```objective-c
MParticleOptions *options = [MParticleOptions optionsWithKey:@"<<<App Key Here>>>" secret:@"<<<App Secret Here>>>"];
// options.hasConsent is NO by default
[[MParticle sharedInstance] startWithOptions:options];

// Later, when the user has granted consent:
[MParticle sharedInstance].hasConsent = YES;
```

## Initialize the SDK

The mParticle SDK is initialized by calling the `startWithOptions` method within the `application:didFinishLaunchingWithOptions:` delegate call. Preferably the location of the initialization method call should be one of the last statements in the `application:didFinishLaunchingWithOptions:`. The `startWithOptions` method requires an options argument containing your key and secret and an initial Identity request.
Expand Down
3 changes: 3 additions & 0 deletions UnitTests/ObjCTests/MPBaseTestCase.m
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ - (void)setUpWithCompletionHandler:(void (^)(NSError * _Nullable))completion {

[instance reset:^{
MPNetworkCommunication_PRIVATE.connectorFactory = [[MPTestConnectorFactory alloc] init];
// Most unit tests assume legacy behavior where kits are allowed to initialize immediately.
// Tests that need consent-gated behavior can override this in their own setup.
[MParticle sharedInstance].hasConsent = YES;
completion(nil);
}];
}
Expand Down
116 changes: 116 additions & 0 deletions UnitTests/ObjCTests/MPKitConsentGateTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>

#import "mParticle.h"
#import "MPKitContainer.h"
#import "MPIConstants.h"

#import "MParticle+PrivateMethods.h"

@interface MPKitConsentGateTests : XCTestCase
@end

@implementation MPKitConsentGateTests

- (void)setUp {
[super setUp];

// Ensure we control the shared instance for these tests.
MParticle *instance = [[MParticle alloc] init];
[MParticle setSharedInstance:instance];
}

- (void)tearDown {
[MParticle setSharedInstance:nil];
[super tearDown];
}

- (void)testConfigureKitsIsDeferredUntilHasConsent {
MParticle *mp = [MParticle sharedInstance];
mp.hasConsent = NO;

MPKitContainer_PRIVATE *kitContainer = [[MPKitContainer_PRIVATE alloc] init];
mp.kitContainer_PRIVATE = kitContainer;

NSArray<NSDictionary *> *kitConfig = @[
@{@"id": @42, @"as": @{}}
];

[kitContainer configureKits:kitConfig];

XCTAssertEqualObjects(mp.deferredKitConfiguration_PRIVATE, kitConfig);
XCTAssertFalse(kitContainer.kitsInitialized);
}

- (void)testInitializeKitsIsDeferredUntilHasConsent {
MParticle *mp = [MParticle sharedInstance];
mp.hasConsent = NO;

MPKitContainer_PRIVATE *kitContainer = [[MPKitContainer_PRIVATE alloc] init];
mp.kitContainer_PRIVATE = kitContainer;

[kitContainer initializeKits];

XCTAssertFalse(kitContainer.kitsInitialized);
}

- (void)testSettingHasConsentProcessesDeferredKitConfigurationWithoutConsentFilters {
MParticle *mp = [MParticle sharedInstance];
mp.hasConsent = NO;

NSArray<NSDictionary *> *deferredConfig = @[
@{@"id": @42, @"as": @{}}
];
mp.deferredKitConfiguration_PRIVATE = deferredConfig;

id kitContainerMock = OCMProtocolMock(@protocol(MPKitContainerProtocol));
XCTestExpectation *expectation = [self expectationWithDescription:@"configureKits called"];

OCMExpect([kitContainerMock configureKits:deferredConfig]).andDo(^(NSInvocation *invocation) {
[expectation fulfill];
});

[mp setKitContainer:kitContainerMock];

mp.hasConsent = YES;

[self waitForExpectationsWithTimeout:2 handler:nil];
OCMVerifyAll(kitContainerMock);
XCTAssertNil(mp.deferredKitConfiguration_PRIVATE);
}

- (void)testSettingHasConsentDoesNotProcessDeferredConfigurationRequiringIdentity {
MParticle *mp = [MParticle sharedInstance];
mp.hasConsent = NO;

// Force a "no initial identity" state (MPID == 0) to exercise the deferral path.
[mp.identity.currentUser setValue:@0 forKey:@"userId"];

NSArray<NSDictionary *> *deferredConfig = @[
@{
@"id": @42,
@"as": @{},
// Include a consent filter marker so the SDK continues to defer until identity is resolved.
kMPConsentKitFilter: @{ @"i": @YES }
}
];
mp.deferredKitConfiguration_PRIVATE = deferredConfig;

id kitContainerMock = OCMProtocolMock(@protocol(MPKitContainerProtocol));
XCTestExpectation *expectation = [self expectationWithDescription:@"configureKits not called"];
expectation.inverted = YES;

OCMStub([kitContainerMock configureKits:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
[expectation fulfill];
});

[mp setKitContainer:kitContainerMock];

mp.hasConsent = YES;

[self waitForExpectationsWithTimeout:0.5 handler:nil];
XCTAssertEqualObjects(mp.deferredKitConfiguration_PRIVATE, deferredConfig);
}

@end

2 changes: 2 additions & 0 deletions UnitTests/ObjCTests/MPURLRequestBuilderTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ - (void)tearDown {

- (void)testCustomUserAgent {
MParticleOptions *options = [MParticleOptions optionsWithKey:@"testKey" secret:@"testSecret"];
options.hasConsent = YES;
options.customUserAgent = @"Test User Agent";
[[MParticle sharedInstance] startWithOptions:options];

Expand All @@ -125,6 +126,7 @@ - (void)testCustomUserAgent {

- (void)testDisableCollectUserAgent {
MParticleOptions *options = [MParticleOptions optionsWithKey:@"testKey" secret:@"testSecret"];
options.hasConsent = YES;
options.customUserAgent = nil;
options.collectUserAgent = NO;
[[MParticle sharedInstance] startWithOptions:options];
Expand Down
Loading