Skip to content

Commit

Permalink
Merge pull request #89 from adjust/receipt
Browse files Browse the repository at this point in the history
Receipt verification
  • Loading branch information
nonelse committed Mar 27, 2015
2 parents ab2d5d9 + 78fcf85 commit 6da3a2e
Show file tree
Hide file tree
Showing 13 changed files with 181 additions and 13 deletions.
4 changes: 2 additions & 2 deletions Adjust.podspec
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
Pod::Spec.new do |s|
s.name = "Adjust"
s.version = "4.0.8"
s.version = "4.1.0"
s.summary = "This is the iOS SDK of adjust. You can read more about it at http://adjust.com."
s.homepage = "http://adjust.com"
s.license = { :type => 'MIT', :file => 'MIT-LICENSE' }
s.author = { "Christian Wellenbrock" => "[email protected]" }
s.source = { :git => "https://github.com/adjust/ios_sdk.git", :tag => "v4.0.8" }
s.source = { :git => "https://github.com/adjust/ios_sdk.git", :tag => "v4.1.0" }
s.platform = :ios, '4.3'
s.framework = 'SystemConfiguration'
s.weak_framework = 'AdSupport', 'iAd'
Expand Down
10 changes: 10 additions & 0 deletions Adjust.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
969952D21A01309200928462 /* ADJAttribution.m in Sources */ = {isa = PBXBuildFile; fileRef = 969952D11A01309200928462 /* ADJAttribution.m */; };
96C0EFE01A3EF47A00B39F31 /* NSString+ADJAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96CD2BDD1A13BFC600A40AFB /* NSString+ADJAdditions.m */; };
96C0EFE11A3EF47A00B39F31 /* UIDevice+ADJAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96CD2BDF1A13BFC600A40AFB /* UIDevice+ADJAdditions.m */; };
96C93DC51AC41A8300B53F56 /* Adjust.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 96E5E34C18BBB48A008E7B30 /* Adjust.h */; };
96C93DF51AC47F2E00B53F56 /* NSData+ADJAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96C93DF41AC47F2E00B53F56 /* NSData+ADJAdditions.m */; };
96C93DF61AC47FE000B53F56 /* NSData+ADJAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96C93DF41AC47F2E00B53F56 /* NSData+ADJAdditions.m */; };
96CD2BE01A13BFC600A40AFB /* NSString+ADJAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96CD2BDD1A13BFC600A40AFB /* NSString+ADJAdditions.m */; };
96CD2BE11A13BFC600A40AFB /* UIDevice+ADJAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 96CD2BDF1A13BFC600A40AFB /* UIDevice+ADJAdditions.m */; };
96E5E38118BBB48A008E7B30 /* Adjust.m in Sources */ = {isa = PBXBuildFile; fileRef = 96E5E34D18BBB48A008E7B30 /* Adjust.m */; };
Expand Down Expand Up @@ -86,6 +89,7 @@
dstPath = "include/$(PRODUCT_NAME)";
dstSubfolderSpec = 16;
files = (
96C93DC51AC41A8300B53F56 /* Adjust.h in CopyFiles */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -113,6 +117,8 @@
969952CE1A012F5300928462 /* ADJAttributionHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ADJAttributionHandler.m; sourceTree = "<group>"; };
969952D01A01309200928462 /* ADJAttribution.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ADJAttribution.h; sourceTree = "<group>"; };
969952D11A01309200928462 /* ADJAttribution.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ADJAttribution.m; sourceTree = "<group>"; };
96C93DF31AC47F2E00B53F56 /* NSData+ADJAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+ADJAdditions.h"; sourceTree = "<group>"; };
96C93DF41AC47F2E00B53F56 /* NSData+ADJAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+ADJAdditions.m"; sourceTree = "<group>"; };
96CD2BDC1A13BFC600A40AFB /* NSString+ADJAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+ADJAdditions.h"; sourceTree = "<group>"; };
96CD2BDD1A13BFC600A40AFB /* NSString+ADJAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+ADJAdditions.m"; sourceTree = "<group>"; };
96CD2BDE1A13BFC600A40AFB /* UIDevice+ADJAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIDevice+ADJAdditions.h"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -245,6 +251,8 @@
96CD2BDD1A13BFC600A40AFB /* NSString+ADJAdditions.m */,
96CD2BDE1A13BFC600A40AFB /* UIDevice+ADJAdditions.h */,
96CD2BDF1A13BFC600A40AFB /* UIDevice+ADJAdditions.m */,
96C93DF31AC47F2E00B53F56 /* NSData+ADJAdditions.h */,
96C93DF41AC47F2E00B53F56 /* NSData+ADJAdditions.m */,
);
path = ADJAdditions;
sourceTree = "<group>";
Expand Down Expand Up @@ -433,6 +441,7 @@
96E5E39918BBB48A008E7B30 /* ADJUtil.m in Sources */,
96E5E39818BBB48A008E7B30 /* ADJTimer.m in Sources */,
96E5E38C18BBB48A008E7B30 /* ADJActivityKind.m in Sources */,
96C93DF51AC47F2E00B53F56 /* NSData+ADJAdditions.m in Sources */,
96E5E38D18BBB48A008E7B30 /* ADJActivityPackage.m in Sources */,
965307F61A000DA400107FF9 /* ADJDeviceInfo.m in Sources */,
969952D21A01309200928462 /* ADJAttribution.m in Sources */,
Expand Down Expand Up @@ -464,6 +473,7 @@
96E5E3B418BBB49E008E7B30 /* ADJRequestHandlerMock.m in Sources */,
96E5E3B518BBB49E008E7B30 /* AIRequestHandlerTests.m in Sources */,
96ED00391A38A4CD00209110 /* ADJAttributionHandlerMock.m in Sources */,
96C93DF61AC47FE000B53F56 /* NSData+ADJAdditions.m in Sources */,
96E5E3B218BBB49E008E7B30 /* ADJPackageHandlerMock.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
15 changes: 15 additions & 0 deletions Adjust/ADJAdditions/NSData+ADJAdditions.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// NSData+ADJAdditions.h
// adjust
//
// Created by Pedro Filipe on 26/03/15.
// Copyright (c) 2015 adjust GmbH. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface NSData(ADJAdditions)

- (NSString *)adjEncodeBase64;

@end
64 changes: 64 additions & 0 deletions Adjust/ADJAdditions/NSData+ADJAdditions.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// NSData+ADJAdditions.m
// adjust
//
// Created by Pedro Filipe on 26/03/15.
// Copyright (c) 2015 adjust GmbH. All rights reserved.
//

#import "NSData+ADJAdditions.h"

@implementation NSData(ADJAdditions)

static const char _base64EncodingTable[64] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

// http://stackoverflow.com/a/4727124
- (NSString *)adjEncodeBase64 {
const unsigned char * objRawData = self.bytes;
char * objPointer;
char * strResult;

// Get the Raw Data length and ensure we actually have data
NSUInteger intLength = self.length;
if (intLength == 0) return nil;

// Setup the String-based Result placeholder and pointer within that placeholder
strResult = (char *)calloc((((intLength + 2) / 3) * 4) + 1, sizeof(char));
objPointer = strResult;

// Iterate through everything
while (intLength > 2) { // keep going until we have less than 24 bits
*objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
*objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
*objPointer++ = _base64EncodingTable[((objRawData[1] & 0x0f) << 2) + (objRawData[2] >> 6)];
*objPointer++ = _base64EncodingTable[objRawData[2] & 0x3f];

// we just handled 3 octets (24 bits) of data
objRawData += 3;
intLength -= 3;
}

// now deal with the tail end of things
if (intLength != 0) {
*objPointer++ = _base64EncodingTable[objRawData[0] >> 2];
if (intLength > 1) {
*objPointer++ = _base64EncodingTable[((objRawData[0] & 0x03) << 4) + (objRawData[1] >> 4)];
*objPointer++ = _base64EncodingTable[(objRawData[1] & 0x0f) << 2];
*objPointer++ = '=';
} else {
*objPointer++ = _base64EncodingTable[(objRawData[0] & 0x03) << 4];
*objPointer++ = '=';
*objPointer++ = '=';
}
}

// Terminate the string-based result
*objPointer = '\0';

// Return the results as an NSString object
NSString *encodedString = [NSString stringWithCString:strResult encoding:NSASCIIStringEncoding];
free(strResult);
return encodedString;
}

@end
14 changes: 12 additions & 2 deletions Adjust/ADJEvent.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
@property (nonatomic, copy, readonly) NSNumber* revenue;
@property (nonatomic, readonly) NSDictionary* callbackParameters;
@property (nonatomic, readonly) NSDictionary* partnerParameters;
@property (nonatomic, copy) NSString* transactionId;
@property (nonatomic, copy, readonly) NSString* transactionId;
@property (nonatomic, copy, readonly) NSString* currency;
@property (nonatomic, copy, readonly) NSData* receipt;

/**
* Create Event object with Event Token.
Expand Down Expand Up @@ -69,10 +70,19 @@
* A transaction ID can be used to avoid duplicate revenue events. The last ten
* transaction identifiers are remembered.
*
* @param The identifier used to avoid duplicate revenue events
* @param transactionId The identifier used to avoid duplicate revenue events
*/
- (void) setTransactionId:(NSString *)transactionId;

