内购
内购的概念
IAP,即in-App Purchase,是一种智能移动终端应用程序付费的模式,在苹果(Apple)iOS、谷歌安卓(Google Android)、微软WindowsPhone等智能移动终端操作系统中都有相应的实现。-- 百度百科
-
In-App Purchase:
Offer users additional content and services through purchases made within your app.
账户请求流程
App store connect地址:https://appstoreconnect.apple.com/login
-
注册app (新建App)
- 填写协议、税务和银行业务
-
配置内购产品ID
根据产品类型来添加:
-
添加沙盒测试员
添加测试员就好了。此时注册的AppleID是一个虚拟的AppleID
- 沙盒账号是什么
iOS应用里面用到了苹果应用内付费(IAP)功能,在项目上线前一定要进行功能测试。测试肯定是需要的,何况这个跟money有关。。。开发完成了之后,如何进行测试呢?难道我测试个内购功能要自己掏钱?就算是公司掏钱,但是苹果要吃掉3成的啊,想想如果是99刀的商品,点下购买的时候心里都有点发慌。。。
苹果当然没这么坑了,测试内购,苹果提供了沙盒账号(也叫沙箱账号)的方式。这个沙箱账号其实是虚拟的AppleID,在开发者账号后台的iTune Connect上配置了之后就能使用沙盒账号测试内购,有了沙盒账号,就能体验一把土豪的感觉了,游戏钻石什么的随便充,反正不用我的钱。
App代码集成
- 获取商品信息
#pragma mark - 获取商品ID成功的代理方法
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
//返回的是SKProduct对象数组
//如果你上面请求的是多个,那么这里返回的也是多个
if (response) {
SKProduct *product = [response.products firstObject];
//查询成功,开始支付
[self startPaymentWithProduct:product];
}
}
- 拿到商品信息,创建支付对象
#pragma mark -- 拿到商品信息,创建支付对象
- (void)startPaymentWithProduct:(SKProduct *)product {
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.applicationUsername = @"myOrderID";
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
把交易添加到队列 (上面的addPayment:方法)
监听支付结果(paymentQueue:updatedTransactions:),如果支付成功,Apple会把支付成功的凭证(recipt)存到沙盒中
#pragma mark - 监听的代理方法
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
[transactions enumerateObjectsUsingBlock:^(SKPaymentTransaction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
SKPaymentTransaction *transation = obj;
switch (transation.transactionState) {
case SKPaymentTransactionStatePurchasing:
{
NSLog(@"购买中");
}
break;
case SKPaymentTransactionStatePurchased:
{
NSLog(@"交易完成");
//获取透传字段
NSString *orderNo = transation.payment.applicationUsername;
//transactionIdentifier:相当于Apple的订单号
NSString *transationId = transation.transactionIdentifier;
NSLog(@"orderNo = %@, 交易ID = %@", orderNo, transationId);
//从沙盒中获取交易凭证
NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
//转化成Base64字符串(用于校验)
/*
//其作用是将生成的Base64字符串按照64个字符长度进行等分换行。
NSDataBase64Encoding64CharacterLineLength = 1UL << 0,
//其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
NSDataBase64Encoding76CharacterLineLength = 1UL << 1,
//其作用是将生成的Base64字符串以回车结束。
NSDataBase64EncodingEndLineWithCarriageReturn = 1UL << 4,
//其作用是将生成的Base64字符串以换行结束。
NSDataBase64EncodingEndLineWithLineFeed = 1UL << 5,
*/
//NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//NSLog(@"%@",reciptString);
//传给后台做二次验证
[self checkReceipt:reciptString];
[[SKPaymentQueue defaultQueue] finishTransaction:transation];
}
break;
case SKPaymentTransactionStateFailed:
{
//localizedDescription可以作为提示信息(交易失败无法连接到 iTunes Store)
NSLog(@"交易失败%@", transation.error.localizedDescription);
[[SKPaymentQueue defaultQueue] finishTransaction:transation];
}
break;
case SKPaymentTransactionStateRestored:
{
NSLog(@"恢复购买完成");
//恢复完成(对应restoreCompletedTransactions)方法
}
break;
case SKPaymentTransactionStateDeferred:
{
NSLog(@"交易推迟, 等待外部操作");
//交易推迟
//官方解释是:交易已经加入队列,但是需要等待外部操作
//主要用于儿童模式,需要询问家长同意。这种情况下不能关闭订单(完成交易),否则这类充值将无法处理。
}
break;
default:
break;
}
}];
}
- 我们从沙盒中取到凭证(recipt),发送给我们自己后台进行二次验证,验证成功表示支付成功.
#pragma mark -- 从沙盒中取到凭证,发送给我们自己后台进行二次验证,验证成功表示支付成功
- (void)checkReceipt:(NSString *)receipt {
//NSLog(@"%@",receipt);
// 拼接请求数据
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\"}", receipt];
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//1.创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//2.根据会话对象创建task
NSURL *url = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
//3.创建可变的请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//4.修改请求方法为POST
request.HTTPMethod = @"POST";
//5.设置请求体
request.HTTPBody = bodyData;
//6.根据会话对象创建一个Task(发送请求)
/*
第一个参数:请求对象
第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
data:响应体信息(期望的数据)
response:响应头信息,主要是对服务器端的描述
error:错误信息,如果请求失败,则error有值
*/
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//8.解析数据
//NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@"%@",[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
//7.执行任务
[dataTask resume];
}
- 验证凭证时可能失败:凭证有可能丢失
- 凭证校验出错可以使用
SKReceiptRefreshRequest
刷新,阅读SKReceiptRefreshRequest官方文档。
- (void)refreshReceiptData
{
self.receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] init];
self.receiptRefreshRequest.delegate = self;
[self.receiptRefreshRequest start];
}
- 凭证校验地址:
开发环境: https://sandbox.itunes.apple.com/verifyReceipt
生产环境: https://buy.itunes.apple.com/verifyReceipt
- 凭证校验异常 code 参照码:
内购验证凭据返回结果状态码说明(status 状态)
21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
demo完整代码
#import "ViewController.h"
#import
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
UIButton *btn1 = [[UIButton alloc]initWithFrame:CGRectMake(100, 50, 100, 100)];
[btn1 setTitle:@"60游戏币" forState:UIControlStateNormal];
[btn1 addTarget:self action:@selector(clickBtn1) forControlEvents:UIControlEventTouchUpInside];
btn1.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn1];
// Do any additional setup after loading the view, typically from a nib.
UIButton *btn2 = [[UIButton alloc]initWithFrame:CGRectMake(100, 180, 100, 100)];
[btn2 setTitle:@"120游戏币" forState:UIControlStateNormal];
[btn2 addTarget:self action:@selector(clickBtn2) forControlEvents:UIControlEventTouchUpInside];
btn2.backgroundColor = [UIColor blueColor];
[self.view addSubview:btn2];
}
- (void)requestDidFinish:(SKRequest *)request {
NSLog(@"requestDidFinish");
//从沙盒中获取交易凭证
NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
[self checkReceipt:reciptString];
}
-(void)clickBtn1 {
NSSet *sets = [NSSet setWithObjects:@"dollar1", nil];
SKProductsRequest *productrequest = [[SKProductsRequest alloc] initWithProductIdentifiers:sets];
productrequest.delegate = self;
[productrequest start];
//首先我们要在 viewDidLoad 方法中添加监听对象
}
-(void)clickBtn2 {
NSSet *sets = [NSSet setWithObjects:@"dollar2", nil];
SKProductsRequest *productrequest = [[SKProductsRequest alloc] initWithProductIdentifiers:sets];
productrequest.delegate = self;
[productrequest start];
//首先我们要在 viewDidLoad 方法中添加监听对象
}
#pragma mark - 获取商品ID成功的代理方法
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
//返回的是SKProduct对象数组
//如果你上面请求的是多个,那么这里返回的也是多个
if (response) {
SKProduct *product = [response.products firstObject];
//查询成功,开始支付
[self startPaymentWithProduct:product];
}
}
#pragma mark -- 拿到商品信息,创建支付对象
- (void)startPaymentWithProduct:(SKProduct *)product {
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
payment.applicationUsername = @"myOrderID";
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
#pragma mark - 监听的代理方法
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {
[transactions enumerateObjectsUsingBlock:^(SKPaymentTransaction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
SKPaymentTransaction *transation = obj;
switch (transation.transactionState) {
case SKPaymentTransactionStatePurchasing:
{
NSLog(@"购买中");
}
break;
case SKPaymentTransactionStatePurchased:
{
NSLog(@"交易完成");
//获取透传字段
NSString *orderNo = transation.payment.applicationUsername;
//transactionIdentifier:相当于Apple的订单号
NSString *transationId = transation.transactionIdentifier;
NSLog(@"orderNo = %@, 交易ID = %@", orderNo, transationId);
//从沙盒中获取交易凭证
NSData *reciptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
//转化成Base64字符串(用于校验)
/*
//其作用是将生成的Base64字符串按照64个字符长度进行等分换行。
NSDataBase64Encoding64CharacterLineLength = 1UL << 0,
//其作用是将生成的Base64字符串按照76个字符长度进行等分换行。
NSDataBase64Encoding76CharacterLineLength = 1UL << 1,
//其作用是将生成的Base64字符串以回车结束。
NSDataBase64EncodingEndLineWithCarriageReturn = 1UL << 4,
//其作用是将生成的Base64字符串以换行结束。
NSDataBase64EncodingEndLineWithLineFeed = 1UL << 5,
*/
//NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSString *reciptString = [reciptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
//NSLog(@"%@",reciptString);
//传给后台做二次验证
[self checkReceipt:reciptString];
[[SKPaymentQueue defaultQueue] finishTransaction:transation];
}
break;
case SKPaymentTransactionStateFailed:
{
//localizedDescription可以作为提示信息(交易失败无法连接到 iTunes Store)
NSLog(@"交易失败%@", transation.error.localizedDescription);
[[SKPaymentQueue defaultQueue] finishTransaction:transation];
}
break;
case SKPaymentTransactionStateRestored:
{
NSLog(@"恢复购买完成");
//恢复完成(对应restoreCompletedTransactions)方法
}
break;
case SKPaymentTransactionStateDeferred:
{
NSLog(@"交易推迟, 等待外部操作");
//交易推迟
//官方解释是:交易已经加入队列,但是需要等待外部操作
//主要用于儿童模式,需要询问家长同意。这种情况下不能关闭订单(完成交易),否则这类充值将无法处理。
}
break;
default:
break;
}
}];
}
#pragma mark -- 从沙盒中取到凭证,发送给我们自己后台进行二次验证,验证成功表示支付成功
- (void)checkReceipt:(NSString *)receipt {
//NSLog(@"%@",receipt);
// 拼接请求数据
NSString *bodyString = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\"}", receipt];
NSData *bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];
//1.创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//2.根据会话对象创建task
NSURL *url = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];
//3.创建可变的请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//4.修改请求方法为POST
request.HTTPMethod = @"POST";
//5.设置请求体
request.HTTPBody = bodyData;
//6.根据会话对象创建一个Task(发送请求)
/*
第一个参数:请求对象
第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
data:响应体信息(期望的数据)
response:响应头信息,主要是对服务器端的描述
error:错误信息,如果请求失败,则error有值
*/
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//8.解析数据
//NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@"%@",[NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
}];
//7.执行任务
[dataTask resume];
}
@end
参考文章:
- iOS开发支付篇——内购(IAP)详解
- IOS内购(IAP)的那些事
- 【iOS】苹果IAP(内购)中沙盒账号使用注意事项