ios - iOS 自动续订订阅收据验证问题
问题描述
由于以下原因,Apple 拒绝了我的 iOS 应用程序:
- 2.1 购买In App Purchase后,App Store认证确认,但APP验证收据失败。
当我在沙盒环境中测试应用程序时,我没有遇到过这种情况。我在购买时成功禁用了广告 - 这是我在 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
请帮助了解可能出现的问题。
解决方案
警告
不要从您的应用程序调用 App Store 服务器 verifyReceipt 端点。您无法直接在用户设备和 App Store 之间建立可信连接,因为您无法控制该连接的任何一端,这使其容易受到中间人攻击。
我认为这就是苹果拒绝你的应用程序的原因。
推荐阅读
- javascript - 表单未提交,因为字段类型
- javascript - JavaScript for 循环仅在第一次单击事件时找到子 div 类
- gradle - OpenAPI Generator 从命令行工作,但不能从 Gradle 插件工作
- amazon-web-services - 如何使用 s3cmd 列出所有空文件夹
- javascript - JavaScript DOM 样式在 Angular 中导入动画模块后停止工作
- javascript - 我试图让“技术”这个词只在页面滚动后出现滚动功能词,但它也在顶部 onload 我不想要这个
- python - 计算两个 3D 对象之间最小距离的简单方法
- mysql - 删除 MySQL WHERE id IN (@VARIABLE) 中的记录 -- (2,3,4)
- rasa-nlu - 如何修复错误用户警告:“evaluate_every_number_of_epochs”的值大于“epochs”的值。不会进行评估
- python - 从数据框python中选择2种类型的行