应用内支付IAP全部流程
本文Demo地址 ,内附详细文档。
下边的文档仅供开发参考,建议到最新文档 阅读。
(__Deprecated!)
今天, 我们的App审核又因为支付方式被拒绝了, 所以选择了增加应用内支付这种方式。
大致的业务逻辑是这样的。
1.向服务器请求商品订单号码以及ituns配置的商品ID
2.发起IAP购买请求
3.购买流程结束后, 向服务器发起验证凭证以及支付结果的请求
4.验证流程结束完成购买流程。
流程
- 首先打开itunesconnect看一下有没有配置用户账户等信息, 点击 『协议, 税务与银行卡业务』进去配置就可以。
-
创建App或者选择已经有的App, 点击进入详情之后, 点击App内购项目这一选项
屏幕快照 2015-09-24 上午11.53.06.png
进入, 点击右上角的CreateNew按钮进行创建。 如果已经有的话可以点击进详情编辑内容。
消耗型项目
对于消耗型 App 内购买项目,用户每次下载时都必须进行购买。一次性服务通常属于消耗型项目,例如钓鱼 App 中的鱼饵。
非消耗型项目
对于非消耗型 App 内购买项目,用户仅需要购买一次。不会过期或随使用而减少的服务通常为非消耗型项目,例如游戏 App 的新跑道。
自动续订订阅
通过自动续订订阅,用户可以购买指定时间期限内的更新和动态内容。除非用户取消选择,否则订阅(例如杂志订阅等)会自动续订。
免费订阅
免费订阅是开发人员在“报刊杂志”中推广其内容的绝佳方式。用户注册免费订阅后,此订阅内容在与该用户 Apple ID 相关联的所有设备上可用。免费订阅不会过期,并且仅能在位于“报刊杂志”类别中的 App 中提供。
非续订订阅
非续订订阅允许有时限性的营销服务。对于 App 内购买项目中的限时访问内容,就需使用非续订订阅。例如,导航 App 中语音导航功能的一周订阅,或者年度订阅已存档的视频或音频的在线目录。
通常我们都选择消耗形项目, 如果你要按月付费之类的就要选择非续订订阅之类的喽。
选择消耗形项目, 然后继续,输入商品的名称, 产品的ID(自定义), 在下边添加语言的地方添加一下商品的描述信息, 然后上传一张商品界面的截图(这里可以随便, 影响不是很大)保存就可以了。
屏幕快照 2015-09-24 上午11.57.16.png这是我们创建的结果, 右边的准备好去提交的提示是用于你提交App审核的时候同时提交一下内购项目的审核, 在应用程序的附加信息里边。必须要提交- - 。 这里的产品ID就是后期用于请求商品的商品ID。这时候就可以开始测试了。
代码
- 导入 StoreKit.Framework 这个框架
并在VC中
#import <StoreKit/StoreKit.h>
- 实现
SKPaymentTransactionObserver, SKProductsRequestDelegate
这两个代理
3.在ViewDidLoad中添加购买监听
// 添加购买监听
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
- 开始模拟购买, 要先检测是否允许内购。
// 检测是否允许内购
if([SKPaymentQueue canMakePayments]){
[self requestProductData:productID];
}else{
NSLog(@"不允许程序内付费");
}
//请求商品
- (void)requestProductData:(NSString *)type{
NSLog(@"请求商品");
[SVProgressHUD showWithStatus:@"正在请求商品信息" maskType:SVProgressHUDMaskTypeGradient];
NSArray *product = [[NSArray alloc] initWithObjects:type, nil];
NSSet *nsset = [NSSet setWithArray:product];
// 请求动作
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}
//收到产品返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
NSLog(@"收到了请求反馈");
NSArray *product = response.products;
if([product count] == 0){
NSLog(@"没有这个商品");
return;
}
NSLog(@"productID:%@", response.invalidProductIdentifiers);
NSLog(@"产品付费数量:%ld",[product count]);
SKProduct *p = nil;
// 所有的商品, 遍历招到我们的商品
for (SKProduct *pro in product) {
NSLog(@"%@", [pro description]);
NSLog(@"%@", [pro localizedTitle]);
NSLog(@"%@", [pro localizedDescription]);
NSLog(@"%@", [pro price]);
NSLog(@"%@", [pro productIdentifier]);
if([pro.productIdentifier isEqualToString:productID]) {
p = pro;
}
}
SKPayment * payment = [SKPayment paymentWithProduct:p];
NSLog(@"发送购买请求");
[SVProgressHUD showWithStatus:@"正在发送购买请求" maskType:SVProgressHUDMaskTypeGradient];
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
NSLog(@"商品信息请求错误:%@", error);
[SVProgressHUD showErrorWithStatus:[error localizedDescription]];
}
- (void)requestDidFinish:(SKRequest *)request {
NSLog(@"请求结束");
[SVProgressHUD dismiss];
}
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction {
for(SKPaymentTransaction *tran in transaction){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
NSLog(@"交易完成");
[SVProgressHUD showSuccessWithStatus:@"交易完成"];
// 结束掉请求
[self completeTransaction:tran];
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表");
[SVProgressHUD showWithStatus:@"正在请求付费信息" maskType:SVProgressHUDMaskTypeGradient];
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已经购买过商品");
[SVProgressHUD showErrorWithStatus:@"已经购买过商品"];
break;
case SKPaymentTransactionStateFailed:
NSLog(@"交易失败");
[SVProgressHUD showErrorWithStatus:@"交易失败, 请重试"];
break;
default:
[SVProgressHUD dismiss];
break;
}
}
}
一定要在监听到购买结果的时候结束掉这个交易, 不然StoreKit不能即时的做记录
在交易结束的时候, 要向服务器做凭证的验证, 因为要链接苹果的服务器, 所以这里的网络请求可能稍慢一点。 所以建议做一下本地化。
//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction{
NSLog(@"交易结束");
[SVProgressHUD dismiss];
NSString * productIdentifier = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];
NSString * receipt = [[productIdentifier dataUsingEncoding:NSUTF8StringEncoding] base64EncodedString];
if ([productIdentifier length] > 0) {
// 向自己的服务器验证购买凭证
// https://sandbox.itunes.apple.com/verifyReceipt
// receipt-data
}
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}
最后在dealloc中移除监听
- (void)dealloc{
// 移除监听
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
自己了解一下流程很重要, 顺便附上服务器端的验证代码
<?php
//服务器二次验证代码
function getReceiptData($receipt, $isSandbox = false)
{
if ($isSandbox) {
$endpoint = 'https://sandbox.itunes.apple.com/verifyReceipt';
}
else {
$endpoint = 'https://buy.itunes.apple.com/verifyReceipt';
}
$postData = json_encode(
array('receipt-data' => $receipt)
);
$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0); //这两行一定要加,不加会报SSL 错误
curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
$response = curl_exec($ch);
$errno = curl_errno($ch);
$errmsg = curl_error($ch);
curl_close($ch);
//判断时候出错,抛出异常
if ($errno != 0) {
throw new Exception($errmsg, $errno);
}
$data = json_decode($response);
//判断返回的数据是否是对象
if (!is_object($data)) {
throw new Exception('Invalid response data');
}
//判断购买时候成功
if (!isset($data->status) || $data->status != 0) {
throw new Exception('Invalid receipt');
}
//返回产品的信息
return array(
'quantity' => $data->receipt->quantity,
'product_id' => $data->receipt->product_id,
'transaction_id' => $data->receipt->transaction_id,
'purchase_date' => $data->receipt->purchase_date,
'app_item_id' => $data->receipt->app_item_id,
'bid' => $data->receipt->bid,
'bvrs' => $data->receipt->bvrs
);
}
//获取 App 发送过来的数据,设置时候是沙盒状态
$receipt = $_GET['data'];
$isSandbox = true;
//开始执行验证
try
{
$info = getReceiptData($receipt, $isSandbox);
// 通过product_id 来判断是下载哪个资源
switch($info['product_id']){
case 'com.application.xxxxx.xxxx':
Header("Location:xxxx.zip");
break;
}
}
//捕获异常
catch(Exception $e)
{
echo 'Message: ' .$e->getMessage();
}
?>
在我们公司的测试服务器中,我们会连接苹果的测试服务器https://sandbox.itunes.apple.com/verifyReceipt
验证。
在我们部署在线上的正式服务器中,我们会连接苹果的正式服务器https://buy.itunes.apple.com/verifyReceipt
验证。
我们提交给苹果审核的是正式版,我们以为苹果审核时,我们应该连接苹果的线上验证服务器来验证购买凭证。结果我理解错了,苹果在审核App时,只会在sandbox环境购买,其产生的购买凭证,也只能连接苹果的测试验证服务器。但是审核的app又是连接的我们的线上服务器。所以我们这边的服务器无法验证通过IAP购买,造成我们app的又一次审核被拒。
解决方法是判断苹果正式验证服务器的返回code,如果是21007,则再一次连接测试服务器进行验证即可。苹果的这一篇文档上有对返回的code的详细说明 (引自 唐巧, 上边有文章地址).
测试
需要添加沙箱的测试帐号, 在itunsconnect中选择用户与职能,然后添加测试帐号, 这个帐号可以用于测试购买。 另外, 在测试的时候, 可能比较慢, 所以我的项目中加入了不可交互的HUD进行提示, 避免用户进行多次商品的添加与购买。
源码
工程的bundleID必须同你创建内购的App的BID相同呦
CopyRight@Dylan 2015-9-24.