真·iOS内购的完整流程

作者: 严谨风 | 来源:发表于2016-07-19 17:55 被阅读8508次

    iOS的内购流程如下

    1. 通过产品ID获取产品信息列表
    2. 添加监听
    3. 把产品包装成SKPayment(支付)发送给苹果服务器
    4. 苹果服务器购买成功后会回调监听方法,根据苹果服务器返回信息判断是否购买成功。
    5. 购买失败或已经购买过该商品则注销交易。如果购买成功,此时可以向自家服务器发送购买成功的消息,并通过后台向苹果服务器发送验证,然后注销交易。
    一般而言,这就是iOS内购的基本过程,看似很简单,但是其实实际操作起来,还是比较麻烦的,因为要考虑到各种意外情况。

    下面讲一讲iOS内购的具体过程

    1.获取产品信息列表
    if ([SKPaymentQueue canMakePayments]) {
        NSSet *IDSet = [NSSet setWithArray:proID];
        SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:IDSet];
        productsRequest.delegate = self;
        [productsRequest start];
    } else {
        NSLog(@"用户禁止付费");
    }
    

    上面代码中的proID就是装有你在开发者后台创建内购产品时输入的产品ID的NSArray。
    delegate是指SKProductsRequestDelegate
    首先判断用户是否禁止付费,如果没有禁止付费,就想苹果服务器请求产品信息。
    请求的信息会在SKProductsRequestDelegate的方法中返回

    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
    {
        NSLog(@"%i", response.products.count);
        NSArray *myProducts = response.products;
        if (0 == myProducts.count) {
            NSLog(@"无法获取产品信息列表");
        } else {
            self.products = [myProducts sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
                SKProduct *pro1 = (SKProduct *)obj1;
                SKProduct *pro2 = (SKProduct *)obj2;
                return pro1.price.integerValue < pro2.price.integerValue ? NSOrderedAscending : NSOrderedDescending;
            }];;
            for (SKProduct *pro in myProducts) {
                NSLog(@"%@", [pro localizedTitle]);
                NSLog(@"%@", [pro localizedDescription]);
                NSLog(@"%@", [pro price]);
                NSLog(@"%@", [pro.priceLocale objectForKey:NSLocaleCurrencySymbol]);
                NSLog(@"%@", [pro.priceLocale objectForKey:NSLocaleCurrencyCode]);
                NSLog(@"%@", [pro productIdentifier]);
            }
        }
    }
    

    拿到产品信息以后可以进行排序处理,因为请求的时候发送的产品ID是装在一个NSSet中的,所以返回的产品信息也是乱序的,这里需要注意一下。

    2.内购监听

    拿到产品信息以后要设置监听,因为当你点击购买和购买后苹果服务器会通过监听方法通知应用。

    - (void)startObserver {
        if (!self.isObserver) {
            [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
            NSLog(@"开始监听 ------ 内购");
            self.isObserver = YES;
        }
    }
    
    - (void)stopObserver {
        if (self.isObserver) {
            [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
            NSLog(@"移除监听 ------ 内购");
            self.isObserver = NO;
        }
    }
    

    监听方法和移除监听的方法一起送上,isObserver是一个判断是否已经监听的BOOL数据。
    我的建议是将监听方法和移除监听的方法都在AppDelegate中执行。当App启动时(- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions)开始监听,当App被关闭时(- (void)applicationWillTerminate:(UIApplication *)application)移除监听。至于原因,后面会提到。

    3.实现监听方法

    - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
    {
        NSLog(@"调用了几次这个方法?");
        SKPaymentTransaction *transaction = transactions.lastObject;
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchased: {
                NSLog(@"购买完成,向自己的服务器验证 ---- %@", transaction.payment.applicationUsername);
                NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
                NSString *receipt = [data base64EncodedStringWithOptions:0];
                [self buySuccessWithReceipt:receipt transaction:transaction];
            }
                break;
            case SKPaymentTransactionStateFailed: {
                NSLog(@"交易失败");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }
                break;
            case SKPaymentTransactionStateRestored: {
                NSLog(@"已经购买过该商品");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }
                break;
            case SKPaymentTransactionStatePurchasing: {
                NSLog(@"商品添加进列表");
            }
                break;
            default: {
                NSLog(@"这是什么情况啊?");
            }
                break;
        }
    }
    

    finishTransaction:就是注销方法,如果不注销会出现报错和苹果服务器不停的通知监听方法等等情况。总之,记住要注销交易。
    有的同学可能会疑惑,transaction.payment.applicationUsername中的这个applicationUsername属性是干嘛的,先不要急,关于这个属性我们会在后面提到,现在先记住这个点就好。

    NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]]; 
    NSString *receipt = [data base64EncodedStringWithOptions:0];
    [self buySuccessWithReceipt:receipt transaction:transaction];
    

    关于这三句,要注意,receipt是刚才交易的清单,如果后台需要进行二次验证,需要用到这个数据。
    至于最后一句,则是购买成功后向自家的服务器发送的请求。

    发送内购请求

    完成上面的代码后,就可以进行发送内购请求的部分啦

    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
    payment.applicationUsername = [AppManager sharedInstance].userId.stringValue;
    [[SKPaymentQueue defaultQueue] addPayment:payment];
    

    内购请求很简单,就是用请求道的产品信息SKProduct创建一个“内购支付”SKPayment,然后添加进支付队列。

    以上就是iOS内购的全部核心代码。接下来要讲的,就是一些内购中可能出现的坑和如何跳过这些坑。

    细心的同学应该已经发现,在发送内购请求的代码部分,我们又一次见到了applicationUsername这个属性,并将用户的id赋值给了它,那么,这是用来干什么的呢?
    答案是:在某些极端情况下,可能出现在发送内购请求的用户和内购成功后通知自家后台的用户可能不是同一个用户的情况(真是奇葩的用户。。。。但是没办法,用户就是上帝嘛。。。)这种情况下,为payment绑定一个appUsername就可以让这次payment有一个固定的发起者,这样当这次payment在苹果后台支付成功后,我们就可以通过监听的回调,将这个发起者的唯一标识符上传给自家后台,使得这次购买能找到一个合适的主人。就算用户在购买的过程中切换账号或者退出,也能够让这次充值验证成功。

    既然说到了极端情况,那么我们不如更进一步,让情况更极端一点,那就是当购买请求发送后,直到向苹果后台购买成功的这段时间,如果程序崩溃了!或者程序被用户关闭了!怎么办?!
    这种情况下,我们的App自然也就无法进行监听的回调,自然也就无法把购买成功的消息发送给自家后台,用户也就拿不到自己的充值啦。情况很糟糕,但是!不用担心,我们有办法解决。
    还记得上边提到的注销交易的方法吗?没错就是:

    [[SKPaymentQueue defaultQueue] finishTransaction:transaction]
    

    当购买在苹果后台支付成功时,如果你的App没有调用这个方法,那么苹果就不会认为这次交易彻底成功,当你的App再次启动,并且设置了内购的监听时,监听方法- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions就会被调用,直到你调用了上面的方法,注销了这次交易,苹果才会认为这次交易彻底完成。
    利用这个特性,我们可以将完成购买后注销方法放到我们向自家后台发送交易成功后调用。
    讲到这里,关于内购的大坑我目前遇到的都已经解决啦,当然,你如果实际去操作,可能还会遇到各种各样的小坑,但是没关系,我相信你能够自己解决。。。所以,我就不说啦。
    准备爬坑吧!少年!

    相关文章

      网友评论

      • 90后的晨仔:你好,请问点击购买按钮之后,不走回调方法,而是在交易结束的时候再走的回调,我想在等待的过程中添加一个提示框应该添加到什么位置?
      • 问岳:开发时可以获取到产品列表,但是提交审核的时候,苹果反馈说获取不到产品列表,这是什么原因呢,跪求回复啊:joy:
        Exia_L:@问岳 哦哦,好吧,谢啦
        问岳:@Exia_L 没有找到原因:joy: 审核的时候,如果获取不到信息,我转为用产品id来购买了,虽然这个方法被弃用了,但可以忽悠通过审核:sob:
        Exia_L:请问你解决了吗
      • a浮生若梦a:你好,我现在有一个问题,苹果填写内购产品,最多6个吗?我在添加的时候,显示已储存,但是返回列表后还是没有添加的那条数据,请问这是苹果什么原因吗,
      • 李某lkb:对小白友好点,你会有更多的喜欢.
      • 28bb64fffadd:不知道是不是理解出错,感觉版主文章第三步实现监听,不是应该写在发送内购请求之后吗?不应该是请求道商品之后,用户发起请求购买,之后才是监听购买情况吗?为什么先监听,再购买呢?主要想知道updatedTransactions,didReceiveResponse那个先走呢?
      • fba08aef555c:请问购买vip选择哪种类型的项目呀
      • 码了个农啵:楼主知道怎么监听到苹果自动弹出来的提示框的事件么?比如我在最后购买成功弹出的那个框,点击“好的”,需要跳转到另一个界面,怎么监听??求解。谢谢
      • 铭初心:如果同一个设备的不同账号呢,比如说账号A,购买的计费点item1,然后掉单了。这个时候重启app登陆账号B,因为是不同账号不可能把钱给B,所以交易还不能修改成完成状态。这种情况下,B账号的计费点item1也是不可以购买的状态,不知道楼主是否遇到过类似问题
      • 远方的枫叶:第三步有问题,应该遍历transactions而不是取lastObject
        严谨风:是的,一直想着改了重写,总是忘了。
      • 叫我马小帅:有demo么,小白表示,代码在哪写都不知道....一点都不懂

        Aacmr:@严谨风 好呀好呀! 发个demo,注释写清楚点。:smile: 我是不是要求太多啦!哈哈哈。看了很多博客,还是不清楚,什么时候向苹果服务器验证,什么时候向自己后台验证? 反正就是细节不清楚。 先谢谢你啦!
        严谨风:@cmr 一直在忙,我这周争取把代码整理一下放出来。
        Aacmr:小白也如此表示!特别是向服务器验证流程。
      • Charles___:有demo么
      • mysteryemm:返回的结果是商品ID无效 这个问题楼主又遇到没
      • 一夕007:你这个用测试账号测试没有的呢
        严谨风:@呆萌一夕 测试啦,不过这个流程还有个漏洞,某些情况下会造成购买订单积压,一直没时间改,过年前我会修改出来新的。
      • Mario_ZJ:第3步和第4步中提到的服务器,指的是苹果的服务器吗?
        严谨风:@竹间溪流 已经改正
        Mario_ZJ:@严谨风 第3步和第4步没有明确说明是哪个服务器的
        严谨风:@竹间溪流 我应该有标注苹果服务器和自己服务器。这个文章在服务器验证方面还有些问题,最近出去旅游了一直没改,有时间了我会更新。
      • 超_iOS:购买成功并且验证成功后再注销交易吗?那如果后台没有验证成功呢?写到一半卡住了
        严谨风:@李二超 不需要,只要你还有未注销的交易,同时设置了内购监听,每次你进入应用苹果都会给你重新发一次receipt。然后你就可以给你的后台发送了。只要你把发送代码的位置写对,就只需要设置监听就可以啦。
        超_iOS:@严谨风 需要将receipt凭证数据本地持久化,并加入请求失败重发机制吗?还是有些许懵逼
        严谨风:@李二超 购买成功且后台验证成功后再注销。如果后台没有验证成功就不注销交易。这样当用户在苹果服务器付款成功,但是你的后台没有验证时,每当他进入应用,苹果服务器都会通知你交易信息,你就可以拿交易信息发给后台验证。只要你不注销交易,苹果就会一直通知你交易信息,可以防止我文章中提到的各种情况。

      本文标题:真·iOS内购的完整流程

      本文链接:https://www.haomeiwen.com/subject/bxwmjttx.html