• iOS:苹果内购实践


    iOS 苹果的内购

    一、介绍

        苹果规定,凡是虚拟的物品(例如:QQ音乐的乐币)进行交易时,都必须走苹果的内购通道,苹果要收取大约30%的抽成,所以不允许接入第三方的支付方式(微信、支付宝等),当然开发者可以设置后门,在审核时避开审核人员。这个是有风险的,一旦发现,app会被立即下架,还是老老实实接入内购吧。

     

    二、注意

    内购接入还是比较简单的,苹果提供了专门的框架<StoreKit/StoreKit.h>,只要按照它提供的api进行开发就行。然而,接入的过程还是有需要注意的地方,分别是:漏单处理、二次验证、移除交易、游客模式。

     

    漏单处理: 这个是一定会存在的,因为用户的一些误操作,造成的漏单基本无法避免,针对这种情况,最终的处理方式就是人工客服。当然,这个过程是可以优化的,开发者可以进行存储订单票据,server存储订单号,本地存储票据。如果用户启动app后,检测到用户上次付了款,但是需要的商品没有给到用户,此时可以自动进行验证并处理,验证通过,就将商品补给用户。

     

    二次验证:这个步骤必不可少,首先正式环境验证,如果验证通过,说明是线上环境,可以正常操作。如果验证不通过,说明是沙盒环境,需要在沙盒环境下再次验证,沙盒环境下的验证结果会有一个统一的弹框标识[Environment : Sandbox],只要内购没有上线,验证时都是沙盒环境弹框。 二次验证的这个过程可以避免在审核app时,因为没有验证通过直接被拒的风险。二次验证放在server端实现,更加安全。

    移除交易:用户再次交易时,如果上次的交易没有被移除,那么此次的交易会一直在队列中等候,无法被提交,所以一定要在上次交易完成时移除交易。

    游客模式:如果我们的app支持游客使用,那么这个内购就必须要求对游客进行开放,否则审核会被拒绝。

      

    三、使用

    1、实现代理

    @interface InAppPurchaseViewController ()<SKPaymentTransactionObserver,SKProductsRequestDelegate>
    @property (nonatomic, strong)NSMutableArray   *products;
    @property (nonatomic, strong)Product          *currentProduct;
    @property (nonatomic,   copy)NSString         *currentPayNo;
    @end
    View Code

    2、添加观察者

    -(void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        // 添加观察者
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    View Code

    3、移除观察者

    -(void)viewWillDisappear:(BOOL)animated
    {
        // 移除观察者
        [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
    }
    View Code

    4、移除没有关闭的交易并做漏单处理

    #pragma mark 先检查之前是否有未关闭的交易并做漏单处理
    -(void)checkNotCloseAndFinishedTransaction{
        
        NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
        for (SKPaymentTransaction* transaction in transactions) {
            if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }
        }
        
        NSArray *tickets = [InAppPurchaseTicketService fetchInAppPurchaseTicket];
        if (tickets.count > 0) {
            [MBProgressHUD showMessage:@"正在验证未处理的订单,请稍后"];
            self.conchChargeView.userInteractionEnabled = NO;
            for (InAppPurchaseTicket *ticket in tickets) {
                [self checkAppStorePayResultWithTikect:ticket];
            }
        }
    }
    View Code

    5、用户使用productId进行下单

    -(void)loadPayNoData{
        
        AppWeak(weakSelf, self);
        NSMutableDictionary *params = [NSMutableDictionary dictionary];
        params[@"productId"] = self.currentProduct.productId;
        [InAppPurchaseTicketService getIosOrderWithParams:params success:^(NSArray *items, BOOL isLocalData) {
            if (items.count >0 ) {
                InAppPurchaseInfo *inAppPurchaseInfo = items[0];
                weakSelf.currentPayNo = inAppPurchaseInfo.payNo;
                [weakSelf startPayForProduct:weakSelf.currentProduct.productId];
            }
        } failure:^(id errorInfo) {
            [self showErrorInfo:errorInfo];
        }];
    }
    View Code

    6、开始内购

    -(void)startPayForProduct:(NSString *)productID{
        
        if([SKPaymentQueue canMakePayments]){
            [MBProgressHUD showMessage:@"正在请求商品信息,请稍等..."];
            self.conchChargeView.userInteractionEnabled = NO;
            
            // productID就是你在创建购买项目时所填写的产品ID
            [self requestProductID:productID];
            
        }else{
            // NSLog(@"不允许程序内付费");
            UIAlertView *alertError = [[UIAlertView alloc] initWithTitle:@"温馨提示"
                                                                 message:@"请先开启应用内付费购买功能。"
                                                                delegate:nil
                                                       cancelButtonTitle:@"确定"
                                                       otherButtonTitles: nil];
            [alertError show];
        }
    }
    View Code

    7、请求所有的商品ID

    -(void)requestProductID:(NSString *)productID{
        
        // 1.拿到所有可卖商品的ID数组
        NSMutableArray *productIDArray = [NSMutableArray array];
        for (Product *product in self.products) {
            [productIDArray addObject:product.productId];
        }
        NSSet *sets = [[NSSet alloc] initWithArray:productIDArray];
        
        // 2.向苹果发送请求,请求所有可买的商品
        // 2.1.创建请求对象
        SKProductsRequest *sKProductsRequest = [[SKProductsRequest alloc]initWithProductIdentifiers:sets];
        
        // 2.2.设置代理(在代理方法里面获取所有的可卖的商品)
        sKProductsRequest.delegate = self;
        
        // 2.3.开始请求
        [sKProductsRequest start];
    }
    View Code

    8、获取苹果那边的内购监听

    //请求成功
    -(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
        
        NSArray *product = response.products;
        if([product count] == 0){
            [MBProgressHUD hideHUD];
            self.conchChargeView.userInteractionEnabled = YES;
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"没有商品" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
            [alertView show];
            return;
        }
        
        for (SKProduct *sKProduct in product) {
            
            NSLog(@"SKProduct 描述信息:%@", sKProduct.description);
            NSLog(@"localizedTitle 产品标题:%@", sKProduct.localizedTitle);
            NSLog(@"localizedDescription 产品描述信息:%@",sKProduct.localizedDescription);
            NSLog(@"price 价格:%@",sKProduct.price);
            NSLog(@"productIdentifier Product id:%@",sKProduct.productIdentifier);
            
            if([sKProduct.productIdentifier isEqualToString:self.currentProduct.productId]){
                [self buyProduct:sKProduct];
                break;
            }
        }
    }
    
    //请求失败
    - (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    
        [MBProgressHUD hideHUD];
        self.conchChargeView.userInteractionEnabled = YES;
        UIAlertView *alerView =  [[UIAlertView alloc] initWithTitle:@"提示" message:[error localizedDescription] delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil];
        [alerView show];
        
    }
    View Code

    9、创建票据,在队列中等候处理

    -(void)buyProduct:(SKProduct *)product{
        
        // 1.创建票据
        SKPayment *skpayment = [SKPayment paymentWithProduct:product];
        
        // 2.将票据加入到交易队列
        [[SKPaymentQueue defaultQueue] addPayment:skpayment];
    }
    View Code

    10、内购回调

    #pragma mark 4.实现观察者监听付钱的代理方法,只要交易发生变化就会走下面的方法
    -(void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
        
        /*
         SKPaymentTransactionStatePurchasing,    正在购买
         SKPaymentTransactionStatePurchased,     已经购买
         SKPaymentTransactionStateFailed,        购买失败
         SKPaymentTransactionStateRestored,      回复购买中
         SKPaymentTransactionStateDeferred       交易还在队列里面,但最终状态还没有决定
         */
    
        for (SKPaymentTransaction *transaction in transactions) {
            switch (transaction.transactionState) {
                case SKPaymentTransactionStatePurchasing:{
                    [MBProgressHUD hideHUD];
                    [MBProgressHUD showMessage:@"正在购买中,别走开..."];
                    NSLog(@"正在购买...");
                }
                    break;
                case SKPaymentTransactionStatePurchased:{
                    // 购买后告诉交易队列,把这个成功的交易移除掉
                    [queue finishTransaction:transaction];
                    [MBProgressHUD hideHUD];
                    [self SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:transaction];
                    NSLog(@"购买成功");
                }
                    break;
                case SKPaymentTransactionStateFailed:{
                    // 购买失败也要把这个交易移除掉
                    [queue finishTransaction:transaction];
                    [MBProgressHUD hideHUD];
                    self.conchChargeView.userInteractionEnabled = YES;
                    NSString *errorInfo = @"购买失败,请稍后重新购买";
                    if (transaction.error) {
                        NSString *reason = transaction.error.userInfo[NSLocalizedFailureReasonErrorKey];
                        if ([StringUtility isStringNotEmptyOrNil:reason]) {
                            errorInfo = reason;
                        }
                    }
                    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
                    [alertView show];
                    NSLog(@"购买失败");
                }
                    break;
                case SKPaymentTransactionStateRestored:{
                    // 回复购买中也要把这个交易移除掉
                    [queue finishTransaction:transaction];
                    [MBProgressHUD hideHUD];
                    self.conchChargeView.userInteractionEnabled = YES;
                    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"提示" message:@"重复购买了" delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
                    [alertView show];
                    NSLog(@"重复购买了");
                }
                    break;
                case SKPaymentTransactionStateDeferred:{
                    NSLog(@"交易还在队列里面,但最终状态还没有决定");
                }
                    break;
                default:
                    break;
            }
        }
    }
    View Code

    11、本地存储票据

    // 苹果内购支付成功
    - (void)SavePaymentTransactionpAfterbuyAppleStoreProductSucceed:(SKPaymentTransaction *)paymentTransactionp {
        
        // 传输的是BASE64编码的字符串
        // 验证凭据,获取到苹果返回的交易凭据
        // appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
        NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
        
        // 从沙盒中获取到购买凭据
        NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
        
        // 传输的是BASE64编码的字符串
        NSString *reciept = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
        
        // productIdentifier
        NSString *productIdentifier = paymentTransactionp.payment.productIdentifier;
        if ([productIdentifier length]>0) {
            
            //本地存储票据
            InAppPurchaseTicket *ticket = [[InAppPurchaseTicket alloc] init];
            ticket.payNo = self.currentPayNo;
            ticket.productId = self.currentProduct.productId;
            ticket.reciept = reciept;
            ticket.state = 1;
            [InAppPurchaseTicketService saveLocalTransaction:ticket];
            
            // 去验证是否真正的支付成功了
            [MBProgressHUD showMessage:@"购买成功,正在验证订单..."];
            [self checkAppStorePayResultWithTikect:ticket];
        }
    }
    View Code

    12、二次验证

    #pragma mark 服务端验证购买凭据
    - (void)checkAppStorePayResultWithTikect:(InAppPurchaseTicket *)tikect {
        
        /*
         生成订单参数,注意沙盒测试账号与线上正式苹果账号的验证途径不一样,要给后台标明
         
         注意:
         自己测试的时候使用的是沙盒购买(测试环境)
         App Store审核的时候也使用的是沙盒购买(测试环境)
         上线以后就不是用的沙盒购买了(正式环境)
         所以此时应该先验证正式环境,在验证测试环境
         
         正式环境验证成功,说明是线上用户在使用
         正式环境验证不成功返回21007,说明是自己测试或者审核人员在测试
    
         苹果AppStore线上的购买凭证地址是: https://buy.itunes.apple.com/verifyReceipt
         测试地址是:https://sandbox.itunes.apple.com/verifyReceipt
         
         */
    
        NSString *sandbox;
    #ifdef TEST
        sandbox = @"0"; //沙盒测试环境
    #else
        sandbox = @"1"; //线上正式环境
    #endif
        
        if (!tikect || !tikect.payNo || tikect.payNo.length==0 || !tikect.reciept || tikect.reciept.length==0) {
            return;
        }
    
        AppWeak(weakSelf, self);
        NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
        params[@"payno"]   = tikect.payNo;               //订单号
        params[@"sandbox"] = sandbox;                    //使用环境
        params[@"receipt"] = tikect.reciept;             //票据信息
        
        [InAppPurchaseTicketService doubleIosVerifyWithParams:params success:^(NSArray *items, BOOL isLocalData) {
            
            //隐藏loding
            [MBProgressHUD hideHUD];
            [MBProgressHUD showSuccess:@"恭喜你,购买成功" afterDelay:2.0];
            weakSelf.conchChargeView.userInteractionEnabled = YES;
            
            //清除本地当前对应订单票据
            InAppPurchaseInfo *info = items[0];
            [InAppPurchaseTicketService clearInAppPurchaseTicketWithPayNo:info.payNo];
            
            //刷新UI
            if (self.fromVCType == FromVCTypeCurrentInAppPurchaseVC) {
                [weakSelf loadConchData];
            }
            else{
                //回调并跳转页面
                if (weakSelf.chargeConchSuccsssBlock) {
                    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                        weakSelf.chargeConchSuccsssBlock(@(YES));
                        [weakSelf.navigationController popViewControllerAnimated:YES];
                    });
                }
            }
            
        } failure:^(id errorInfo) {
            [MBProgressHUD hideHUD];
            weakSelf.conchChargeView.userInteractionEnabled = YES;
            UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"验证失败" message:errorInfo delegate:nil cancelButtonTitle:@"关闭" otherButtonTitles:nil, nil];
            [alertView show];
        }];
    }
    View Code

    13、懒加载plist中的所有约定的商品productId

    -(NSMutableArray *)products{
        if (!_products) {
            _products = [NSMutableArray array];
            NSString *path = [[NSBundle mainBundle] pathForResource:@"Products" ofType:@"plist"];
            NSArray *items = [NSArray arrayWithContentsOfFile:path];
            if ([ArrayUtility isArrayNotEmptyOrNil:items]) {
                for (NSDictionary *dic in items) {
                    Product *product = [[Product alloc] initWithProperties:dic];
                    [_products addObject:product];
                }
            }
        }
        return _products;
    }
    View Code

    三、推荐

    最好把监听回调和验证写在单例中,这样app一启动时,就可以监听回调状态。

    四、结论

    这就是内购的全部流程了,我把主要的流程梳理了一下,具体的细节,开发人员自己去整理。与君共勉。。。。。

  • 相关阅读:
    vmware centos 共赏目录不显示
    小程序上拉刷新
    chrome json接口数据 插件
    滚动条滚动事件
    smarty循环item命名规范
    css实现梯形样式(含有border)
    设置行高并不能垂直居中
    图片翻转交互效果
    smarty文章字符截取
    css动画 transition
  • 原文地址:https://www.cnblogs.com/XYQ-208910/p/9237895.html
Copyright © 2020-2023  润新知