iOS自动续订订阅收据验证问题

时间:2020-05-07 23:03:44

标签: ios app-store subscription receipt-validation auto-renewing

Apple由于以下原因拒绝了我的iOS应用程序:

  • 2.1购买了“应用内购买”后,App Store进行了身份验证和确认,但该应用无法验证收据。

在沙盒环境中测试应用程序时,我没有遇到这种情况。我成功停用了购买的广告-这是我在iPad上进行购买时的示例视频,然后在iPhone上还原了该视频:

https://photos.app.goo.gl/jeH1gtSKroF7QjVCA

我正在处理IAPManager类中的所有购买-完整代码如下:

//
//  IAPManager.m
//  Sudoku
//
//  Created by szulcu on 03/06/2019.
//  Copyright © 2019 AliorBank. All rights reserved.
//

#import "IAPManager.h"
#import "Utils.h"

@interface IAPManager() <SKProductsRequestDelegate, SKPaymentTransactionObserver>

@property(strong, nonatomic) SKProductsRequest *productsRequest;
@property(strong, nonatomic) NSArray<SKProduct*> *validProducts;
@property(nonatomic, assign) long long validPurchaseMs;
@property(strong, nonatomic) NSTimer *timer;

@end

@implementation IAPManager

+ (instancetype)sharedInstance
{
    static IAPManager *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[IAPManager alloc] init];
        sharedInstance.validPurchaseMs = -1;
    });
    return sharedInstance;
}

- (void)restorePurchase
{
    [SKPaymentQueue.defaultQueue restoreCompletedTransactions];
}

- (void)addTransactionObserver{
    [SKPaymentQueue.defaultQueue addTransactionObserver:self];
}

- (void)removeTransactionObserver{
    [SKPaymentQueue.defaultQueue removeTransactionObserver:self];
}

- (void)fetchAvailableProducts
{
    NSSet *productIdentifiers = [NSSet setWithArray:_productIdentifiers];
    _productsRequest = [[SKProductsRequest alloc]
                        initWithProductIdentifiers:productIdentifiers];
    _productsRequest.delegate = self;
    [_productsRequest start];
}

- (BOOL)canMakePurchases
{
    return [SKPaymentQueue canMakePayments];
}

- (void)purchaseMyProduct:(SKProduct*)product
{
    SKPayment *payment = [SKPayment paymentWithProduct:product];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma mark StoreKit Delegate

-(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing:
                NSLog(@"Purchasing");
                break;

            case SKPaymentTransactionStatePurchased:
                NSLog(@"Purchased ");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                if(self.delegate != nil)
                {
                    if([self.delegate  respondsToSelector:@selector(productWithIdentifier:valid:)])
                    {
                        [self.delegate productWithIdentifier:transaction.payment.productIdentifier valid:[self checkInAppPurchaseStatus]];
                    }
                }
                break;

            case SKPaymentTransactionStateRestored:
                NSLog(@"Restored ");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                if(self.delegate != nil)
                {
                    if([self.delegate  respondsToSelector:@selector(productWithIdentifier:valid:)])
                    {
                        [self.delegate productWithIdentifier:transaction.payment.productIdentifier valid:[self checkInAppPurchaseStatus]];
                    }
                }
                break;

            case SKPaymentTransactionStateFailed:
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                NSLog(@"Purchase failed ");
                break;
            default:
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                break;
        }

        if(self.delegate != nil)
        {
            if([self.delegate  respondsToSelector:@selector(transactionStateChanged:)])
            {
                [self.delegate transactionStateChanged:transaction.transactionState];
            }
        }

    }
}

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    if ([response.products count] > 0) {
        _validProducts = response.products;
    }
}

- (BOOL)hasProducts
{
    return _validProducts !=nil;
}

