Apple отклоняет мое приложение iOS по следующей причине:
- 2.1. После покупки In App Purchase 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
Пожалуйста, помогите понять, что может быть не так.