1 IAP详细规则
IAP(In-App Purchase),是指苹果App Store的应用内购买,是苹果为APP内购买虚拟商品或服务提供的一套交易系统。
1.1适用范围
在APP内需要付费使用的产品功能或虚拟商品、服务。例如在斗鱼上充鱼翅、在视频APP上冲会员、在王者农药里买皮肤等....这些购买的商品或服务只能在APP内消费使用的适用IAP。反之,在京东或淘宝买东西、在滴滴上打车等...这些购买的商品或服务只能在APP外使用的情况是不适用IAP的。
1.2 IAP类型
IAP是一套商品交易系统,而非简单的支付系统。每一个购买项目都需要在App的itunes connect后台创建一个商品,提交给苹果审核,审核通过后,购买项目才会生效。
在创建IAP商品时,主要有四中类型:
- 1.2.1 Consumable products (该类型适用于可多次购买的消耗型项目,如游戏道具、虚拟币等。)
- 1.2.2 Non-consumable products (该类型适用于一次购买永久有效的项目,如电子书、游戏关卡等。
该类型项目支持跨设备同步和本地restore,比如说,用户在某个App中购买了一本书,可在所有相同Apple ID设备的App中免费获取这本书,而不要需要借助App本身的帐号体系,即使在App中删除了这本书,也可免费重新获取。) - 1.2.3 Auto-renewable subscriptions (该类型适用于自动续费的订阅项目,如Apple Music的按月订阅,用户购买后会每月自动续费,直到用户手动取消或者开发者下架IAP项目。
类似Non-consumable products,该类型也支持跨设备同步和本地restore机制。) -
1.2.4 Non-renewable subscriptions (该类型适用于固定有效期的非自动续费项目,如云音乐的会员和一些视频App的会员。没有跨设备同步和本地restore机制,用户可以多次购买。)
*********特别说明本文侧重讲解 消耗性项目 的流程*********
2 IAP设计开发要点
- 2.1 开发之前需要 先向AppStore提交资料,填写协议、税务和银行业务 具体流程可参考 http://www.jianshu.com/p/cb1c8b4ba2c0
- 2.2 然后需要先在itunes connect后台创建IAP商品,并按规范填写product id、商品名称、价格、截图等信息。
如果App当前版本支持新增的IAP项目,可不用发版直接提交IAP审核。如果需要App新功能配合,则需要和App版本一起提交。
《In-App Purchase Configuration Guide for iTunes Connect》详细介绍了IAP的创建和提交流程:https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html#//apple_ref/doc/uid/TP40013727-CH3-SW1
注意点:
2.2.1尽量不要删除已创建的IAP
已创建的IAP除了product id之外的所有信息都可以修改,如果删除了一个IAP,将无法再创建一个相同product id的IAP,也意味着该product id永久失效。而product id一般有特定的命名规则,用来标示App内的购买项目,如果命名规则下有某个product id永久失效,可能会导致整个product id命名规则都要修改,掉进坑里~
2.2.2 注意区分reference name和display name
eference name是给开发者自己看的,display name会在IAP支付流程的确认购买系统弹窗中展示给用户,而且不能随意修改(修改需要重新提交IAP审核),所以命名的时候要弄清楚。 - 2.3 IAP支付流程
IAP的支付模式分为客户端校验和服务端校验两种模式,客户端校验模式因为容易伪造支付凭证,安全性比较低,一般只有简单的单机APP才会使用,大部分APP都会采用服务端校验模式。
不同的IAP支付流程也会有一些小差异,主要是因为restore机制,下面是最常用的Consumable products和Non-renewable subscriptions类型的支付流程(服务端校验模式)
1.用户准备购买某个项目时,App客户端通过product id向苹果API请求支付信息
2.手机系统弹窗验证用户的Apple ID(可能需要输入Apple ID密码或验证touch ID)
3.Apple ID验证完成后,苹果API向App客户端返回用户将要支付的价格和货币单位
4.手机系统弹窗提示用户确认将要购买的内容和价格,用户点击确认购买
5.App客户端获得苹果API返回的支付成功通知以及支付凭据,向App服务端请求校验支付凭据
6.App服务端拿到客户端的支付凭据,再向苹果服务器请求校验支付凭据(避免一些越狱插件伪造客户端支付凭据)
7.App服务端校验支付凭据成功,通知App客户端
8.App收到支付凭据校验成功通知,代表用户付费成功,再处理后续业务逻辑
3 具体的代码实现
- 3.1 想要让用户购买我们的商品,首先我们得有个商品的展示界面。上面我们已经在itunes connect后台创建过我们的商品。创建过的商品都有一个唯一的产品标识 ProductID。通过ProductID我们可以拿到商品的具体信息。
3.1.1 商品的请求
- (void)fetchProductInformationForIds:(NSArray *)productIds{
//产品ID可以以Plist的方式放在本地APP,也可以放在本地的服务器上。不过最好是放在本地服务器上,当后的产品有变化时,就不用升级我们的APP了
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIds]];
request.delegate = self;
[request start];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIds]];
request.delegate = self;
[request start];
}
//苹果的服务器通过此方法向我们返回商品信息
#pragma mark - SKProductsRequestDelegate
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{`
if (response.products.count > 0) { //有效产品
self.availableProducts = [NSMutableArray arrayWithArray:response.products];
}
if (response.invalidProductIdentifiers.count > 0) {//无效产品标示
self.invalidProductIds = [NSMutableArray arrayWithArray:response.invalidProductIdentifiers];
}
self.status = IAPProductRequestResponse;
[[NSNotificationCenter defaultCenter] postNotificationName:IAPProductRequestNotification object:self];
}
//产品请求失败会调用此方法
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
self.status = IAPRequestFailed;
[[NSNotificationCenter defaultCenter] postNotificationName:IAPProductRequestNotification object:self];
NSLog(@"Product Request Status: %@",error.localizedDescription);
}
//然后就是产品的展示了
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if (self.itemArr.count > 0) {
SKProduct *product = self.itemArr[indexPath.row];
cell.textLabel.text = product.localizedTitle;
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@元",product.price];
}
}
3.2.2 商品的购买
- (void)buy:(SKProduct *)product{
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
// 记录购买者ID 后面取到传给服务器
NSString *userID = @"user";
payment.applicationUsername =userID;
payment.quantity = 1;//购买一次
//将商品添加到购买队列
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
//当用户点击完购买,此时付款队列中有了交易,会调用下面的方法。
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
for (SKPaymentTransaction * transaction in transactions) {
switch (transaction.transactionState) {
case SKPaymentTransactionStatePurchasing: //交易正在被添加到付款队列
NSLog(@"交易正在被添加到付款队列");
break;
case SKPaymentTransactionStateDeferred: //最终状态未确定
[self completeTransaction:transaction forStatus:IAPPurchaseFailed];
NSLog(@"最终状态未确定");
break;
case SKPaymentTransactionStatePurchased: //购买成功
NSLog(@"购买成功");
[self completeTransaction:transaction forStatus:IAPPurchaseSucceeded];
break;
case SKPaymentTransactionStateRestored: //已经购买过该商品
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];//消耗型不支持恢复
NSLog(@"已经购买过该商品");
break;
case SKPaymentTransactionStateFailed: //交易失败
NSLog(@"交易失败");
[self completeTransaction:transaction forStatus:IAPPurchaseFailed];
break;
default:
break;
}
}
}
//检查交易状态,做出相应操作
- (void)completeTransaction:(SKPaymentTransaction *)transaction forStatus:(NSInteger)status{
self.status = status;
NSString *detail = nil;
if (transaction.error != nil) {
switch (transaction.error.code) {
case SKErrorUnknown:
NSLog(@"SKErrorUnknown");
detail = @"未知的错误,请稍后重试。";
break;
case SKErrorClientInvalid:
NSLog(@"SKErrorClientInvalid");
detail = @"当前苹果账户无法购买商品(如有疑问,可以询问苹果客服)";
break;
case SKErrorPaymentCancelled:
NSLog(@"SKErrorPaymentCancelled");
detail = @"订单已取消";
break;
case SKErrorPaymentInvalid:
NSLog(@"SKErrorPaymentInvalid");
detail = @"订单无效(如有疑问,可以询问苹果客服)";
break;
case SKErrorPaymentNotAllowed:
NSLog(@"SKErrorPaymentNotAllowed");
detail = @"当前苹果设备无法购买商品(如有疑问,可以询问苹果客服)";
break;
case SKErrorStoreProductNotAvailable:
NSLog(@"SKErrorStoreProductNotAvailable");
detail = @"当前商品不可用";
break;
default:
NSLog(@"No Match Found for error");
detail = @"未知错误";
break;
}
NSLog(@"detail == %@",transaction.error.localizedDescription);
}
if (status == IAPPurchaseSucceeded) {
/***************此处有坑,需特别注意*****************/
//由于网络问题等种种原因,即使用户已经付款成功,客户端也可能一时半会收不到苹果API的支付成功通知,也无法主动向苹果API请求查询支付状态,只能被动等待通知。
//因此有些情况下,客户端会延迟收到支付成功的通知(可能是过了几分钟,也有可能是下次打开App的时候),针对这种情况,需要做好两件事:
//1. 客户端本地保存所有支付结果未确认的交易信息,并设置一个监听进程,在收到支付成功的信息后,继续处理这笔交易的后续流程。极端情况下,用户在交易结果未确认的情况下删除App,保存在App本地数据库中的交易信息也会丢失,因此,更好的方案是把交易信息存到iOS系统的keychain里面
//2.当本地存在支付结果未确认的交易信息时,在交互上提示用户可能需要等待支付结果,避免用户重复付款
//获得交易 凭证
NSURL *receipturl = [[NSBundle mainBundle] appStoreReceiptURL];
NSData *receiptData = [NSData dataWithContentsOfURL:receipturl];
NSLog(@"receiptData == %@",receiptData);
//获取购买者标识
NSLog(@"payment.applicationUsername == %@",transaction.payment.applicationUsername);
//本次交易的唯一标识符
MyLog(@"transaction.transactionIdentifier == %@",transaction.transactionIdentifier);
//将 1、交易凭证 2、购买者标识 3、购买的产品类型(ProductID)4、本次交易的唯一标识符 保存到本地(keyChain)
// [LGJKeyChainTools setObject:receiptStr forService:@"user" account:transaction.transactionIdentifier ];
//向本地服务器发送请求 传送 1、交易凭证 2、购买者标识 3、购买的产品类型(ProductID)4、本次交易的唯一标识符,本地服务器向苹果服务器发送验证验证交易凭证请求。如果凭证有效,则发放产品,删除保存到本地的相应信息,如果无效则提示相应的错误提示,也要删除保存到本地的相应信息。
}
//无论什么状态都应该将本次交易做结束处理,否则下次购买会出现问题。
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
//发送通知
[[NSNotificationCenter defaultCenter] postNotificationName:IAPPurchaseNotification object:self];
//在客户端向服务端轮询结果时,为了避免用户在支付结果页面等待过久,交互层面上可以先结束支付流程(经过一定超时时间),同时提示用户需要等待支付结果,避免用户重复付款
}