iOS-自动订阅

自动订阅做完,对此做一个记录,关于开发者中心的一系列信息,此处不做详细解释,填的东西确实比较多,可以参考集成帮助.

记得添加共享密钥,否则购买会验证失败,如下图:


共享密钥.png

针对无登录用户的App,我是将他buy成功的信息保存在了UserDefaul里,如果将应用删除,下次登录需要点击恢复才可以继续使用会员服务。这种方法还是会有漏洞,无奈现在没什么好办法,有想到的麻烦告知一下。

下面直接上代码

BuyVipPublic.h

//
//  BuyVipPublic.h
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

typedef enum {
    kIAPPurchSuccess = 0,       // 成功
    kIAPPurchFailed = 1,        // 失败
    kIAPPurchCancle = 2,        // 购买
    KIAPPurchVerFailed = 3,     // 校验失败
    KIAPPurchVerSuccess = 4,    // 校验成功
    kIAPPurchNotArrow = 5,      // 不允许
}IAPPurchType;

typedef void (^IAPCompletionHandle)(IAPPurchType type,NSData *data);


@interface BuyVipPublic : NSObject

// buy
- (void)startPurchWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle;

// 恢复
- (void)restore;

@end

NS_ASSUME_NONNULL_END

BuyVipPublic.m

//
//  BuyVipPublic.m
//
#import <StoreKit/StoreKit.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <AdSupport/AdSupport.h>
#import "BuyVipPublic.h"

@interface BuyVipPublic()<SKPaymentTransactionObserver,SKProductsRequestDelegate>

@property (nonatomic,copy) NSString *purchID;

@property (nonnull,strong) IAPCompletionHandle handle;

// 恢复
@property (nonatomic,strong) NSMutableArray *resumeId;

@end

@implementation BuyVipPublic


#pragma mark - system lifecycle

- (instancetype)init{
   
    self = [super init];
    
    if (self) {
        // 监听写在程序入口,程序挂起时移除监听,这样如果有未完成的订单将会自动执行并回调 paymentQueue:updatedTransactions:方法
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    
    return self;
}

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

#pragma mark - Public Method
- (void)startPurchWithID:(NSString *)purchID completeHandle:(IAPCompletionHandle)handle{
    
    if (purchID) {
        
        if ([SKPaymentQueue canMakePayments]) {
            // buy
            self.purchID = purchID;
           
            self.handle = handle;
            
            NSSet *nsset = [NSSet setWithArray:@[purchID]];
            
            SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
            
            request.delegate = self;
            
            [request start];
        
        }else{
           
            [self handleActionWithType:kIAPPurchNotArrow data:nil];
        }
    }
}

#pragma mark - Private Method
- (void)handleActionWithType:(IAPPurchType)type data:(NSData *)data{

    switch (type) {
        case kIAPPurchSuccess:
            SSLog(@"成功");
            break;
        case kIAPPurchFailed:
            SSLog(@"失败");
            break;
        case kIAPPurchCancle:
            SSLog(@"用户取消");
            break;
        case KIAPPurchVerFailed:
            SSLog(@"订单校验失败");
            break;
        case KIAPPurchVerSuccess:
            SSLog(@"校验成功");
            break;
        case kIAPPurchNotArrow:
            SSLog(@"不允许程序内buy");
            break;
        default:
            break;
    }

    if(self.handle){
        self.handle(type,data);
    }
}
// 结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
    [self verifyPurchaseWithPaymentTransaction:transaction isTestServer:NO];
}

// 失败
- (void)failedTransaction:(SKPaymentTransaction *)transaction{
    
    if (transaction.error.code != SKErrorPaymentCancelled) {
       
        [self handleActionWithType:kIAPPurchFailed data:nil];
    
    }else{
        
        [self handleActionWithType:kIAPPurchCancle data:nil];
    }
    
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)verifyPurchaseWithPaymentTransaction:(SKPaymentTransaction *)transaction isTestServer:(BOOL)flag{

    // 验证凭据,获取返回的交易凭据
    NSURL *recepitURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:recepitURL];
    
    if(!receipt){
        // 凭证为空验证失败
        [self handleActionWithType:KIAPPurchVerFailed data:nil];
        return;
    }
    // buy成功将交易凭证发送给服务端进行再次校验
