diff --git a/Adjust.podspec b/Adjust.podspec index a09a96184..c6712c7c4 100644 --- a/Adjust.podspec +++ b/Adjust.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = "Adjust" - s.version = "3.2.1" + s.version = "3.3.0" s.summary = "This is the iOS SDK of Adjust. You can read more about it at http://adjust.io." s.homepage = "http://adjust.io" s.license = { :type => 'MIT', :file => 'MIT-LICENSE' } s.author = { "Christian Wellenbrock" => "welle@adjust.com" } - s.source = { :git => "https://github.com/adeven/adjust_ios_sdk.git", :tag => "v3.2.1" } + s.source = { :git => "https://github.com/adeven/adjust_ios_sdk.git", :tag => "v3.3.0" } s.platform = :ios, '4.3' s.framework = 'SystemConfiguration' s.weak_framework = 'AdSupport' diff --git a/Adjust/AIActivityHandler.h b/Adjust/AIActivityHandler.h index d71ff81d1..f6417e549 100644 --- a/Adjust/AIActivityHandler.h +++ b/Adjust/AIActivityHandler.h @@ -32,6 +32,7 @@ - (void)finishedTrackingWithResponse:(AIResponseData *)response; - (void)setEnabled:(BOOL)enabled; - (BOOL)isEnabled; +- (void)readOpenUrl:(NSURL*)url; @end diff --git a/Adjust/AIActivityHandler.m b/Adjust/AIActivityHandler.m index ae02c5ed8..2b8008cf1 100644 --- a/Adjust/AIActivityHandler.m +++ b/Adjust/AIActivityHandler.m @@ -19,10 +19,11 @@ #import "AIAdjustFactory.h" static NSString * const kActivityStateFilename = @"AdjustIoActivityState"; +static NSString * const kAdjustPrefix = @"adjust_"; static const char * const kInternalQueueName = "io.adjust.ActivityQueue"; -static const uint64_t kTimerInterval = 60 * NSEC_PER_SEC; // 1 minute -static const uint64_t kTimerLeeway = 1 * NSEC_PER_SEC; // 1 second +static const uint64_t kTimerInterval = 60 * NSEC_PER_SEC; // 1 minute +static const uint64_t kTimerLeeway = 1 * NSEC_PER_SEC; // 1 second #pragma mark - @@ -141,6 +142,12 @@ - (BOOL)isEnabled { } } +- (void)readOpenUrl:(NSURL*)url { + dispatch_async(self.internalQueue, ^{ + [self readOpenUrlInternal:url]; + }); +} + #pragma mark - internal - (void)initInternal:(NSString *)yourAppToken { if (![self checkAppTokenNotNil:yourAppToken]) return; @@ -309,6 +316,40 @@ - (void)revenueInternal:(double)amount [self.logger debug:@"Event %d (revenue)", self.activityState.eventCount]; } +- (void) readOpenUrlInternal:(NSURL *)url { + NSArray* queryArray = [url.query componentsSeparatedByString:@"&"]; + NSMutableDictionary* adjustDeepLinks = [NSMutableDictionary dictionary]; + + for (NSString* fieldValuePair in queryArray) { + NSArray* pairComponents = [fieldValuePair componentsSeparatedByString:@"="]; + if (pairComponents.count != 2) continue; + + NSString* key = [pairComponents objectAtIndex:0]; + if (![key hasPrefix:kAdjustPrefix]) continue; + + NSString* value = [pairComponents objectAtIndex:1]; + if (value.length == 0) continue; + + NSString* keyWOutPrefix = [key substringFromIndex:kAdjustPrefix.length]; + if (keyWOutPrefix.length == 0) continue; + + [adjustDeepLinks setObject:value forKey:keyWOutPrefix]; + } + + if (adjustDeepLinks.count == 0) { + return; + } + + AIPackageBuilder *reattributionBuilder = [[AIPackageBuilder alloc] init]; + reattributionBuilder.deeplinkParameters = adjustDeepLinks; + [self injectGeneralAttributes:reattributionBuilder]; + AIActivityPackage *reattributionPackage = [reattributionBuilder buildReattributionPackage]; + [self.packageHandler addPackage:reattributionPackage]; + [self.packageHandler sendFirstPackage]; + + [self.logger debug:@"Reattribution %@", adjustDeepLinks]; +} + #pragma mark - private // returns whether or not the activity state should be written @@ -358,7 +399,7 @@ - (void)writeActivityState { BOOL result = [NSKeyedArchiver archiveRootObject:self.activityState toFile:filename]; if (result == YES) { [AIUtil excludeFromBackup:filename]; - [self.logger verbose:@"Wrote activity state: %@", self.activityState]; + [self.logger debug:@"Wrote activity state: %@", self.activityState]; } else { [self.logger error:@"Failed to write activity state"]; } diff --git a/Adjust/AIActivityKind.h b/Adjust/AIActivityKind.h index bcba929fa..ff39f99db 100644 --- a/Adjust/AIActivityKind.h +++ b/Adjust/AIActivityKind.h @@ -9,10 +9,11 @@ #import typedef enum { - AIActivityKindUnknown = 0, - AIActivityKindSession = 1, - AIActivityKindEvent = 2, - AIActivityKindRevenue = 3, + AIActivityKindUnknown = 0, + AIActivityKindSession = 1, + AIActivityKindEvent = 2, + AIActivityKindRevenue = 3, + AIActivityKindReattribution = 4, } AIActivityKind; AIActivityKind AIActivityKindFromString(NSString *string); diff --git a/Adjust/AIActivityKind.m b/Adjust/AIActivityKind.m index 93e283884..1bbabf6f8 100644 --- a/Adjust/AIActivityKind.m +++ b/Adjust/AIActivityKind.m @@ -15,6 +15,8 @@ AIActivityKind AIActivityKindFromString(NSString *string) { return AIActivityKindEvent; } else if ([@"revenue" isEqualToString:string]) { return AIActivityKindRevenue; + } else if ([@"reattribution" isEqualToString:string]) { + return AIActivityKindReattribution; } else { return AIActivityKindUnknown; } @@ -22,9 +24,10 @@ AIActivityKind AIActivityKindFromString(NSString *string) { NSString* AIActivityKindToString(AIActivityKind activityKind) { switch (activityKind) { - case AIActivityKindSession: return @"session"; - case AIActivityKindEvent: return @"event"; - case AIActivityKindRevenue: return @"revenue"; - case AIActivityKindUnknown: return @"unknown"; + case AIActivityKindSession: return @"session"; + case AIActivityKindEvent: return @"event"; + case AIActivityKindRevenue: return @"revenue"; + case AIActivityKindReattribution: return @"reattribution"; + case AIActivityKindUnknown: return @"unknown"; } } diff --git a/Adjust/AIPackageBuilder.h b/Adjust/AIPackageBuilder.h index c939a97a0..8e356664a 100644 --- a/Adjust/AIPackageBuilder.h +++ b/Adjust/AIPackageBuilder.h @@ -37,8 +37,13 @@ @property (nonatomic, copy) NSDictionary *callbackParameters; @property (nonatomic, assign) double amountInCents; +// reattributions +@property (nonatomic, copy) NSDictionary* deeplinkParameters; + + - (AIActivityPackage *)buildSessionPackage; - (AIActivityPackage *)buildEventPackage; - (AIActivityPackage *)buildRevenuePackage; +- (AIActivityPackage *)buildReattributionPackage; @end diff --git a/Adjust/AIPackageBuilder.m b/Adjust/AIPackageBuilder.m index 6b02d9eaf..d52ffafef 100644 --- a/Adjust/AIPackageBuilder.m +++ b/Adjust/AIPackageBuilder.m @@ -54,6 +54,19 @@ - (AIActivityPackage *)buildRevenuePackage { return revenuePackage; } +- (AIActivityPackage *)buildReattributionPackage { + NSMutableDictionary *parameters = [self defaultParameters]; + [self parameters:parameters setDictionaryJson:self.deeplinkParameters forKey:@"deeplink_parameters"]; + + AIActivityPackage *reattributionPackage = [self defaultActivityPackage]; + reattributionPackage.path = @"/reattribute"; + reattributionPackage.activityKind = AIActivityKindReattribution; + reattributionPackage.suffix = @""; + reattributionPackage.parameters = parameters; + + return reattributionPackage; +} + #pragma mark private - (AIActivityPackage *)defaultActivityPackage { AIActivityPackage *activityPackage = [[AIActivityPackage alloc] init]; @@ -87,9 +100,9 @@ - (NSMutableDictionary *)defaultParameters { - (void)injectEventParameters:(NSMutableDictionary *)parameters { // event specific - [self parameters:parameters setInt:self.eventCount forKey:@"event_count"]; - [self parameters:parameters setString:self.eventToken forKey:@"event_token"]; - [self parameters:parameters setDictionary:self.callbackParameters forKey:@"params"]; + [self parameters:parameters setInt:self.eventCount forKey:@"event_count"]; + [self parameters:parameters setString:self.eventToken forKey:@"event_token"]; + [self parameters:parameters setDictionaryBase64:self.callbackParameters forKey:@"params"]; } - (NSString *)amountString { @@ -138,7 +151,7 @@ - (void)parameters:(NSMutableDictionary *)parameters setDuration:(double)value f [self parameters:parameters setInt:intValue forKey:key]; } -- (void)parameters:(NSMutableDictionary *)parameters setDictionary:(NSDictionary *)dictionary forKey:(NSString *)key { +- (void)parameters:(NSMutableDictionary *)parameters setDictionaryBase64:(NSDictionary *)dictionary forKey:(NSString *)key { if (dictionary == nil) return; NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; @@ -146,5 +159,13 @@ - (void)parameters:(NSMutableDictionary *)parameters setDictionary:(NSDictionary [self parameters:parameters setString:dictionaryString forKey:key]; } +- (void)parameters:(NSMutableDictionary *)parameters setDictionaryJson:(NSDictionary *)dictionary forKey:(NSString *)key { + if (dictionary == nil) return; + + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:nil]; + NSString *dictionaryString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + [self parameters:parameters setString:dictionaryString forKey:key]; +} + @end diff --git a/Adjust/AIUtil.m b/Adjust/AIUtil.m index 3714b2ba6..ef7c9cac7 100644 --- a/Adjust/AIUtil.m +++ b/Adjust/AIUtil.m @@ -14,7 +14,7 @@ #include static NSString * const kBaseUrl = @"https://app.adjust.io"; -static NSString * const kClientSdk = @"ios3.2.1"; +static NSString * const kClientSdk = @"ios3.3.0"; static NSString * const kDateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'Z"; static NSDateFormatter * dateFormat; diff --git a/Adjust/Adjust.h b/Adjust/Adjust.h index 71db0b850..f40eb8a57 100644 --- a/Adjust/Adjust.h +++ b/Adjust/Adjust.h @@ -164,6 +164,11 @@ static NSString * const AIEnvironmentProduction = @"production"; */ + (BOOL)isEnabled; +/** + * Read the URL that opened the application to search for + * an adjust deep link + */ ++ (void)appWillOpenUrl:(NSURL *)url; @end diff --git a/Adjust/Adjust.m b/Adjust/Adjust.m index c82d51d83..0a71e70c0 100644 --- a/Adjust/Adjust.m +++ b/Adjust/Adjust.m @@ -132,4 +132,8 @@ + (BOOL)isEnabled { return [activityHandler isEnabled]; } ++ (void)appWillOpenUrl:(NSURL *)url { + [activityHandler readOpenUrl:url]; +} + @end diff --git a/AdjustTests/AIActivityHandlerMock.m b/AdjustTests/AIActivityHandlerMock.m index 693f9b0c1..7689ababc 100644 --- a/AdjustTests/AIActivityHandlerMock.m +++ b/AdjustTests/AIActivityHandlerMock.m @@ -72,4 +72,8 @@ - (BOOL)isEnabled { return YES; } +- (void)readOpenUrl:(NSURL *)url { + [self.loggerMock test:[prefix stringByAppendingFormat:@"readOpenUrl"]]; +} + @end diff --git a/AdjustTests/AIActivityHandlerTests.m b/AdjustTests/AIActivityHandlerTests.m index 78bf373ae..5310b5efd 100644 --- a/AdjustTests/AIActivityHandlerTests.m +++ b/AdjustTests/AIActivityHandlerTests.m @@ -71,10 +71,12 @@ - (void)testFirstRun [NSThread sleepForTimeInterval:10.0]; // test that the file did not exist in the first run of the application - XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Activity state file not found"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Activity state file not found"], + @"%@", self.loggerMock); // when a session package is being sent the package handler should resume sending - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], + @"%@", self.loggerMock); // if the package was build, it was sent to the Package Handler XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler addPackage"], @"%@", self.loggerMock); @@ -86,7 +88,7 @@ - (void)testFirstRun AIActivityPackage *activityPackage = (AIActivityPackage *) self.packageHandlerMock.packageQueue[0]; // check the Sdk version is being tested - XCTAssertEqual(@"ios3.2.1", activityPackage.clientSdk, @"%@", activityPackage.extendedString); + XCTAssertEqual(@"ios3.3.0", activityPackage.clientSdk, @"%@", activityPackage.extendedString); // packageType should be SESSION_START XCTAssertEqual(@"/startup", activityPackage.path, @"%@", activityPackage.extendedString); @@ -111,14 +113,15 @@ - (void)testFirstRun XCTAssertNil(parameters[@"last_interval"], @"%@", activityPackage.extendedString); // after adding, the activity handler ping the Package handler to send the package - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], + @"%@", self.loggerMock); // check that the package handler calls back with the delegate //XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AdjustDelegate adjustFinishedTrackingWithResponse"], // @"%@", self.loggerMock); // check that the activity state is written by the first session or timer - XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Wrote activity state: "], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Wrote activity state: "], @"%@", self.loggerMock); // ending of first session XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"First session"], @"%@", self.loggerMock); @@ -151,7 +154,8 @@ - (void)testSessions { [NSThread sleepForTimeInterval:1]; // check that a new subsession was created - XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"Processed Subsession 2 of Session 1"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"Processed Subsession 2 of Session 1"], + @"%@", self.loggerMock); // check that it's now on the 2nd session XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Session 2"], @"%@", self.loggerMock); @@ -171,7 +175,8 @@ - (void)testSessions { XCTAssertEqual(2, [(NSString *)parameters[@"subsession_count"] intValue], @"%@", activityPackage.extendedString); // check that the package handler was paused - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], + @"%@", self.loggerMock); } - (void)testEventsBuffered { @@ -219,13 +224,14 @@ - (void)testEventsBuffered { // check the injected parameters XCTAssert([(NSString *)eventPackageParameters[@"params"] isEqualToString:@"eyJrZXkiOiJ2YWx1ZSIsImZvbyI6ImJhciJ9"], - @"%@", eventPackage.extendedString); + @"%@", eventPackage.extendedString); // check that the event was buffered XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"Buffered event 'abc123'"], @"%@", self.loggerMock); // check the event count in the written activity state - XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Wrote activity state: ec:1"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Wrote activity state: ec:1"], + @"%@", self.loggerMock); // check the event count in the logger XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1"], @"%@", self.loggerMock); @@ -250,17 +256,20 @@ - (void)testEventsBuffered { XCTAssertEqual(45, [(NSString *)revenuePackageParameters[@"amount"] intValue], @"%@", revenuePackage.extendedString); // check the event token - XCTAssert([(NSString *)revenuePackageParameters[@"event_token"] isEqualToString:@"abc123"], @"%@", revenuePackage.extendedString); + XCTAssert([(NSString *)revenuePackageParameters[@"event_token"] isEqualToString:@"abc123"], + @"%@", revenuePackage.extendedString); // check the injected parameters XCTAssert([(NSString *)revenuePackageParameters[@"params"] isEqualToString:@"eyJrZXkiOiJ2YWx1ZSIsImZvbyI6ImJhciJ9"], - @"%@", eventPackage.extendedString); + @"%@", eventPackage.extendedString); // check that the revenue was buffered - XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"Buffered revenue (4.5 cent, 'abc123')"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"Buffered revenue (4.5 cent, 'abc123')"], + @"%@", self.loggerMock); // check the event count in the written activity state - XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Wrote activity state: ec:2"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Wrote activity state: ec:2"], + @"%@", self.loggerMock); // check the event count in the logger XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 2 (revenue)"], @"%@", self.loggerMock); @@ -307,10 +316,12 @@ - (void)testEventsNotBuffered { XCTAssertNil(eventPackageParameters[@"params"], @"%@", eventPackage.extendedString); // check that the package handler was called - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], + @"%@", self.loggerMock); // check the event count in the written activity state - XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Wrote activity state: ec:1"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Wrote activity state: ec:1"], + @"%@", self.loggerMock); // check the event count in the logger XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1"], @"%@", self.loggerMock); @@ -341,10 +352,12 @@ - (void)testEventsNotBuffered { XCTAssertNil(eventPackageParameters[@"params"], @"%@", eventPackage.extendedString); // check that the package handler was called - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], + @"%@", self.loggerMock); // check the event count in the written activity state - XCTAssert([self.loggerMock containsMessage:AILogLevelVerbose beginsWith:@"Wrote activity state: ec:2"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Wrote activity state: ec:2"], + @"%@", self.loggerMock); // check the event count in the logger XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 2 (revenue)"], @"%@", self.loggerMock); @@ -400,19 +413,22 @@ - (void)testChecks { XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Missing App Token"], @"%@", self.loggerMock); // check the invalid app token message - XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Malformed App Token '12345678901'"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Malformed App Token '12345678901'"], + @"%@", self.loggerMock); // check the nil event token XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Missing Event Token"], @"%@", self.loggerMock); // check the invalid event token - XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Malformed Event Token 'abc1234'"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Malformed Event Token 'abc1234'"], + @"%@", self.loggerMock); // check the invalid revenue amount token XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Invalid amount -0.1"], @"%@", self.loggerMock); // check the invalid revenue token - XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Malformed Event Token 'abc12'"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelError beginsWith:@"Malformed Event Token 'abc12'"], + @"%@", self.loggerMock); } @@ -448,10 +464,12 @@ - (void)testDisable { XCTAssert([self.loggerMock containsMessage:AILogLevelInfo beginsWith:@"First session"], @"%@", self.loggerMock); // delete the first session package from the log - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], + @"%@", self.loggerMock); // making sure the timer fired did not call the package handler - XCTAssertFalse([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], @"%@", self.loggerMock); + XCTAssertFalse([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler sendFirstPackage"], + @"%@", self.loggerMock); // test if the event was not triggered XCTAssertFalse([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1"], @"%@", self.loggerMock); @@ -460,10 +478,12 @@ - (void)testDisable { XCTAssertFalse([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 1 (revenue)"], @"%@", self.loggerMock); // verify that the application was paused - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], + @"%@", self.loggerMock); // verify that it was not resumed - XCTAssertFalse([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], @"%@", self.loggerMock); + XCTAssertFalse([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], + @"%@", self.loggerMock); // enable again [activityHandler setEnabled:YES]; @@ -485,11 +505,73 @@ - (void)testDisable { XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Event 2 (revenue)"], @"%@", self.loggerMock); // verify that the application was paused - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler pauseSending"], + @"%@", self.loggerMock); // verify that it was also resumed - XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], @"%@", self.loggerMock); + XCTAssert([self.loggerMock containsMessage:AILogLevelTest beginsWith:@"AIPackageHandler resumeSending"], + @"%@", self.loggerMock); +} + +- (void)testOpenUrl { + // reseting to make the test order independent + [self reset]; + + // starting from a clean slate + XCTAssert([AITestsUtil deleteFile:@"AdjustIoActivityState" logger:self.loggerMock], @"%@", self.loggerMock); + + // create handler to start the session + id activityHandler = [AIAdjustFactory activityHandlerWithAppToken:@"123456789012"]; + + NSString* normal = @"AdjustTests://example.com/path/inApp?adjust_foo=bar&other=stuff&adjust_key=value"; + NSString* emptyQueryString = @"AdjustTests://"; + NSString* emptyString = @""; + NSString* single = @"AdjustTests://example.com/path/inApp?adjust_foo"; + NSString* prefix = @"AdjustTests://example.com/path/inApp?adjust_=bar"; + NSString* incomplete = @"AdjustTests://example.com/path/inApp?adjust_foo="; + + [activityHandler readOpenUrl:[NSURL URLWithString:normal]]; + [activityHandler readOpenUrl:[NSURL URLWithString:emptyQueryString]]; + [activityHandler readOpenUrl:[NSURL URLWithString:emptyString]]; + [activityHandler readOpenUrl:[NSURL URLWithString:single]]; + [activityHandler readOpenUrl:[NSURL URLWithString:prefix]]; + [activityHandler readOpenUrl:[NSURL URLWithString:incomplete]]; + + [NSThread sleepForTimeInterval:2]; + + // check that all supposed packages were sent + // 1 session + 1 reattributions + XCTAssertEqual((NSUInteger)2, [self.packageHandlerMock.packageQueue count], @"%@", self.loggerMock); + + // check that the normal url was parsed and sent + AIActivityPackage *package = (AIActivityPackage *) self.packageHandlerMock.packageQueue[1]; + + // testing the activity kind is the correct one + AIActivityKind activityKind = package.activityKind; + XCTAssertEqual(AIActivityKindReattribution, activityKind, @"%@", package.extendedString); + + // testing the conversion from activity kind to string + NSString* activityKindString = AIActivityKindToString(activityKind); + XCTAssertEqual(@"reattribution", activityKindString); + + // testing the conversion from string to activity kind + activityKind = AIActivityKindFromString(activityKindString); + XCTAssertEqual(AIActivityKindReattribution, activityKind); + + // packageType should be reattribute + XCTAssertEqual(@"/reattribute", package.path, @"%@", package.extendedString); + + // suffix should be empty + XCTAssertEqual(@"", package.suffix, @"%@", package.extendedString); + + NSDictionary *parameters = package.parameters; + + // check that deep link parameters contains the base64 with the 2 keys + XCTAssert([(NSString *)parameters[@"deeplink_parameters"] isEqualToString:@"{\"foo\":\"bar\",\"key\":\"value\"}"], + @"%@", parameters.description); + // check that sent the reattribution package + XCTAssert([self.loggerMock containsMessage:AILogLevelDebug beginsWith:@"Reattribution {\n foo = bar;\n key = value;\n}"], @"%@", self.loggerMock); } @end diff --git a/README.md b/README.md index 74008bbb9..21eeace9f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ If you're using [CocoaPods][cocoapods], you can add the following line to your `Podfile` and continue with [step 3](#step3): ```ruby -pod 'Adjust', :git => 'git://github.com/adjust/ios_sdk.git', :tag => 'v3.2.1' +pod 'Adjust', :git => 'git://github.com/adjust/ios_sdk.git', :tag => 'v3.3.0' ``` ### 1. Get the SDK @@ -257,8 +257,8 @@ The delegate function will get called every time any activity was tracked or failed to track. Within the delegate function you have access to the `responseData` parameter. Here is a quick summary of its attributes: -- `AIActivityKind activityKind` indicates what kind of activity - was tracked. It has one of these values: +- `AIActivityKind activityKind` indicates what kind of activity was tracked. It + has one of these values: ``` AIActivityKindSession @@ -266,7 +266,8 @@ failed to track. Within the delegate function you have access to the AIActivityKindRevenue ``` -- `NSString activityKindString` human readable version of the activity kind. Possible values: +- `NSString activityKindString` human readable version of the activity kind. + Possible values: ``` session @@ -298,17 +299,35 @@ in the `didFinishLaunching` method of your Application Delegate: ### 10. Disable tracking -You can disable the adjust SDK from tracking by invoking the method `setEnabled` -with the enabled parameter as `NO`. This setting is remembered between sessions, but it can only -be activated after the first session. +You can disable the adjust SDK from tracking by invoking the method +`setEnabled` with the enabled parameter as `NO`. This setting is remembered +between sessions, but it can only be activated after the first session. ```objc [Adjust setEnabled:NO]; ``` -You can verify if the adjust SDK is currently active with the method `isEnabled`. It is always possible -to activate the adjust SDK by invoking `setEnabled` with the enabled parameter as `YES`. +You can verify if the adjust SDK is currently active with the method +`isEnabled`. It is always possible to activate the adjust SDK by invoking +`setEnabled` with the enabled parameter as `YES`. +### 11. Handle deep linking + +You can also set up the adjust SDK to read deep links that come to your app, +also known as custom URL schemes in iOS. We will only read the data that is +injected by adjust tracker URLs. This is essential if you are planning to run +retargeting or re-engagement campaigns with deep links. + +In the Project Navigator open the source file your Application Delegate. Find +or add the method `openURL` and add the following call to adjust: + +```objc +- (BOOL) application:(UIApplication *)application openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +{ + [Adjust appWillOpenUrl:url]; +} +``` [adjust.io]: http://adjust.io [cocoapods]: http://cocoapods.org @@ -325,7 +344,7 @@ to activate the adjust SDK by invoking `setEnabled` with the enabled parameter a ## License -The adjust-sdk is licensed under the MIT License. +The adjust-SDK is licensed under the MIT License. Copyright (c) 2012-2013 adeven GmbH, http://www.adeven.com diff --git a/VERSION b/VERSION index e4604e3af..15a279981 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.2.1 +3.3.0 diff --git a/doc/migrate.md b/doc/migrate.md index c9ecbac24..7b81dd9db 100644 --- a/doc/migrate.md +++ b/doc/migrate.md @@ -1,4 +1,4 @@ -## Migrate your adjust SDK for iOS to v3.2.1 from v3.0.0 +## Migrate your adjust SDK for iOS to v3.3.0 from v3.0.0 We added an optional parameter `transactionId` to our `trackRevenue` methods. If you are tracking In-App Purchases you might want to pass in the transaction identifier provided by Apple to avoid duplicate revenue tracking. It should look roughly like this: @@ -36,14 +36,14 @@ all adjust SDK calls. ![][rename] -3. Download version v3.2.1 and drag the new folder `Adjust` into your Xcode +3. Download version v3.3.0 and drag the new folder `Adjust` into your Xcode Project Navigator. ![][drag] 4. Build your project to confirm that everything is properly connected again. -The adjust SDK v3.2.1 added delegate callbacks. Check out the [README] for +The adjust SDK v3.3.0 added delegate callbacks. Check out the [README] for details. @@ -99,7 +99,7 @@ meaningful at all times! Especially if you are tracking revenue. 1. The `appDidLaunch` method now expects your App Token instead of your App ID. You can find your App Token in your [dashboard]. -2. The adjust SDK for iOS 3.2.1 uses [ARC][arc]. If you haven't done already, +2. The adjust SDK for iOS 3.3.0 uses [ARC][arc]. If you haven't done already, we recommend [transitioning your project to use ARC][transition] as well. If you don't want to use ARC, you have to enable ARC for all files of the adjust SDK. Please consult the [README] for details.