- (void)showPurchaseDialogInViewController:(UIViewController*) viewController completion:(void (^)(NSString*)) completion
{
    if([self canMakePurchases] == NO)
    {
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"iap_warning", nil) message:NSLocalizedString(@"iap_disabled_msg", nil) preferredStyle:UIAlertControllerStyleAlert];

        UIAlertAction *cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"dialogNewGameOk", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
            completion(IAP_DIALOG_COMPLETION_CANCEL);
        }];

        [alertController addAction:cancel];
        [viewController presentViewController:alertController animated:YES completion:nil];
    }
    else if(self.hasProducts)
    {

        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"iap_purchase", nil) message:@"\n\n\n\n\n\n\n\n" preferredStyle:UIAlertControllerStyleActionSheet];

        weakify(self)

        CGFloat margin = 8;
        int textViewWidth = alertController.view.bounds.size.width - margin*4;
        if ([Utils isIpad]) {
            textViewWidth = 304 - margin*2;
        }

        UITextView *linkTextView = [[UITextView alloc] initWithFrame:CGRectMake(margin, margin*4, textViewWidth+100, 60)];
        linkTextView.text = NSLocalizedString(@"iap_select_subscription_link", nil);
        linkTextView.backgroundColor = UIColor.clearColor;
        linkTextView.scrollEnabled = NO;
        linkTextView.dataDetectorTypes = UIDataDetectorTypeLink;
        linkTextView.editable = NO;
        [alertController.view addSubview:linkTextView];

        UITextView *descriptionTextView = [[UITextView alloc] initWithFrame:CGRectMake(margin, margin*4 + 60, textViewWidth, 120)];
        descriptionTextView.text = NSLocalizedString(@"iap_select_subscription", nil);
        descriptionTextView.backgroundColor = UIColor.clearColor;
        descriptionTextView.scrollEnabled = YES;
        descriptionTextView.showsVerticalScrollIndicator = YES;
        descriptionTextView.dataDetectorTypes = UIDataDetectorTypeLink;
        descriptionTextView.editable = NO;
        [alertController.view addSubview:descriptionTextView];
        ///

        for(SKProduct *product in _validProducts)
        {
            UIAlertAction *action = [UIAlertAction actionWithTitle:[NSString stringWithFormat:@"%@ - %@", product.localizedTitle, product.localizedDescription] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
                [weak_self purchaseMyProduct:product];
                completion(IAP_DIALOG_COMPLETION_PURCHASED);
            }];
            [alertController addAction:action];
        }

        UIAlertAction *restore = [UIAlertAction actionWithTitle:NSLocalizedString(@"iap_restore_purchase", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
            [weak_self restorePurchase];
            completion(IAP_DIALOG_COMPLETION_RESTORED);
        }];

        UIAlertAction *cancel = [UIAlertAction actionWithTitle:NSLocalizedString(@"dialogNewGameCancel", nil) style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
           completion(IAP_DIALOG_COMPLETION_CANCEL);
        }];

        [alertController addAction:restore];
        [alertController addAction:cancel];

        [alertController.popoverPresentationController setPermittedArrowDirections:0];
        CGRect rect = viewController.view.frame;
        rect.origin.x = viewController.view.frame.size.width/20;
        rect.origin.y = viewController.view.frame.size.height/20;
        alertController.popoverPresentationController.sourceView = viewController.view;
        alertController.popoverPresentationController.sourceRect = rect;
        [viewController presentViewController:alertController animated:YES completion:nil];
    }
    else
    {

        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"iap_warning", nil) message:NSLocalizedString(@"iap_no_products_warning", nil) preferredStyle:UIAlertControllerStyleActionSheet];

        UIAlertAction *ok = [UIAlertAction actionWithTitle:NSLocalizedString(@"dialogNewGameOk", nil) style:UIAlertActionStyleCancel handler:nil];

        [alertController addAction:ok];
        [viewController presentViewController:alertController animated:YES completion:nil];
    }
}