//    [self handleActionWithType:kIAPPurchSuccess data:receipt];

    NSError *error;
    NSDictionary *requestContents = @{ @"receipt-data": [receipt base64EncodedStringWithOptions:0] ,@"password":@"这里填写的是开发者中心上创建的共享密钥"};
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
    
    if (!requestData) { // 交易凭证为空验证失败
        
        [self handleActionWithType:KIAPPurchVerFailed data:nil];
        return;
    }
    
    //In the test environment, use https://sandbox.itunes.apple.com/verifyReceipt
    //In the real environment, use https://buy.itunes.apple.com/verifyReceipt

    NSString *serverString = @"https://buy.itunes.apple.com/verifyReceipt";

    if (flag) {

        serverString = @"https://sandbox.itunes.apple.com/verifyReceipt";
    }
    NSURL *storeURL = [NSURL URLWithString:serverString];
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

        if (connectionError) {

            // 无法连接服务器,校验失败
            [self handleActionWithType:KIAPPurchVerFailed data:nil];

        } else {

            NSError *error;

            NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

            SSLog(@"jsonResponse = %@",jsonResponse);

            if (!jsonResponse) {

                // 苹果服务器校验数据返回为空校验失败
                [self handleActionWithType:KIAPPurchVerFailed data:nil];
            }

            //先验证正式服务器,如果正式服务器返回21007再去苹果测试服务器验证,沙盒测试环境苹果用的是测试服务器

            NSString *status = [NSString stringWithFormat:@"%@",jsonResponse[@"status"]];

            if (status && [status isEqualToString:@"21007"]) {

                [self verifyPurchaseWithPaymentTransaction:transaction isTestServer:YES];

            }else if(status && [status isEqualToString:@"0"]){
                
                NSData *data = [NSJSONSerialization dataWithJSONObject:jsonResponse options:NSJSONWritingPrettyPrinted error:nil];
                
                [self handleActionWithType:KIAPPurchVerSuccess data:data];

            }
            SSLog(@"----验证结果 %@",jsonResponse);
        }

    }];
    
    
    // 验证成功与否都注销交易,否则会出现虚假凭证信息一直验证不通过,每次进程序都得输入苹果账号
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    NSArray *product = response.products;
    if([product count] <= 0){
        SSLog(@"--------------没有商品------------------");
        return;
    }
    
    SKProduct *p = nil;
    for(SKProduct *pro in product){
        if([pro.productIdentifier isEqualToString:self.purchID]){
            p = pro;
            break;
        }
    }
    
    SSLog(@"productID:%@", response.invalidProductIdentifiers);
    SSLog(@"产品数量:%lu",(unsigned long)[product count]);
    SSLog(@"%@",[p description]);
    SSLog(@"%@",[p localizedTitle]);
    SSLog(@"%@",[p localizedDescription]);
    SSLog(@"%@",[p price]);
    SSLog(@"%@",[p productIdentifier]);
    SSLog(@"发送请求");
    
    SKPayment *payment = [SKPayment paymentWithProduct:p];
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    SSLog(@"------------------错误-----------------:%@", error);
    
    [SVPShow showFailureWithMessage:[error localizedDescription]];
}

- (void)requestDidFinish:(SKRequest *)request{
    SSLog(@"------------反馈信息结束-----------------");
}

- (void) PurchasedTransaction: (SKPaymentTransaction *)transaction
{
    SSLog(@"-----PurchasedTransaction----");
    NSArray *transactions =[[NSArray alloc] initWithObjects:transaction, nil];
    [self paymentQueue:[SKPaymentQueue defaultQueue] updatedTransactions:transactions];
}

#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    
    for (SKPaymentTransaction *tran in transactions) {
        
        switch (tran.transactionState) {
           
            case SKPaymentTransactionStatePurchased:
                      
                SSLog(@"transactionIdentifier = %@",tran.transactionIdentifier);

                if (tran.originalTransaction) {
                                    
                    // 如果是自动续费的订单,originalTransaction会有内容
                    SSLog(@"自动续费的订单,originalTransaction = %@",tran.originalTransaction);
                    
                } else {
                    // 普通购买,以及第一次购买自动订阅
                    SSLog(@"普通购买,以及第一次购买自动订阅");
                }
                
                [self completeTransaction:tran];

                break;
            
            case SKPaymentTransactionStatePurchasing:
                
                SSLog(@"商品添加进列表");
                break;
            
            case SKPaymentTransactionStateRestored:
                SSLog(@"已经购买过商品");
                // 消耗型不支持恢复购买
                [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                
                //存入数组同步并发送通知
                [self userDefaultSave:@"YES"];
                
                [SVPShow disMiss];
                
                break;
            
            case SKPaymentTransactionStateFailed:
                [self failedTransaction:tran];
                break;
            
            default:
                break;
        }
    }
}


// 恢复购买
- (void)restore
{
    [SVPShow show];
    
    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {
    // 恢复失败
    SSLog(@"恢复失败");
    [SVPShow showFailureWithMessage:@"Restore Purchases fail"];
}

-(void)userDefaultSave:(NSString *)saveData
{
    //存入数组并同步
    [[NSUserDefaults standardUserDefaults] setObject:saveData forKey:@"buyResult"];
    [[NSUserDefaults standardUserDefaults] synchronize];
    // 发送通知
    [KNotification postNotificationName:@"ConnectViewController" object:nil userInfo:nil];
}

@end

具体使用

#define QH_LAZY(object, assignment) (object = object ?: assignment)
@property (nonatomic,strong) BuyVipPublic *iapManager;

///初始化
-(BuyVipPublic *)iapManager{
    
    return QH_LAZY(_iapManager, ({
        BuyVipPublic *iap = [[BuyVipPublic alloc]init];
        iap;
    }));
}


#pragma mark -- 订阅、内购
- (IBAction)subsrcribeAction:(UIButton *)sender {
    
    SSLog(@"订阅");
    // 这里的StoreID是开发者中心申请下来的商品id
    self.productID = StoreID;
    
    [SVPShow show];
    
    if ([SKPaymentQueue canMakePayments]) {
        // 如果允许应用内付费购买
        
        [self byAction:self.productID];
        
    } else {
        // 用户不允许应用内购买,弹出提示
        [self showMessage];
        [SVPShow disMiss];
    }
}

-(void)showMessage
{
    NSString *message = @"In app purchase is not allowed ,Please open Settings - > screen usage time - > content and privacy access restrictions - > purchased items in iTunes Store and App Store - > purchased items in App - > allow";
    
    UIAlertController *alt = [UIAlertController alertControllerWithTitle:@"Remind" message:message preferredStyle:UIAlertControllerStyleAlert];
    
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"ok" style:UIAlertActionStyleDefault handler:nil];

    [alt addAction:okAction];

    [self presentViewController:alt animated:YES completion:nil];
}

