IAP 全称:In-App Purchase,是指苹果 App Store 的应用内购买,是苹果为 App 内购买虚拟商品或服务提供的一套交易系统。
适用范围:在 App 内需要付费使用的产品功能或虚拟商品/服务,如游戏道具、电子书、音乐、视频、订阅会员、App的高级功能等需要使用 IAP,而在 App 内购买实体商品(如淘宝购买手机)或者不在 App 内使用的虚拟商品(如充话费)或服务(如滴滴叫车)则不适用于 IAP。
简而言之,苹果规定:适用范围内的虚拟商品或服务,必须使用 IAP 进行购买支付,不允许使用支付宝、微信支付等其它第三方支付方式(包括Apple Pay),也不允许以任何方式(包括跳出App、提示文案等)引导用户通过应用外部渠道购买。
APP内集成IAP代码之前需要先去开发账号的ITunes Connect进行以下三步操作:
1,后台填写银行账户信息;
2,配置商品信息,包括产品ID,产品价格等;
3,配置用于测试IAP支付功能的沙箱账户。
填写银行账户信息一般交由产品管理人员负责,开发者不需要关注,开发者需要关注的是第二步和第三步。
关于如何去 Itunes Connect 后台填写账户信息,本文不做讨论,可以参考:iOS内购一条龙—账户信息填写
IAP 是一套商品交易系统,而非简单的支付系统,每一个购买项目都需要在开发者后台的Itunes Connect后台为 App 创建一个对应的商品,提交给苹果审核通过后,购买项目才会生效。内购商品有四种类型:
配置商品信息需要注意产品ID和产品价格
1,产品 ID 具有唯一性,建议使用项目的 Bundle Identidier 作为前缀后面拼接自定义的唯一的商品名或者 ID(字母、数字),这里有个坑:一旦新建一个内购商品,它的产品ID将永远被占用,即使该商品已经被删除,已创建的内购商品除了产品 ID 之外的所有信息都可以修改,如果删除了一个内购商品,将无法再创建一个相同产品 ID 的商品,也意味着该产品 ID 永久失效。
2,在创建IAP项目的时候,需要设定价格,产品价格只能从苹果提供的价格等级去选择,这个价格等级是固定的,同一价格等级会对应各个国家的货币,比如等级1对应1美元、6元人民币,等级2对应2美元、12元人民币……最高等级87对应999.99美元、6498元人民币。另外可能是为了照顾某些货币区的开发者和用户,还有一些特殊的等级,比如备用等级A对应1美元、1元人民币,备用等级B对应1美元、3元人民币这样。除此之外,IAP项目不能定一个9.9元人民币这样不符合任何等级的价格。详细价格等级表可以看苹果的官方价格等级文档。苹果的价格等级表通常是不会调整的,但也不排除在某些货币汇率发生巨大变化的情况下,对该货币的定价进行调整,调整前苹果会发邮件通知开发者。
3,商品分成,App Store上的付费App和App内购,苹果与开发者默认是3/7分成。但实际上,在某些地区苹果与开发者分成之前需要先扣除交易税,开发者的实际分成不一定是70%。从2015年10月开始,苹果对中国地区的App Store购买扣除了2%的交易税,对于中国区帐号购买的IAP,开发者的实际分成在68%~69%之间,而且中国以外不同地区的交易税标准也存在差异。
新的内购产品上线之前,测试人员一般需要对内购产品进行测试,但是内购涉及到钱,所以苹果为内购测试提供了 沙箱测试账号 的功能,Apple Pay 推出之后 沙箱测试账号`也可以用于 Apple Pay 支付的测试,沙箱测试账号 简单理解就是:只能用于内购和 Apple Pay 测试功能的 Apple ID,它并不是真实的 Apple ID。
填写沙箱测试账号信息需要注意以下几点:
沙箱账号测试的使用:
流程图如下:
代码逻辑:
---------------------LCLInAppPurchase.h---------------------
#import
#import
static NSString *InAppPurchaseFailRefuse = @"该商品暂时无法购买,请稍后重试";
static NSString *InAppPurchaseFailRequest = @"操作失败,请稍后重试";
static NSString *InAppPurchaseFailBuy = @"购买失败,请稍后重试";
static NSString *InAppPurchaseFailResume = @"恢复失败,您未购买过该商品";
@interface LCLInAppPurchase : NSObject
- (id)init;
//发起内购
- (void)launchInAppPurchase:(NSString *)productId;
//恢复内购
- (void)resumeInAppPurchase:(NSString *)productId ;
-(void)removeObserver;
@end
---------------------LCLInAppPurchase.m---------------------
#import "LCLInAppPurchase.h"
@interface LCLInAppPurchase()
{
int _isResume;//是否恢复的购买
NSString *_productId;//内购中的产品ID
}
@end
@implementation LCLInAppPurchase
- (id)init{
self = [super init];
if (self) {
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
return self;
}
-(void)removeObserver{
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
- (void)launchInAppPurchase:(NSString *)productId{
_isResume = 0;
_productId = productId;
if([SKPaymentQueue canMakePayments]){
[self requestProductData:productId];
}else{
NSLog(@"不允许程序内付费");
}
}
- (void)resumeInAppPurchase:(NSString *)productId{
_isResume=1;
_productId = productId;
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
- (void)requestProductData:(NSString *)type{
NSLog(@"-------------请求对应的产品信息----------------");
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);
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(@"发送购买请求");
[[SKPaymentQueue defaultQueue] addPayment:payment];
// 可以把我们的自己订单和IAP的交易订单绑定,本地存储订单信息
}
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
NSLog(@"------------------错误-----------------:%@", error);
}
- (void)requestDidFinish:(SKRequest *)request{
NSLog(@"------------反馈信息结束-----------------");
}
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
NSString *resultA=@"";
SKPaymentTransaction *tran = [transaction lastObject];
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
NSLog(@"交易完成");
if (_isResume==0) {
NSData *receiptData = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
resultA=[self encode:receiptData.bytes length:receiptData.length];
NSLog(@"购买结果票据:%@",resultA);
// 收据发送到服务器
// 收据验证成功之后结束交易
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
// 删除保存的订单信息
}
else
{
NSString *resultB=[self encode:tran.transactionReceipt.bytes length:tran.transactionReceipt.length];
NSLog(@"恢复结果票据:%@",resultB);
// 收据发送到服务器
// 收据验证成功之后结束交易
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStatePurchasing:
NSLog(@"商品添加进列表");
break;
case SKPaymentTransactionStateRestored:
NSLog(@"已经购买过商品");
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
break;
case SKPaymentTransactionStateFailed:
NSLog(@"交易失败");
NSLog(@"%ld",tran.error.code);
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
[self errorReason:tran.error];
break;
default:
break;
}
}
- (void)errorReason:(NSError *)error{
NSString *detail;
if (error != nil) {
switch (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;
}
}
}
- (NSString *)encode:(const uint8_t *)input length:(NSInteger)length {
static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
NSMutableData *data = [NSMutableData dataWithLength:((length + 2) / 3) * 4];
uint8_t *output = (uint8_t *)data.mutableBytes;
for (NSInteger i = 0; i < length; i += 3) {
NSInteger value = 0;
for (NSInteger j = i; j < (i + 3); j++) {
value <<= 8;
if (j < length) {
value |= (0xFF & input[j]);
}
}
NSInteger index = (i / 3) * 4;
output[index + 0] = table[(value >> 18) & 0x3F];
output[index + 1] = table[(value >> 12) & 0x3F];
output[index + 2] = (i + 1) < length ? table[(value >> 6) & 0x3F] : '=';
output[index + 3] = (i + 2) < length ? table[(value >> 0) & 0x3F] : '=';
}
return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
}
- (void)dealloc{
[self removeObserver];
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
@end
自动续期订阅需要增加一个参数password,秘钥在APP内购买项目处创建。服务器提供URL用以接收苹果服务器通知,包含订阅状态变更或App内购买项目退款等。
IAP的支付流程:
1,发起支付
2,扣费成功
3,得到receipt(支付凭据)
4,去后台验证凭据获取商品交易状态
5,返回数据,验证成功前端刷新数据
漏单情况一:2到3环节出问题属于苹果的问题,目前没做处理。
漏单情况二:3到4的时候出问题,比如断网。此时前端会把支付凭据持久化存储下来,如果期间用户卸载APP此单在前端就真漏了,如果没有协助,下次重新打开app进入购买页会先判断有无未成功的支付,有就提示用户,用户选择找回,重走4,5流程。这一步看产品需求怎么做,可以让用户自主选择是否恢复未成功的支付也可以前端默默恢复就行。
漏单情况三:4到5的时候出问题。此时后台其实已经成功,只是前端没获取到数据,当漏单处理,下次进入的时候先刷新数据即可。
交易凭据receipt判重。一般来说验证支付凭据(receipt)是否有效放后台去做,如果后台不做判重,同一个凭据就可以无数次验证通过,因为苹果也不判重,这就会导致前端可以凭此取到的一个支付凭据可以去后台无数次做校验