iOS内购(IAP)模块总结(含漏单处理)

我也是第一次接触iOS内购,其实一直以来我接触的公司都是不愿意接入内购的,毕竟要给苹果分成,所以也就没有学习IAP流程编写。最近公司考虑到虚拟会员防审核期间可以把iOS内购作为备用方案,所以也就把iOS内购流程做个梳理并根据咋们公司的业务特点写了非续期订购类型的IAP代码。
刚开始接触内购,也是千头万绪,所以现在网上找了几篇IAP文章看了下,结合自己的整理,先总结了IAP大致流程。

前期准备工作:

1. 阅读苹果的《App内购买项目》文档
2.去App Store connect税务里面签署内购协议
登陆Apple开发官网,选择connect:

image.png

image.png

image.png

image.png

3.配置内购项目,“App -->功能-->APP内购买项目”
具体配置可以参考创建及发布说明
,下面是我的演示实例:
选择“我的APP”

image.png

然后选择你要接入内购的APP,点击进入,选择功能-APP内购买项目
image.png

然后点击+新增一个内购商品,弹出下面对话框:
image.png

这里说明一下,内购产品分为4种,分别消耗、非消耗、续期、非续期,具体解释整理如下:

苹果的内购分以下四类商品:
1、消耗型项目
只可使用一次的产品,使用之后即失效,必须再次购买。
示例:钓鱼 App 中的鱼食。
2、非消耗型项目
只需购买一次,不会过期或随着使用而减少的产品。
示例:游戏 App 的赛道。
3、自动续期订阅
允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
示例:每月订阅提供流媒体服务的 App。
4、非续期订阅
允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
示例:为期一年的已归档文章目录订阅。

4.Xcode capablities 打开IAP开关

image.png

IAP内购流程图:

iOS内购流程图

代码实现:

1.判断用户是否具备支付权限

- (BOOL)canMakePurchase {
    if ([SKPaymentQueue canMakePayments]) {
        return YES;
    }else {
        //用户未开启内购,弹框提示
        UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"内购未开启" message:@"进入“【设置】 - 开启【屏幕使用时间】功能。然后在【屏幕使用时间】选项中选择【内容和隐私访问限制】,选择【iTunes Store 与 App store 购买】- 选择【App内购项目】- 选择“允许”,将该功能开启" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles: nil];
        [alertView show];
        //内购结束
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseNotAllow);
        }
        return NO;
    }
}

2.获取Apple内购商品列表

- (void)fetchIAPProducts:(void(^)(void))block {
    if (!self.productResp) {
        //如果没有商品信息,异步接口获取
        Weakify(self);
        [self queryByPuoductId:IAProductID productInfoReuslts:^(SKProductsResponse * _Nonnull resp) {
            if (resp == nil) {
                if (weakself.IAPurchaseResult) {
                    weakself.IAPurchaseResult(IAPurchaseFailed);
                }
                return ;
            }
            if (resp.products.count == 0) {
                if (weakself.IAPurchaseResult) {
                    weakself.IAPurchaseResult(IAPurchaseNoProducts);
                }
                return ;
            }
            if (block) {
                block();
            }
        }];
    }else {
        if (block) {
            block();
        }
    }
}

//通过产品ID查询商品信息
- (void)queryByPuoductId:(NSString *)productId
      productInfoReuslts:(void(^)(SKProductsResponse *resp))block {
    self.fetchProductBlock = block;
    NSSet *set = [NSSet setWithObject:productId];
    SKProductsRequest * request = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
    request.delegate = self;
    [request start];
}

#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(nonnull SKProductsRequest *)request didReceiveResponse:(nonnull SKProductsResponse *)response {
    self.productResp = response;
    if (self.fetchProductBlock) {
        self.fetchProductBlock(response);
    }
}

- (void)requestDidFinish:(SKRequest *)request {
    //do nothting
}

- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
    self.productResp = nil;
    if (self.fetchProductBlock) {
        self.fetchProductBlock(nil);
    }
}

3.创建苹果内购支付

