In rare cases it seems that some of my users are unable to make a non-consumable purchase. When they attempt to purchase it doesn't activate "premium" and when they restore from either their current install or a fresh install paymentQueue: updatedTransactions:
is not called.
I've added a lot of logging specifically to try and determine why the restore is not following an expected flow. During a failed restore none of the "RESTORE" category events are fired.
For reference [self success];
just displays the content view and [self fail:]
displays an error message to the user instead.
Also [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
is called in viewDidLoad
and [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
is called on button press.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {
// COMPLETION POINT - RESTORE COMPLETE***
[MBProgressHUD hideHUDForView:self.view animated:TRUE];
if ([SKPaymentQueue defaultQueue].transactions.count == 0) {
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
action:@"failure_hard"
label:@"no_purchases"
value:nil] build]];
[self fail:@"There are no items available to restore at this time."];
} else {
[self success];
}
}
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
// COMPLETION POINT - RESTORE FAILED
[MBProgressHUD hideHUDForView:self.view animated:TRUE];
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
action:@"failure_hard"
label:error.localizedDescription
value:nil] build]];
[self fail:error.localizedDescription];
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
// Make sure completion states call [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
// in order to prevent sign in popup
// http://stackoverflow.com/a/10853107/740474
[MBProgressHUD hideHUDForView:self.view animated:TRUE];
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing:
break;
case SKPaymentTransactionStateDeferred:
break;
case SKPaymentTransactionStateFailed:
// COMPLETION POINT - PURCHASE FAILED
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"PURCHASE"
action:@"failure_hard"
label:transaction.error.localizedDescription
value:nil] build]];
if (transaction.error.code != SKErrorPaymentCancelled) {
// only show error if not a cancel
[self fail:transaction.error.localizedDescription];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStatePurchased:
// COMPLETION POINT - PURCHASE SUCCESS
if ([transaction.payment.productIdentifier isEqualToString:(NSString*)productID]) {
// premium purchase successful
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"PURCHASE"
action:@"success"
label:nil
value:nil] build]];
[Utils setPremium:YES];
[self success];
} else {
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"PURCHASE"
action:@"failure_hard"
label:@"no_id"
value:nil] build]];
[self fail:@"The item you purchased was not returned from Apple servers. Please contact us."];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
case SKPaymentTransactionStateRestored:
if ([transaction.payment.productIdentifier isEqualToString:(NSString*)productID]) {
// premium purchase restored
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
action:@"restore_success"
label:nil
value:nil] build]];
[Utils setPremium:YES];
} else {
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"RESTORE"
action:@"failure_hard"
label:@"no_id"
value:nil] build]];
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
default:
// For debugging
[self.tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"STORE"
action:@"transaction_weird"
label:[NSString stringWithFormat:@"Unexpected transaction state %@", @(transaction.transactionState)]
value:nil] build]];
break;
}
}
}
Any suggestions would be appreciated
Is there any chance you are using Firebase analytics in your app?
https://firebase.google.com/docs/analytics/ios/start says
If you're tracking in-app purchases, you must initialize your transaction observer in application:didFinishLaunchingWithOptions: before initializing Firebase, or your observer may not receive all purchase notifications. See Apple's In-App Purchase Best Practices for more information.
In this case the suggestion is to init your observer before initializing Firebase analytics.
Here is a blogpost with additional details: https://www.greensopinion.com/2017/03/22/This-In-App-Purchase-Has-Already-Been-Bought.html
have you implemented below method:
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error NS_AVAILABLE_IOS(3_0);
It's one of the optional methods from SKRequestDelegate.
We also were facing the same problem of missing restore purchase call. Handling this delegate helped us. All the requests which were not even being delivered to queue due to whatever reason, were delivered in this failure delegate.
So,I think you might be facing the same issue.
Start the restore process -
-(void)restore{
isRestored = false;
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
If any transaction is successfully restored the following method is called:
-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
case SKPaymentTransactionStateRestored:
DDLogVerbose(@"Restored");
//Check with your product id if it is the right product that you want to restore
if ([transaction.payment.productIdentifier isEqualToString:IAP_PRODUCT_ID]) {
isRestored = true;
// Successfully restored the payment, provide the purchased content to the user.
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
break;
}
When payment queue has finished sending restored transactions, following method is called (If it is called means its completed the transaction and not that restore is success)-
-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{
DDLogVerbose(@"Restore completed");
if (isRestored) {
// Successfully restored
} else {
// No transaction to restore
}
}
paymentQueueRestoreCompletedTransactionsFinished
When any error occurred while restoring transactions, following method is called -
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error{
DDLogVerbose(@"Error in restoring:%@",error);
if (error.code == 0) {
// unable to connect to iTunes
}
}
Few steps to make your application behaviour as expected :
1. Add transaction observers in AppDelegate
which keeps track of delayed response & whenever your application gets launched it will update & finish the transaction in queue
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
Remove observer in applicationWillTerminate
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
2. For second step, check in-app receipt before asking from user to purchase
-(void)validateReceiptsFromAppStoreFor:(NSString *)productTag completionBlock:(void (^)(NSDictionary *receiptResponse,NSError *error))completion
{
//check if receipt exists in app bundle
//else request for refresh receipt data..
//if receipt exists,verify with server & check if product tag exists in receipt & send receipt response as success msg
//else wait for refresh request success
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
if (!receipt)
{ /* No local receipt -- handle the error. */
refreshRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:nil];
refreshRequest.delegate = self;
[refreshRequest start];
return;
}
/* ... Send the receipt data to your server ... */
NSError *error;
NSDictionary *requestContents = @{
@"password":@"Your shared secret key",
@"receipt-data": [receipt base64EncodedStringWithOptions:0]
};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
if (!requestData) { /* ... Handle error ... */ }
// Create a POST request with the receipt data.
NSString *storeURL = SANDBOX_VERIFY_RECEIPT_URL; //ITMS_PROD_VERIFY_RECEIPT_URL;
if ([[[FFGDefaults sharedDefaults] objectForKey:@"environmentType"] isEqualToString:@"prod"])
{
storeURL = PROD_VERIFY_RECEIPT_URL;
}
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:storeURL]];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue.
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (connectionError) {
/* ... Handle error ... */
if (completion) {
completion(nil,connectionError);
}
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (completion)
{
jsonResponse = jsonResponse[@"receipt"];
if ([jsonResponse[@"bundle_id"] isEqualToString:[NSBundle mainBundle].bundleIdentifier])
{
//check if product was purchased earlier..
NSString *str_productID = [CFCommonUtils productIDForPlanTag:productTag];
NSArray *receiptArr = jsonResponse[@"in_app"];
if (receiptArr && receiptArr.count>0)
{
NSArray *filteredArray = [receiptArr filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"product_id = %@",str_productID]];
if (filteredArray.count>0) {
completion(jsonResponse,error);
}
else
{
NSError *err = [NSError errorWithDomain:@"" code:100 userInfo:nil];
completion(nil,err);
}
}
else
{
NSError *err = [NSError errorWithDomain:@"" code:100 userInfo:nil];
completion(nil,err);
}
}
else
{
NSError *err = [NSError errorWithDomain:@"" code:100 userInfo:nil];
completion(nil,err);
}
}
}
}];
}
3. Handle Receipt refresh delegate methods to check receipt from updated one
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
{
#ifdef DEBUG
NSLog(@"SKRequest : didFailWithError :%@",error);
#endif
if ([request isMemberOfClass:[SKReceiptRefreshRequest class]] && refreshRequest && delegate && [delegate respondsToSelector:@selector(receiptRefreshed:error:)])
{
[self receiptRefreshed:self error:error];
refreshRequest = nil;
}
else
{
}
}
- (void)requestDidFinish:(SKRequest *)request
{
#ifdef DEBUG
NSLog(@"SKRequest : requestDidFinish ");
#endif
if ([request isMemberOfClass:[SKReceiptRefreshRequest class]] && refreshRequest && delegate && [delegate respondsToSelector:@selector(receiptRefreshed:error:)])
{
[self receiptRefreshed:self error:nil];
refreshRequest = nil;
}
else
{
}
}
-(void) receiptRefreshed:(CFStorekitManager*)ebp error:(NSError *)error
{
if (error)
{
}
else
{
[self validateSubscriptionReceiptsFromAppStoreWithRefreshReceipt:YES completion:^(NSDictionary *receiptResponse, NSError *error)
{
dispatch_async(dispatch_get_main_queue(), ^{
if (error)
{
//show subscription for purchase
}
else
{
}
});
}];
}
}
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