I've found some very odd issues with base64 encoding of iOS7 receipts.
At the moment (Xcode5/iOS7) have 2 methods for getting a receipt for an in-app purchase:
[SKPaymentTransaction transactionReceipt]
appStoreReceiptURL
My App sells web site based services using a credit system which makes server receipt validation necessary. The App also sells downloadable extensions. So a mixture of consumables and non-consumables. The non-consumables are downloaded from Apple so no verification required. The consumables are packages of credits used for purchasing services on the website.
When iOS7 & XCode5 launched I updated my App but struggled with the new bundled receipt located at appStoreReceiptURL. With the new style, all receipts bundled in one, my server got back this error from Apple's verifyReceipt sandbox
[status] => 21002
[exception] => java.lang.IllegalArgumentException
I gave up after seening posts on stackoverflow saying others had experienced the same issue and that the opinion at the time was it was a bug, or yet to be added feature, on Apple's part. However I've been seeing issues with using the old deprecated method which has forced me back to trying again. After a lot of debugging and searching I finally worked out some of what was going wrong - as well as getting it working but in a very ugly way which I'm not confident with going live with.
I would have thought that NSData (plain byte buffer) encoded output wouldn't differ much. Here's the weird part I'm seeing.
The original deprecated single receipts can be base64 encode either as 64 character lines with \r\n on the end or without. Apple verification server doesn't care. It's happy either way.
For the new receipt bundle it won't work unless 2 things are done. First the base64 encoding cannot have line breaks. I notice Apple has added it's own base64 encoder into iOS7. This is what I'm using to get an encoded output that works for both receipt types.
NSString *receiptDataString = [transactionReceipt base64EncodedStringWithOptions:0];
The 2nd thing that needs to be done is to search and replace all space characters from the received encoded receipt bundle on the server. i.e.
$receiptdata = str_replace(' ', '+', $receiptdata);
I happened to noticed this was a difference between the receipts being received by my server. Why I don't know? I'm using AFNetworking-1.3.2's JSONRequestOperationWithRequest
NSMutableArray *pairs = [[NSMutableArray alloc] initWithCapacity:0];
for (NSString *key in parameters) {
[pairs addObject:[NSString stringWithFormat:@"%@=%@", key, [parameters objectForKey:key]]];
}
postData = [[pairs componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://myserver.com/index.php"]];
[request setHTTPMethod:@"POST"];
[request setValue:[NSString stringWithFormat:@"%d", postData.length] forHTTPHeaderField:@"Content-Length"];
[request setValue:@"application/x-www-form-urlencoded charset=utf-8" forHTTPHeaderField:@"Content-Type"];
[request setHTTPBody:postData];
operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
Chris Prince claims to have it working with AFNetworking 2 in his reply to this question
Why could these two NSData receipts cause encoding/decoding problems like I'm seeing? I've been as detailed and clear as I can be here to help others as I see there's a lot of folks feeling the same pain here with this new receipt method Apple's introduced.
The answer turned out to be due to
stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding
not escaping all special characters like :/?#[]@!$&’()*+,;= etc. The new receipt Apple provides, once base64 encoded, contains characters which need escaping but are not escaped by the above iOS lib escape encoder. Looking around I found one on here which works well. See below for working code. (Server doesn't need to parse receipt code now and can use it straight out of $_POST)
// Lifted from:
// http://stackoverflow.com/questions/2159341/nsstring-method-to-percent-escape-for-url
//
- (NSString *)urlEncodeValue:(NSString *)str {
NSString *result = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)str, NULL, CFSTR(":/?#[]@!$&’()*+,;="), kCFStringEncodingUTF8));
return result;
}
// Encode and pair basic parameters
NSMutableArray *pairs = [[NSMutableArray alloc] initWithCapacity:0];
NSString *part;
for (NSString *key in parameters) {
NSString *encodedValue = [[parameters objectForKey:key] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSString *encodedKey = [key stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
part = [NSString stringWithFormat: @"%@=%@", encodedKey, encodedValue];
[pairs addObject:part];
[pairs addObject:[NSString stringWithFormat:@"%@=%@", key, [parameters objectForKey:key]]];
}
// Receipt encoded and paired separately
receiptDataString = [self urlEncodeValue:receiptDataString];
part = [NSString stringWithFormat: @"receipt=%@", receiptDataString];
[pairs addObject:part];
// Post data.
postData = [[pairs componentsJoinedByString:@"&"] dataUsingEncoding:NSUTF8StringEncoding];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With