// 购买

-(void)byAction:(NSString *)plyID
{
    ///使用
    //点击购买按钮后,先从后台获取订单号(order_id),然后再发起内购.
    //plyID 为 产品ID,之前在创建的时候自定义的
    [self.iapManager startPurchWithID:plyID completeHandle:^(IAPPurchType type,NSData *data) {
                    
        SSLog(@"data --- %@",data);
                    
        if (type == kIAPPurchSuccess) {      // 购买成功
        
            if (data) {
                //返回数据
                SSLog(@"购买成功,正在验证凭证...");
                [SVPShow showSuccessWithMessage:@"Purchase succeeded, verifying credentials..."];
            }
                
        }else if(type == kIAPPurchCancle){
                    
            [SVPShow showInfoWithMessage:@"Cancel purchase"];
            
            [self userDefaultSave:@"YES"];
            
        }else if(type == kIAPPurchNotArrow){
                    
            [SVPShow showFailureWithMessage:@"In app purchase is not allowed"];
                                  
        }else if(type == kIAPPurchFailed){
                    
            [SVPShow showFailureWithMessage:@"Purchase failed"];
            
            [self userDefaultSave:@"NO"];
        }
                    
        if (type == KIAPPurchVerSuccess) {
                        
            SSLog(@"//订单第二次校验成功");
            ///购买凭证
            NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
            SSLog(@"%@",jsonDict);
            
            NSArray *latestArray = jsonDict[@"latest_receipt_info"];
            
            if (latestArray.count > 0)
            {
                // 过期时间
                NSInteger dateIn = [SafeString(latestArray[0][@"purchase_date_ms"]) integerValue];
                
                NSDate *date = [[NSDate alloc]initWithTimeIntervalSince1970:dateIn/1000];

                NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
                
                dateFormatter.dateFormat = @"yyyy-MM-dd";
                 
                NSString *dateStr = [dateFormatter stringFromDate:date];
                
                [KeychainTool addKeychainData:dateStr forKey:@"ExpirationTime"];
            }
            
            // 将购买结果存到本地
            [self userDefaultSave:@"YES"];
            
            [SVPShow disMiss];
                    
        }
    }];
}

-(void)userDefaultSave:(NSString *)saveData
{
    //存入数组并同步
    [[NSUserDefaults standardUserDefaults] setObject:saveData forKey:@"buyResult"];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

// 恢复订阅
- (IBAction)restoreAction:(UIButton *)sender {

    [self.iapManager restore];
    SSLog(@"恢复订阅");
}
  • 审核碰到的问题
  1. 大概意思是说app的名称是free,里面却有订阅,需要改一下名称

Guideline 2.3.7 - Performance - Accurate Metadata
Your app name to be displayed on the App Store include references to the price of your app or the service it provides, which is not considered a part of these metadata items
Next Steps
To resolve this issue, please remove any references to pricing from your app’s name, including any references to your app or service being free or discounted. If you would like to advertise changes to your app’s price, consider including this information in the app description. Changes to your app’s price can be made in the Pricing and Availability section of App Store Connect.
Resources
For information on how to revise your app name, please review Renaming a Project or App.
For resources on metadata best practices, you may want to review the App Store Product Page information available on the Apple Developer website.
For information on scheduling price tier changes, please review the Schedule price changes section of App Store Connect Developer Help.

  1. 大概意思是说协议相关的,需要在描述信息中也添加上相关协议

Guideline 3.1.2 - Business - Payments - Subscriptions
We noticed that your app did not meet all the terms and conditions for auto-renewing subscriptions, as specified in Schedule 2, section 3.8(b) of the Paid Applications agreement.
We were unable to find the following required item(s) in your app's metadata:
– A functional link to the Terms of Use (EULA)
Next Steps
To resolve this issue, please add this missing information. If the above information is present, please reply to this message in Resolution Center to provide details on where to locate it.
If you are using the standard Apple Terms of Use (EULA), you will need to include a link to the Terms of Use in your App Description. If you are using a custom EULA, add it in App Store Connect.
Resources

  • Learn more about offering auto-renewable subscriptions on the App Store.
  • Review the Paid Applications agreement (App Store Connect login required).

目前就这些,有问题请指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容