- (void)createInPurchasePay {
    SKProduct *product = nil;
    for (SKProduct *prod in self.productResp.products) {
        if ([prod.productIdentifier isEqualToString:IAProductID]) {
            product = prod;
            break;
        }
    }
    if (!product) {
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseNoProducts);
        }
        return;
    }
    SKMutablePayment * payment = [SKMutablePayment paymentWithProduct:product];
    //透传业务订单
    payment.applicationUsername = self.orderId;
    [[SKPaymentQueue defaultQueue] addPayment:payment];
}

#pragma mark - 监听用户支付交易变化
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions {
    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState)
        {
            case SKPaymentTransactionStatePurchased://交易完成
                [self verifyReceiptByTransaction:transaction];
                break;
            case SKPaymentTransactionStateFailed://交易失败
                [self failTransation:transaction];
                break;
            case SKPaymentTransactionStateRestored://已经购买过该商品
                [self verifyReceiptByTransaction:transaction];
                break;
            case SKPaymentTransactionStatePurchasing:      //商品添加进列表
                //解决applicationUsername支付一半kill进程后为nil的问题
                [self saveCurrTransationBindedOrderId];
                break;
            default:
                break;
        }
    }
}

//交易失败
- (void)failTransation:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
    IAPurchaseStatus status = IAPurchaseFailed;
    if (transaction.error.code != SKErrorPaymentCancelled) {
        status = IAPurchaseCancel;
    }
    if (self.IAPurchaseResult) {
        self.IAPurchaseResult(status);
    }
}

//持久化当前正在交易绑定的业务订单
- (void)saveCurrTransationBindedOrderId {
    NSLog(@"商品添加进列表");
    if (self.orderId) {
        NSDictionary *orderdic = @{@"productId":IAProductID,
                                   @"orderId": self.orderId
                                   };
        [[NSUserDefaults standardUserDefaults] setObject:orderdic forKey:@"persient.IAP.order"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}

- (NSString *)bindedOrderId {
    NSDictionary *dic = [[NSUserDefaults standardUserDefaults] objectForKey:@"persient.IAP.order"];
    if (dic) {
        return dic[@"orderId"];
    }else {
        return nil;
    }
}

4.验证票据

- (void)verifyReceiptByTransaction:(SKPaymentTransaction *)transaction {
    NSString *receiptString = [self iapReceipt];
    if (!receiptString) {
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
        }
        return;
    }
    NSError *error;
    NSDictionary *requestContents = @{@"receipt-data": receiptString};
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
    
    if (!requestData) { // 交易凭证为空验证失败
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
        }
        return;
    }
    //向苹果服务器验证支付凭据真实性
    [self verifyRequestData:requestData testSandbox:NO transaction:transaction];
}

//获取内购票据
- (NSString *)iapReceipt {
    NSString *receiptString = nil;
    NSURL *rereceiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:rereceiptURL];
    receiptString = [receipt base64EncodedStringWithOptions:0];
    return receiptString;
}

