最近公司的APP需要新增苹果内购产品,需要重构一下苹果内购功能。顺便写篇文章总结一下遇到的所有内购的坑。
一、嵌入流程介绍
- 1.1 简介
- 1.2 如何开放内购功能
- 1.3 商品的创建
- 1.4 商品类型
- 1.5 商品定价
- 1.6 产品ID
二、编程指南
- 2.1 常用类说明
- 2.2 流程代码
- 2.2.1 获取产品信息列表
- 2.2.2 购买商品
- 2.2.3 沙盒测试账号
- 2.2.4 校验支付凭证
- 2.3 漏单处理
IAP(In-App Purchase) 苹果应用内购买。通过在应用程序内部的购买为用户提供额外的内容和服务。属于StoreKit下的功能。
这里就不直译官方文档的内容了,简单总结一下就是购买应用程序内的虚拟产品,例如游戏金币、软件服务、订阅等,凡是苹果App内售卖的虚拟产品都可以走苹果内购买渠道。如果使用苹果内购购买的商品,苹果公司是会分成的(会抽取商品总价的30%)。
本文只介绍在选择使用苹果内购的情况下如果去嵌入内购功能,其他方式本文暂不讨论。
详细原理见官方文档,这里就不过多阐述了。
一个APP如果想要加入苹果内购,是需要在创建 AppId 的时候勾选 In-App Purchase 功能的(后期也可以修改)。
需要购买的商品需要在App Store Connect后台注册后方可被程序获取。
流程如下:
使用具有App管理功能的开发者账号登录App Store Connect --> 我的App --> 选择需要添加内购功能的App --> 功能 --> App内购项目 --> 点击右侧“加号“ 即可添加app内购项目了。
首先需要选择商品类型,然后参考名称、产品ID、价格、本地描述 、截图和审核备注等信息。具体每一步都有说明。
对于苹果内购的来说,用户每次购买的都是一个商品,商品和商品之间是有区别的。苹果提供了4中不同种类的商品模式,供开发者选择,也已经足够应付大部分应用的需求了。
只可使用一次的产品,使用之后即失效,必须再次购买。
示例:钓鱼App中的鱼食
只需购买一次,不会过期或随着使用而减少的产品。
示例:游戏 App 的赛道。
允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。
示例:每月订阅提供流媒体服务的 App。
允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。
示例:为期一年的已归档文章目录订阅。
当用户购买一个 自动续期订阅 或 非续期订阅 时,应用程序负责使它在所有用户的设备都可用,并使用户能够恢复过去的购买。
商品价格并不是随意定制的,是以美元为单位计算的,最低0.99美元,对应6.00元人民币(以前最低6.00人民币,现在也推出了1.00人民币的商品)。
有一个价格列表,开发者可根据公司产品定价选择最接近的产品价格。
美国(USD) | 中国大陆(CNY) |
---|---|
$0.99 | ¥6.00 |
$1.99 | ¥12.00 |
$3.99 | ¥25.00 |
… | … |
只有表中出现的价格可以选择,例如6元 12元,表中没有出现的价格是无法选择的(因为是以美元为单位的)。
每个产品都有一个产品ID号,用来在程序中对某个商品进行定位。一般建议使用 App 的 Bundle Identifier 后面再加一个产品名称。
例如建议 Bundle Identifier 为 com.XXX.XXX ,则产品ID为 com.XXX.XXX.productName(商品描述),
productName 可以是任意商品描述或缩写
恭喜你 到这里就成功创建了一个可供使用的内购商品了。通过创建的 产品ID 即可在程序中获取指定商品信息,和购买该商品了。
用来描述一个在 App Store Connect 里注册的内购商品的信息。
可以检索 App Store Connect 上注册的产品列表的对象。
对一个产品信息列表请求的响应对象。
包含订阅持续时间的对象。
订阅商品的折扣信息。
对应用程序内购买附加功能商品的一次支付行为的描述对象。
对应用程序内购买附加功能商品的一次支付行为的描述的可变对象。
一个处理对App Store购买行对象的队列(购买队列)
工程内加入 StoreKit 库,同时在需要使用支付的文件内加入头文件
#import
//商品ID数组
NSArray *productIdArray = @[IAP_PRODUCT_ID_1,
IAP_PRODUCT_ID_2,
IAP_PRODUCT_ID_3,
IAP_PRODUCT_ID_4,
IAP_PRODUCT_ID_5];
NSSet *productIdSet = [NSSet setWithArray:productIdArray];
//创建商品信息获取请求
SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdSet];
//设置代理
productsRequest.delegate = self;
//开始请求
[productsRequest start];
此处的 IAP_PRODUCT_ID_1 就是上文中提到的 产品ID
实现代理
@protocol SKProductsRequestDelegate
@required
// Sent immediately before -requestDidFinish:
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE(10_7, 3_0);
@end
通过协议方法返回的 SKProductsResponse 获取 SKProduct 的数组
//SKProductsResponse 的属性
// Array of SKProduct instances.
@property(nonatomic, readonly) NSArray *products NS_AVAILABLE(10_7, 3_0);
SKProduct 包含了可购买商品的详细信息(包含商品的本地描述,价格,商品ID等详细信息)
,这些信息可用于展示给用户,供用户选择购买。
//self 实现协议
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
实现协议 SKPaymentTransactionObserver 的必要方法
@protocol SKPaymentTransactionObserver
@required
// Sent when the transaction array has changed (additions or state changes). Client should check state of transactions and finish as appropriate.
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions NS_AVAILABLE(10_7, 3_0);
@optional
...
...
@end
根据用户选择的商品 SKProduct 创建支付对象 SKPayment
//创建支付对象 product为用户选择的商品的SKProduct对象
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
//设置购买数量
payment.quantity = quantity;
//可记录一个字符串,用于帮助苹果检测不规则支付活动
//payment.applicationUsername = [self encryptionString:userName];
//将支付加入支付队列
[[SKPaymentQueue defaultQueue]addPayment:payment];
注意: 每个商品的单次购买数量不能超过10个,所以请结合公司业务设计每个商品,(以前就被购买个数不足的问题坑过,由于文档说明的地方很隐蔽,所以第一次都没有发现)
//SKPayment
@property(nonatomic, readonly) NSInteger quantity;
The default value is 1, the minimum value is 1, and the maximum value is 10.
当将支付加入支付队列后,会出现提示用户输入 Apple ID 密码以完成支付的弹窗,待用户进一步操作
则 SKPaymentTransactionObserver 协议的回调会被触发
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions) {
switch (transaction.transactionState) {
// Call the appropriate custom method for the transaction state.
//支付中
case SKPaymentTransactionStatePurchasing:
break;
//支付失败
case SKPaymentTransactionStateFailed:
break;
//支付成功
case SKPaymentTransactionStatePurchased:
//结束本次交易
//[[SKPaymentQueue defaultQueue]finishTransaction:transaction]; //支付完成后调用(建议验证支付凭证有效后再调用)
break;
//支付被恢复
case SKPaymentTransactionStateRestored:
break;
//支付延迟(这种情况我还没有碰到过)
case SKPaymentTransactionStateDeferred:
break;
default:
break;
}
}
}
支付完成后需要调用 finishTransaction:
[[SKPaymentQueue defaultQueue]finishTransaction:transaction];
如果不调用,则每次启动app的时候都会有支付完成的回调上来。
这里建议验证交易凭证成功后再调用支付完成方法
虽然支付过程到这里就结束了,但是为了安全起见建议将支付凭证发送给服务器校验,获取校验结果后再结束支付并下发商品。下文中将介绍如何校验支付凭证。
支付的代码加好之后,要开始测试一下了,但总不可能用真钱去购买吧。 别担心, 苹果提供了一种沙盒测试账号,可以随意购买商品而不用真正消费。
沙盒账号的创建方式:
登录 App Store Connect --> 用户和访问 --> 测试员
这里就可以创建用于测试的沙盒账号了。
沙盒账号使用起来和一个正常的 AppleID 账号几乎没有区别。
可以直接登录在设备上,也可以在苹果内购支付的时候再填写账号密码。
获取支付凭证
//获取支付凭证
NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
NSString *receiptStr = [receiptData base64EncodedStringWithOptions:0];
此处拿到的receiptStr可以传给公司自己的服务器进行校验。
也可以自己做校验
将凭证做成json格式 key = @“receipt-data”
然后转成NSData
NSString *key = @"receipt-data";
NSDictionary *dic = [[NSDictionary alloc]initWithObjects:@[receiptString] forKeys:@[key]];
NSError *error;
NSData *postData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
将拿到的 postData 通过 POST 请求传给苹果服务器进行验证,这样即可获取凭证的校验结果。
下面是正式校验地址 和 沙盒测试校验地址
#if 1 //正式校验地址
static NSString *productionURL = @"https://buy.itunes.apple.com/verifyReceipt";
#else //沙盒测试校验地址
static NSString *productionURL = @"https://sandbox.itunes.apple.com/verifyReceipt";
#endif
由于服务器不知道凭证是否由沙盒账号购买生成的,可先进行正式地址校验,如果校验失败再进行沙盒地址校验。
凭证返回结果内也会有是否是沙盒的提示信息。
在App实际上线后,我们发现经常会有漏单,总结后大致分为两种原因
建议使用https请求,并且加入带有时间戳的验证字符串,交易凭证本身也要一同加密并拼接在验证字符串后面。虽然会使上传数据明显增加,但能提高安全性 所以还是很有必要的。
猜测可能由于网络等问题造成,或者app本身收到的支付完成回调比较滞后。
如果是网络原因造成的交易凭证验证请求失败,可在验证请求发送失败时将交易凭证存在本地,待稍后或者下次App启动再行验证。
以上就是大致的支付流程,希望对刚入坑的同学有些帮助。:)
作者:张文宇 向日葵远程控制软件/花生壳/蒲公英路由器 iOS高级软件工程师