- (BOOL)checkInAppPurchaseStatus
{
    if(self.validPurchaseMs > 0)
    {
        return YES;
    }

    // Load the receipt from the app bundle.
    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];
    if (receipt) {
        BOOL sandbox = [[receiptURL lastPathComponent] isEqualToString:@"sandboxReceipt"];
        // Create the JSON object that describes the request
        NSError *error;
        NSDictionary *requestContents = @{
            @"receipt-data": [receipt base64EncodedStringWithOptions:0], @"password":IAP_SHARED_SECRET, @"exclude-old-transactions" : @YES
                                          };
        NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                              options:0
                                                                error:&error];

        if (requestData) {
            // Create a POST request with the receipt data.
            NSURL *storeURL = [NSURL URLWithString:@"https://buy.itunes.apple.com/verifyReceipt"];
            if (sandbox) {
                storeURL = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
            }
            NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
            [storeRequest setHTTPMethod:@"POST"];
            [storeRequest setHTTPBody:requestData];

            BOOL validPurchase = NO;
            //Can use sendAsynchronousRequest to request to Apple API, here I use sendSynchronousRequest
            NSError *error;
            NSURLResponse *response;

            NSData *resData = [self sendSynchronousRequest:storeRequest returningResponse:&response error:&error];

            if (error) {
                validPurchase = NO;
            }
            else
            {
                NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:resData options:0 error:&error];
                if (!jsonResponse) {
                    validPurchase = NO;
                }
                else
                {
                    NSLog(@"jsonResponse:%@", jsonResponse);

                    NSArray *latestReceiptsInfo = jsonResponse[@"latest_receipt_info"];
                    long long expirationDateMs = [[latestReceiptsInfo valueForKeyPath:@"@max.expires_date_ms"] longLongValue];
                    long long requestDateMs = [jsonResponse[@"receipt"][@"request_date_ms"] longLongValue];
                    NSLog(@"%lld--%lld", expirationDateMs, requestDateMs);
                    validPurchase = [[jsonResponse objectForKey:@"status"] integerValue] == 0 && (expirationDateMs > requestDateMs);
                    self.validPurchaseMs = expirationDateMs - requestDateMs;

                    if(self.validPurchaseMs > 0)
                    {
                        weakify(self)

                        if (@available(iOS 10.0, *)) {
                            _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
                                [weak_self timerAction];
                            }];
                        } else {
                            // Fallback on earlier versions
                            _timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
                        }
                    }
                }
            }
            return validPurchase;
        }
        else
        {
            return NO;
        }
    }
    else
    {
        return NO;
    }
}

- (void)timerAction
{
    self.validPurchaseMs -= 1000;
    if(self.validPurchaseMs < 0)
    {
        [self.timer invalidate];
        [self.delegate productWithIdentifier:@"" valid:NO];
    }
}

- (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error
{

    NSError __block *err = NULL;
    NSData __block *data;
    BOOL __block reqProcessed = false;
    NSURLResponse __block *resp;

    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable _data, NSURLResponse * _Nullable _response, NSError * _Nullable _error) {
        resp = _response;
        err = _error;
        data = _data;
        reqProcessed = true;
    }] resume];

    while (!reqProcessed) {
        [NSThread sleepForTimeInterval:0];
    }

    *response = resp;
    *error = err;
    return data;
}

- (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    for(SKPaymentTransaction *transaction in transactions){
        NSLog(@"Transaction: %@", transaction);
    }
}

// Sent when an error is encountered while adding transactions from the user's purchase history back to the queue.
- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error{

}

// Sent when all transactions from the user's purchase history have successfully been added back to the queue.
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue{

}

// Sent when the download state has changed.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray<SKDownload *> *)downloads{

}

// Sent when a user initiates an IAP buy from the App Store
- (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product{
    return true;
}

- (void)paymentQueueDidChangeStorefront:(SKPaymentQueue *)queue{

}

@end

请帮助您了解可能出了什么问题。

1 个答案:

答案 0 :(得分:0)

https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store

警告

请勿从您的应用程序调用App Store服务器的verifyReceipt端点。您无法直接在用户的设备与App Store之间建立信任的连接,因为您无法控制该连接的任何一端,这会使它容易受到中间人攻击。

我认为这就是苹果拒绝您的应用的原因。

相关问题