一、简单介绍
iOS 内购即(In-App Purchase)一共分为四种类型:(详细文档参考官网)
-
消耗型商品:只可使用一次的产品,使用之后即失效,必须再次购买。 示例:钓鱼 App 中的鱼食。
-
非消耗型商品:只需购买一次,不会过期或随着使用而减少的产品。 示例:游戏 App 的赛道。
-
自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。 示例:每月订阅提供流媒体服务的 App。
-
非续期订阅:允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。 示例:为期一年的已归档文章目录订阅。
只要在 iOS/iPadOS 设备上的 App 里购买非实物产品 (也就是虚拟产品,如:“金币、qq 币、鱼翅、电子书……”) ,都需要走内购流程,苹果从这里面抽走 30% 分成,实际结算是分成之前还需要先扣除交易税
二、内购前准备
APP内集成IAP代码之前需要先去开发账号的ITunes Connect进行以下三步操作(可参考该配置文档):
-
后台填写银行账户信息
-
配置商品信息,包括产品ID,产品价格等
这里需要注意的是产品 ID 具有唯一性,建议使用项目的 Bundle Identidier
作为前缀后面拼接自定义的唯一的商品名或者 ID(字母、数字),这里有个坑:一旦新建一个内购商品,它的产品ID将永远被占用,即使该商品已经被删除,已创建的内购商品除了产品 ID 之外的所有信息都可以修改,如果删除了一个内购商品,将无法再创建一个相同产品 ID 的商品,也意味着该产品 ID 永久失效,一般来说产品ID有特定的命名规则,如果命名规则下有某个产品 ID 永久失效,可能会导致整个产品ID命名规则都要修改,这里千万要注意!
- 配置用于测试IAP支付功能的沙箱账户。
三、内购流程
内购通用流程
-
用户向苹果服务器发起购买请求,收到购买完成的回调(购买完成后会把钱打给申请内购的银行卡内)
-
购买成功流程结束后, 向服务器发起验证凭证(app端自己也可以不依靠服务器自行验证)
-
自己的服务器工作分 4 步:
3.1 接收 iOS 端发过来的购买凭证。
3.2 判断凭证是否已经存在或验证过,然后存储该凭证。
3.3 将该凭证发送到苹果的服务器(区分沙盒环境还是正式环境)验证,并将验证结果返回给客户端(注意需要传密钥,要不会报错21003)
sandbox 开发环境: https://sandbox.itunes.apple.com/verifyReceipt
prod 生产环境: https://buy.itunes.apple.com/verifyReceipt 具体操作可以看这个 通过App Store验证收据。
3.4 修改用户相应的会员权限或发放虚拟物品。
简单来说就是将该购买凭证用 Base64 编码,然后 POST 给苹果的验证服务器,苹果将验证结果以 JSON 形式返回
恢复购买
内购有4种:消耗型项目,非消耗型,自动续期订阅,非续期订阅。 其中”非消耗型“和”自动续期订阅“需要提供恢复购买的功能,例如创建一个恢复按钮,不然审核很可能会被拒绝。
//调起苹果内购恢复接口[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
“消耗型项目”和“非续期订阅”苹果不会提供恢复的接口,不要调用上述方法去恢复,否则有可能被拒!!!
“非续期订阅”也是跨设备同步的,所以原则上来说也需要提供恢复购买的功能,但需要依靠app自建的账户体系恢复,不能用上述苹果提供的接口。
四、常见错误
获取不到商品信息:检查itc商品审核以及下线情况,检查设备知否支持内购
无法连接: 网络问题,切记沙盒环境下不可开vpn或者网络代理
五、注意事项
- 订阅产品需要验证订阅是否过期,自动续费在购买流程上,与普通购买没有区别,主要的区别:”除了第一次购买行为是用户主动触发的,后续续费都是 Apple 自动完成的,一般在要过期的前24小时开始,苹果会尝试扣费,扣费成功的话,在 App 下次启动的时候主动推送给 App“。
// 订阅特殊处理
if (transaction.originalTransaction) {
// 如果是自动续费的订单 originalTransaction 会有内容
} else {
// 普通购买,以及第一次购买自动订阅
}
-
沙盒账号在创建时就已经设置好了地区,中国的只能在中国的 App Store 测试,否则会提示不在此地区,请切回本地的应用商店
-
关于掉单的问题 答案:一定要在服务器校验完票据后,客户端收到服务器的反馈结果后再:
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
-
自动订阅时间在沙盒环境下时间会缩短
六、自己本地验证收据
有时候可能需要自己本地验证收据来调试一些问题,可以参考以下代码,本地调用https://sandbox.itunes.apple.com/verifyReceipt去验证,传参记得传递密码(即共享密钥),成功后会收到苹果返回的数据
然后对于连续订阅型:
1.第一次对账
遍历latest_receipt_info里的票据,如果transactionIdentifier相同,那么本次交易就是有效的,找到对应的票据后,其中最关键的是expires_date_ms这个时间戳,这个和expires_date字符串不相符, expires_date是GMT时间,少8个小时,而expires_date_ms转换后是正常的,因此不用把这个时间戳再加8小时.
2.之后的对账
由于苹果会在到期前充值,充值失败也会有回调通知,所以当数据库中的时间到期后,再去用之前存的凭据调用对账接口,
然后遍历latest_receipt_info根据product_id,也就是商品id,把连续订阅的票据筛选出来
现在筛选出来的这些就是所有的此商品的订阅票据了,而且顺序是按照时间排序的,新的在后面,如果不放心,可以自己根据里面的expires_date_ms(或者其他时间戳)再排序,然后创建订单,更新过期时间等等操作
-(void)verifyFinishedTransaction:(SKPaymentTransaction *)transaction{
NSString *str = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
NSString *environment = [self environmentForReceipt:str];
NSLog(@"------ 完成交易调用的方法completedTransaction 1----------%@", environment);
// 验证凭据,获取到苹果返回的交易凭据
// appStoreReceiptURL iOS 7.0 增加的,购买交易完成后,会将凭据存放在该地址
NSURL *url = [[NSBundle mainBundle] appStoreReceiptURL];
NSString *receipt = [[NSData dataWithContentsOfURL:url] jk_base64EncodedString];
[self log:[NSString stringWithFormat:@"sendString %@", receipt]];
/** 注意:这里可以不用自己去验证,直接调用自己服务器接口,让后台去APP Store 验证*/
NSURL *storeUrl = nil;
storeUrl = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
NSDictionary *requestContents = @{
@"receipt-data": receipt,
@"password":@"共享密钥",
@"exclude-old-transactions":@(YES)
};
NSError *error;
// 转换为 JSON 格式
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
options:0
error:&error];
NSString *verifyUrlString = @"https://sandbox.itunes.apple.com/verifyReceipt";
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[[NSURL alloc] initWithString:verifyUrlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];
[storeRequest setHTTPMethod:@"POST"];
[storeRequest setHTTPBody:requestData];
// 在后台对列中提交验证请求,并获得官方的验证JSON结果
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"链接失败");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self log:[NSString stringWithFormat:@"verifyFinishedTransaction error%@", error]];
} else {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (!jsonResponse) {
NSLog(@"验证失败");
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
[self log:[NSString stringWithFormat:@"verifyFinishedTransaction %@", jsonResponse]];
NSLog(@"验证成功");
//TODO:取这个json的数据去判断,道具是否下发
}
}];
[task resume];
}