- (void)verifyRequestData:(NSData *)postData
                      testSandbox:(BOOL)test
              transaction:(SKPaymentTransaction *)transaction
{
    NSString *url = @"https://buy.itunes.apple.com/verifyReceipt";
    if (test) {
        url = @"https://sandbox.itunes.apple.com/verifyReceipt";
    }
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
    request.HTTPBody = postData;
    static NSString *requestMethod = @"POST";
    request.HTTPMethod = requestMethod;
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [NSURLConnection sendAsynchronousRequest:request queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                               if (connectionError) {
                                   // 无法连接服务器,购买校验失败
                                   if (self.IAPurchaseResult) {
                                       self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
                                   }
                               } else {
                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
                                   if (!jsonResponse) {
                                       // 苹果服务器校验数据返回为空校验失败
                                       if (self.IAPurchaseResult) {
                                           self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
                                       }
                                       [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                       return ;
                                   }
                                   //先验证正式服务器,如果正式服务器返回21007再去苹果测试服务器验证,沙盒测试环境苹果用的是测试服务器
                                   NSString *status = [NSString stringWithFormat:@"%@", jsonResponse[@"status"]];
                                   if (status && [status isEqualToString:@"21007"]) {
                                       [self verifyRequestData:postData testSandbox:YES transaction:transaction];
                                   } else if (status && [status isEqualToString:@"0"]) {
                                       //订单校验成功,给蜗蜗生活订单会员充值
                                       NSString *orderId = transaction.payment.applicationUsername;
                                       if (!orderId) {
                                           orderId = [self bindedOrderId];
                                       }
                                       [self chargeWowoVipOrderId:orderId];
                                       [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                   }else {
                                       // 苹果服务器校验数据返回为空校验失败
                                       if (self.IAPurchaseResult) {
                                           self.IAPurchaseResult(IAPurchaseVerifyReciptFailed);
                                       }
                                       [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
                                   }
                               }
                           }];
    
}

5.最后一步,给会员订单冲会员

- (void)chargeWowoVipOrderId:(NSString *)orderId {
    if (!orderId.length) {
        if (self.IAPurchaseResult) {
            self.IAPurchaseResult(IAPurchaseVerifyReciptSuccess);
        }
        return;
    }
    //开始调用充值接口...
    if (self.IAPurchaseResult) {
        self.IAPurchaseResult(IAPurchaseSuccess);
    }
}

关于漏单的问题

由于用户可能在支付过程中中途网络不佳,或者程序突然crash的情况下,有可能用户支付成功了,但是验证票据等后续操作没有走完,也就没有给用户实际充值的情况。
这种情况下,我们可以将payment监听放到APP启动里启用全局监听,那么下次APP启动后,会重新走支付交易事务变化的监听,就可以继续完成票据验证以及给用户充值的操作。
我们就可以将内购类设计成单例模式,在init时候即添加全局通知,然后实现SKPaymentTransactionObserver委托:

//单例模式
+ (instancetype)shared {
    dispatch_once(&onceToken, ^{
        sharedInstance = [[InAppPurchaseManager alloc] init];
    });
    return sharedInstance;
}

- (instancetype)init {
    if (self = [super init]) {
        //添加支付交易的全局Observer
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
    }
    return self;
}

然后我们再appDlegate的启动方法里调用如下方法:

/**
 内购准备环境(在appDelegateAPP每次启动时调用)
 */
- (void)parpareIAP {
    [self queryByPuoductId:IAProductID productInfoReuslts:^(SKProductsResponse * _Nonnull resp) {
        
    }];
}

这样appDelegate会自动添加了全局事务观察了。

关于applicationUsername为nil的问题

我们将我们自己的业务订单ID跟Apple的支付事务是通过applicationUsername这个属性关联的。但是苹果并不帮我们将这个属性做了持久化操作,只在内存中
复现场景:当用户杀掉APP后,重新打开APP后,上次的订单ID透传给applicationUsername=nil,也就是订单ID丢失了,那么后续给用户充值的重要入参订单ID没有了,也就无法充值。

解决方案:

这个解决方案,我是参考了这篇作者提供的思路:贝聊 IAP 实战之订单绑定,粗放性订单持久化。

思路

上述思路实现:
粗放型持久化思路

//持久化当前正在交易绑定的业务订单
- (void)saveCurrTransationBindedOrderId {
    NSLog(@"商品添加进列表");
    if (self.orderId) {
        NSDictionary *orderdic = @{@"productId":IAProductID,
                                   @"orderId": self.orderId
                                   };
        [[NSUserDefaults standardUserDefaults] setObject:orderdic forKey:@"persient.IAP.order"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}
获取持久化订单

以上,内购IAP完结!

关于网上很多IAP文章做了大量支付票据本地存储操作,我觉得完全没有必要,其实苹果内购的每次交易事务都做了本地持久化了,所有的上次没有完成的事务,都可以在下次交易变化中一次性再提交上去。客户端没必要再存了。所以,希望大家不要被一些本地存储事务票据等给误导了。

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

推荐阅读更多精彩内容