iOS内购(IAP)模块总结(含漏单处理)
我也是第一次接触iOS内购,其实一直以来我接触的公司都是不愿意接入内购的,毕竟要给苹果分成,所以也就没有学习IAP流程编写。最近公司考虑到虚拟会员防审核期间可以把iOS内购作为备用方案,所以也就把iOS内购流程做个梳理并根据咋们公司的业务特点写了非续期订购类型的IAP代码。
刚开始接触内购,也是千头万绪,所以现在网上找了几篇IAP文章看了下,结合自己的整理,先总结了IAP大致流程。
前期准备工作:
1. 阅读苹果的《App内购买项目》文档
2.去App Store connect税务里面签署内购协议
登陆Apple开发官网,选择connect:
image.png
image.png
image.png
3.配置内购项目,“App -->功能-->APP内购买项目”
具体配置可以参考创建及发布说明
,下面是我的演示实例:
选择“我的APP”
然后选择你要接入内购的APP,点击进入,选择功能-APP内购买项目
image.png
然后点击+新增一个内购商品,弹出下面对话框:
image.png
这里说明一下,内购产品分为4种,分别消耗、非消耗、续期、非续期,具体解释整理如下:
苹果的内购分以下四类商品:
1、消耗型项目
只可使用一次的产品,使用之后即失效,必须再次购买。
示例:钓鱼 App 中的鱼食。
2、非消耗型项目
只需购买一次,不会过期或随着使用而减少的产品。
示例:游戏 App 的赛道。
3、自动续期订阅
允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
示例:每月订阅提供流媒体服务的 App。
4、非续期订阅
允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
示例:为期一年的已归档文章目录订阅。
4.Xcode capablities 打开IAP开关
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完结!