- (BOOL) isValid;

/**
*
* Validate a in-app-purchase receipt.
*
* @param receipt The receipt to validate
* @param transactionId The identifier used to validate the receipt and to avoid duplicate revenue events
*/
- (void) setReceipt:(NSData *)receipt transactionId:(NSString *)transactionId;

@end
24 changes: 23 additions & 1 deletion Adjust/ADJEvent.m
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,26 @@ - (BOOL) checkRevenue:(NSNumber*) revenue
- (BOOL) isValid {
if (![self checkEventToken:self.eventToken]) return NO;
if (![self checkRevenue:self.revenue currency:self.currency]) return NO;
if (![self checkReceipt:self.receipt transactionId:self.transactionId]) return NO;

return YES;
}

- (void) setReceipt:(NSData *)receipt transactionId:(NSString *)transactionId {
if (![self checkReceipt:receipt transactionId:transactionId]) return;

_receipt = receipt;
_transactionId = transactionId;
}

- (BOOL) checkReceipt:(NSData *)receipt transactionId:(NSString *)transactionId {
if (receipt != nil && transactionId == nil) {
[self.logger error:@"Missing transactionId"];
return NO;
}
return YES;
}

- (BOOL) isValidParameter:(NSString *)attribute
attributeType:(NSString *)attributeType
parameterName:(NSString *)parameterName
Expand Down Expand Up @@ -178,7 +194,13 @@ -(id)copyWithZone:(NSZone *)zone
}
copy.callbackMutableParameters = [self.callbackMutableParameters copyWithZone:zone];
copy.partnerMutableParameters = [self.partnerMutableParameters copyWithZone:zone];
copy.transactionId = [self.transactionId copyWithZone:zone];
if (self.transactionId != nil) {
if (self.receipt != nil) {
[copy setReceipt:self.receipt transactionId:self.transactionId];
} else {
[copy setTransactionId:self.transactionId];
}
}
}
return copy;
}
Expand Down
7 changes: 7 additions & 0 deletions Adjust/ADJPackageBuilder.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#import "ADJActivityPackage.h"
#import "ADJUtil.h"
#import "ADJAttribution.h"
#import "NSData+ADJAdditions.h"

#pragma mark -
@implementation ADJPackageBuilder
Expand Down Expand Up @@ -51,6 +52,12 @@ - (ADJActivityPackage *)buildEventPackage:(ADJEvent *) event{
[self parameters:parameters setDictionaryJson:event.callbackParameters forKey:@"callback_params"];
[self parameters:parameters setDictionaryJson:event.partnerParameters forKey:@"partner_params"];

if (event.receipt != nil) {
NSString *receiptBase64 = [event.receipt adjEncodeBase64];
[self parameters:parameters setString:receiptBase64 forKey:@"receipt"];
[self parameters:parameters setString:event.transactionId forKey:@"transaction_id"];
}

ADJActivityPackage *eventPackage = [self defaultActivityPackage];
eventPackage.path = @"/event";
eventPackage.activityKind = ADJActivityKindEvent;
Expand Down
2 changes: 1 addition & 1 deletion Adjust/ADJUtil.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#include <sys/xattr.h>

static NSString * const kBaseUrl = @"https://app.adjust.com";
static NSString * const kClientSdk = @"ios4.0.8";
static NSString * const kClientSdk = @"ios4.1.0";

static NSString * const kDateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'Z";
static NSDateFormatter *dateFormat;
Expand Down
16 changes: 15 additions & 1 deletion AdjustTests/ADJActivityHandlerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ - (void)testFirstRun
ADJActivityPackage *activityPackage = (ADJActivityPackage *) self.packageHandlerMock.packageQueue[0];

// check the Sdk version is being tested
XCTAssertEqual(@"ios4.0.8", activityPackage.clientSdk, @"%@", activityPackage.extendedString);
XCTAssertEqual(@"ios4.1.0", activityPackage.clientSdk, @"%@", activityPackage.extendedString);

// check the server url
XCTAssertEqual(@"https://app.adjust.com", ADJUtil.baseUrl);
Expand Down Expand Up @@ -358,6 +358,9 @@ - (void)testEventsBuffered {
// add revenue
[thirdEvent setRevenue:0 currency:@"USD"];

// add receipt information
[thirdEvent setReceipt:[@"{ \"transaction-id\" = \"t_id_2\"; }" dataUsingEncoding:NSUTF8StringEncoding] transactionId:@"t_id_2"];

// track the third event
[activityHandler trackEvent:thirdEvent];

Expand Down Expand Up @@ -449,6 +452,9 @@ - (void)testEventsBuffered {
XCTAssert([(NSString *)firstEventPackageParameters[@"revenue"] isEqualToString:@"0.0001"], @"%@", firstEventPackage.extendedString);
XCTAssert([(NSString *)firstEventPackageParameters[@"currency"] isEqualToString:@"EUR"], @"%@", firstEventPackage.extendedString);

// check the that the transaction id was not injected
XCTAssertNil(firstEventPackageParameters[@"transaction_id"], @"%@", firstEventPackage.extendedString);

// check the injected parameters
XCTAssert([(NSString *)firstEventPackageParameters[@"callback_params"] isEqualToString:@"{\"keyCall\":\"valueCall2\",\"fooCall\":\"barCall\"}"],
@"%@", firstEventPackage.extendedString);
Expand All @@ -473,6 +479,10 @@ - (void)testEventsBuffered {
XCTAssert([(NSString *)thirdEventPackageParameters[@"revenue"] isEqualToString:@"0"], @"%@", thirdEventPackage.extendedString);
XCTAssert([(NSString *)thirdEventPackageParameters[@"currency"] isEqualToString:@"USD"], @"%@", thirdEventPackage.extendedString);

// check the receipt and transaction_id
XCTAssert([(NSString *)thirdEventPackageParameters[@"receipt"] isEqualToString:@"eyAidHJhbnNhY3Rpb24taWQiID0gInRfaWRfMiI7IH0="], @"%@", thirdEventPackage.extendedString);
XCTAssert([(NSString *)thirdEventPackageParameters[@"transaction_id"] isEqualToString:@"t_id_2"], @"%@", thirdEventPackage.extendedString);

// check the that the parameters were not injected
XCTAssertNil(thirdEventPackageParameters[@"callback_params"], @"%@", thirdEventPackage.extendedString);
XCTAssertNil(thirdEventPackageParameters[@"partner_params"], @"%@", thirdEventPackage.extendedString);
Expand Down Expand Up @@ -611,6 +621,8 @@ - (void)testChecks {
[firstEvent setRevenue:0 currency:@""];
[firstEvent setRevenue:-0.0001 currency:@"EUR"];

[firstEvent setReceipt:@"value" transactionId:nil];

[activityHandler trackEvent:firstEvent];

[NSThread sleepForTimeInterval:2];
Expand Down Expand Up @@ -646,6 +658,8 @@ - (void)testChecks {
// check revenue is invalid
XCTAssert([self.loggerMock containsMessage:ADJLogLevelError beginsWith:@"Invalid amount -0.0001"], @"%@", self.loggerMock);

// check the receipt had a nil transaction id
XCTAssert([self.loggerMock containsMessage:ADJLogLevelError beginsWith:@"Missing transactionId"], @"%@", self.loggerMock);

// check the first parameters
ADJActivityPackage *firstEventPackage = (ADJActivityPackage *) self.packageHandlerMock.packageQueue[1];
Expand Down
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,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 => 'v4.0.8'
pod 'Adjust', :git => 'git://github.com/adjust/ios_sdk.git', :tag => 'v4.1.0'
```

### 1. Get the SDK
Expand Down Expand Up @@ -201,7 +201,7 @@ that you have set in your adjust dashboard.**

You can read more about revenue and event tracking in the [event tracking guide.][event-tracking]

#### Revenue deduplication
#### <a id="deduplication"></a> Revenue deduplication

You can also pass in an optional transaction ID to avoid tracking duplicate
revenues. The last ten transaction IDs are remembered and revenue events with
Expand Down Expand Up @@ -232,6 +232,25 @@ tracking revenue that is not actually being generated.
}
```

#### Receipt verification

If you track in-app purchases, you can also attach the receipt to the tracked
event. In that case our servers will verify that receipt with Apple and discard
the event if the verification failed. To make this work, you also need to send
us the transaction ID of the purchase. The transaction ID will also be used for
SDK side deduplication as explained [above](#deduplication):

```objc
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

ADJEvent *event = [ADJEvent eventWithEventToken:...];
[event setRevenue:... currency:...];
[event setReceipt:receipt transactionId:transaction.transactionIdentifier];

[Adjust trackEvent:event];
```
### 7. Set up deep link reattributions
You can set up the adjust SDK to handle deep links that are used to open your
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.0.8
4.1.0
2 changes: 1 addition & 1 deletion doc/migrate.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## Migrate your adjust SDK for iOS to v4.0.8 from v3.4.0
## Migrate your adjust SDK for iOS to v4.1.0 from v3.4.0

### Initial setup

Expand Down
Loading

0 comments on commit 6da3a2e

Please sign in to comment.