iOS开发 IAP支付总结
一、IAP介绍
1.1、简介
这里先把官方文档给大家
App 内购买项目配置流程
内购:只要在iPhone App上购买的不是实物产品(也就是虚拟产品如qq币、鱼翅、电子书......) 都需要走内购流程,苹果这里面抽走30%
苹果规定,凡是在App内提供的服务需要付费时,必须使用IAP,比如说游戏的金币,道具等;而在App外提供的服务需要付费时,可以使用其他的支付方式,比如支付宝SDK、微信SDK等。说的更通俗一点,如果付费购买的商品是虚拟商品,比如游戏中的道具,并不是现实中存在的,那么必须使用IAP;如果付费购买的商品是真实产品,比如在淘宝中买了件衣服,是实实在在存在的,那么没有必要使用IAP。因此,在使用IAP之前,首先要确认是否一定要使用IAP,如果不使用IAP也可以,那么尽量不要用IAP,因为IAP流程、使用复杂度相比支付宝SDK、微信SDK来说,要复杂很多。
1.2、内购流程
1.1.1 填写协议,税务和银行业务
1、登录https://appstoreconnect.apple.com,选择进入App Store Connect。
2、进入“协议、税务和银行业务”
3、内购用的是付费应用程序,先签署《付费应用程序协议》,同意后状态变更为“用户信息待处理”,等待审核。
4、状态更改完毕后,点击“开始设置税务、银行业务和联系信息”。
a.添加银行账户,按照要求填写相关内容即可。
b.选择报税表,并填写。(我是可爱的中国公民,在美国有没有商业活动,所以我填的是否。)
然后继续填写报税表,按照填写要求填写就行了(要是英文阅读有点困难,那就双击网页,应该会有翻译成中文的功能;没有的话,那就词典。。。你懂得,哈哈哈), 我是个人开发者账户相对公司开发者账户填的会少一点,不过没关系。都是一些基本信息。
c.填写联系信息,一共5个。高级管理、财务、技术、法务、营销。
5、上面的税务表填完了之后,点击“我的APP”,进入到项目APP的信息页,点击功能,在弹出的页面点击App内购买项目后面的+。
创建完成之后 填写内购买项目信息
信息填写完成了点击右上角的 “存储”,然后点击左边 “App 内购买项目”。出现“元数据丢失”说明里面信息没填写完整,在点进去填写。直到显示“准备提交”。
6、添加沙箱测试人员
7、我们需要在工程里配置好证书,测试证书是必须的因为我们内购需要连接到苹果的App Store的,需要正式的测试证书才能测试,同时把下图工程中的这一配置打开
二、IAP代码部分
我这里就直接上代码记录了
2.1、大体代码流程
typedefenum: NSUInteger {
EPaymentTransactionStateNoPaymentPermission,//没有Payment权限
EPaymentTransactionStateAddPaymentFailed,//addPayment失败
EPaymentTransactionStatePurchasing,//正在购买
EPaymentTransactionStatePurchased,//购买完成(销毁交易)
EPaymentTransactionStateFailed,//购买失败(销毁交易)
EPaymentTransactionStateCancel,//用户取消
EPaymentTransactionStateRestored,//恢复购买(销毁交易)
EPaymentTransactionStateDeferred,//最终状态未确定
} EPaymentTransactionState;
// 这个大家要熟悉哦~
步骤一:App Store请求内购项
注意:此步骤建议在开始创建购买订单前完成,这样可以减少购买时查询订单的时间
1、判断用户是否具备支付权限
//是否允许内购
if ([SKPaymentQueue canMakePayments]) {
[self getRequestAppleProduct];
}else{
[self removeLoadingHandle];
[self removeIAPObserverHandle];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showFailedHUD:TZKeyWindow text:@"请打开Apple支付"];
});
}
2、创建一个商品查询的请求,productIdentifiers指需要查询的“产品ID”的数组
- (void)getRequestAppleProduct
{
NSLog(@"---------请求对应的产品信息------------");
[MBProgressHUDManager showHUD:TZKeyWindow text:@"等待响应..."];
NSArray *product = [[NSArray alloc] initWithObjects:self.productID, nil];
NSSet *nsset = [NSSet setWithArray:product];
SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
request.delegate = self;
[request start];
}
查询的结果将通过SKProductsRequestDelegate得到查询的结果
获取商品的查询结果
#pragma mark - SKProductsRequestDelegate
//接收到产品的返回信息,然后用返回的商品信息进行发起购买请求
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response NS_AVAILABLE_IOS(3_0){
NSArray *product = response.products;
//没有产品
if([product count] == 0){
[self removeLoadingHandle];
[self removeIAPObserverHandle];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showFailedHUD:TZKeyWindow text:@"网络开小差了,请稍后重试"];
});
return;
}
SKProduct *requestProduct = nil;
for (SKProduct *pro in product) {
CYLOG(@"描述信息-%@", [pro description]);
CYLOG(@"产品标题-%@", [pro localizedTitle]);
CYLOG(@"产品描述信息-%@", [pro localizedDescription]);
CYLOG(@"价格-%@", [pro price]);
CYLOG(@"Product id-%@", [pro productIdentifier]);
CYLOG(@"位置-%@", pro.priceLocale.localeIdentifier);
// 确保订单的正确性
if([pro.productIdentifier isEqualToString:self.productID]){
requestProduct = pro;
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
payment.applicationUsername = self.orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
break;
}
}
}``
步骤二:开始构建购买请求
SKPayment * payment = [SKPayment paymentWithProduct:product];
[[SKPaymentQueue defaultQueue] addPayment:payment];
步骤三:添加支付交易的Observer
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
注意在适当的时候移除Observer
[[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
可以通过遵循SKPaymentTransactionObserver协议来监听整个交易的过程
交易状态发生改变时,包括状态的改变,交易的结束
//监听购买结果
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions{
NSLog(@"==监听购买结果==");
[self addLoadingHandle];
[self addIAPObserverHandle];
for(SKPaymentTransaction *tran in transactions){
switch (tran.transactionState) {
case SKPaymentTransactionStatePurchased:
{
NSLog(@"交易完成");
[self didPurchaseTransaction:tran queue:queue];
}
break;
case SKPaymentTransactionStatePurchasing:{
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showHUD:TZKeyWindow text:@"正在购买..."];
});
}
break;
case SKPaymentTransactionStateRestored:{
CYLOG(@"已经购买过商品");
//消耗型不用写
// [self removeLoadingHandle];
// [self removeIAPObserverHandle];
// [[SKPaymentQueue defaultQueue] finishTransaction:tran];
}
break;
case SKPaymentTransactionStateFailed:{
NSLog(@"交易失败");
[self removeLoadingHandle];
[self removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:tran];
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showFailedHUD:TZKeyWindow text:@"交易失败"];
});
}
break;
case SKPaymentTransactionStateDeferred:{
NSLog(@"还在队列里");
dispatch_async(dispatch_get_main_queue(), ^{
[MBProgressHUDManager showHUD:TZKeyWindow text:@"正在购买..."];
});
}
break;
default:
break;
}
}
}
步骤四:校验凭证
我这里直接把我这边相关的思路以及代码提供大家参考了:
有问题可以随时联系我、我后面会讲一下我所遇到过的坑以及解决方案。
#pragma mark Transaction State
我这里使用后台校验凭证、更加安全
- (void)didPurchaseTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
{
CYLOG(@"transaction purchased with product ---%@", transaction.payment.productIdentifier);
CYLOG(@"transaction ID ---%@", transaction.transactionIdentifier);
if(transaction.payment.productIdentifier != nil){
if([self.orderId length] && !self.ischecking){
//如果这个参数存在,则肯定是通过主动发起购买请求引起的
//在支付成功后,将parameters中的预订单号存起来,并与苹果的订单号绑定起来,并存储到keychain中
if([self.orderId length] && transaction.transactionIdentifier){
[SAMKeychain setPassword:self.orderId forService:TZServiceKey account:transaction.transactionIdentifier];
}
}
}
WS(weakSelf);
// appStoreReceiptURL iOS7.0增加的,购买交易完成后,会将凭据存放在该地址
NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
// 从沙盒中获取到购买凭据
NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
NSString *encodeStr = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
NSString *payload = [NSString stringWithFormat:@"{\"receipt-data\" : \"%@\"}", encodeStr];
NSData *payloadData = [payload dataUsingEncoding:NSUTF8StringEncoding];
NSString *applicationUsername = transaction.payment.applicationUsername;
NSString *productId = transaction.payment.productIdentifier;
NSString *transactionId = transaction.transactionIdentifier;
//发送POST请求,对购买凭据进行验证
//测试验证地址:https://sandbox.itunes.apple.com/verifyReceipt
//正式验证地址:https://buy.itunes.apple.com/verifyReceipt
if(applicationUsername.length == 0){
NSString *savedOrderNumber = [SAMKeychain passwordForService:TZServiceKey account:transactionId];
if ([savedOrderNumber length]) {
applicationUsername = savedOrderNumber;//获取到订单号
}
}
if ([applicationUsername length] && [encodeStr length] && [productId length] && [transactionId length]) {
[[HTTPAPIManager manager] reqeustPayAppleReceiptWithOutTradeNo:applicationUsername receiptData:encodeStr useSandbox:TZPAYSandbox productId:productId transactionId:transactionId success:^(NSURLSessionDataTask * _Nullable task, id _Nullable responseObject, NSDictionary * _Nullable inforDict) {
_errTimes = 0;
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:(2.0)
target:weakSelf
selector:@selector(handleTimer:)
userInfo:@{@"transaction":transaction}
repeats:YES];
[[NSRunLoop mainRunLoop]addTimer:weakSelf.timer forMode:NSDefaultRunLoopMode];
[weakSelf.timer setFireDate:[NSDate date]];
} failure:^(NSURLSessionDataTask * _Nullable task, YWHTTPError * _Nullable error, NSDictionary * _Nullable responseDict) {
_errTimes = 0;
weakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:(2.0)
target:weakSelf
selector:@selector(handleTimer:)
userInfo:@{@"transaction":transaction}
repeats:YES];
[[NSRunLoop mainRunLoop]addTimer:weakSelf.timer forMode:NSDefaultRunLoopMode];
[weakSelf.timer setFireDate:[NSDate date]];
}];
if (!_ischecking) {
[MBProgressHUDManager showHUD:TZKeyWindow text:@"正在确认支付..."];
}
} else {
[[HTTPAPIManager manager] reqeustAppleFailRecordWithOutTradeNo:applicationUsername receiptData:encodeStr productId:productId transactionId:transactionId success:^(NSURLSessionDataTask * _Nullable task, id _Nullable responseObject, NSDictionary * _Nullable inforDict) {
_ischecking = NO;
[MBProgressHUDManager hiddenHUD:TZKeyWindow];
[weakSelf destroyTimer];
[weakSelf removeLoadingHandle];
[weakSelf removeIAPObserverHandle];
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[[NSNotificationCenter defaultCenter] postNotificationName:TZBuyVipSuccessNotification object:nil userInfo:nil];
} failure:^(NSURLSessionDataTask * _Nullable task, YWHTTPError * _Nullable error, NSDictionary * _Nullable responseDict) {
[MBProgressHUDManager hiddenHUD:TZKeyWindow];
}];
}
}
三、重点总结
1.获取内购列表(从App内读取或从自己服务器读取)
2.App Store请求可用的内购列表
3.向用户展示内购列表
4.用户选择了内购列表,再发个购买请求,收到购买完成的回调(购买完成
后会把钱打给申请内购的银行卡内)
5.购买流程结束后, 向服务器发起验证凭证以及支付结果的请求
6.自己的服务器将支付结果信息返回给前端并发放虚拟产品
7.服务端的工作比较简单,分4步:
7.1.接收ios端发过来的购买凭证。
7.2.判断凭证是否已经存在或验证过,然后存储该凭证。
7.3.将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。
7.4.如果需要,修改用户相应的会员权限。
7.5.考虑到网络异常情况,服务器的验证应该是一个可恢复的队列,如果网络失败了,应该进行重试。
简单来说就是将该购买凭证用Base64编码,然后POST给苹果的验证服务
器,苹果将验证结果以JSON形式返回。
四、总结坑
SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:requestProduct];
payment.applicationUsername = self.orderId;
[[SKPaymentQueue defaultQueue] addPayment:payment];
//我这里刚才只是对订单号的存储、并且把订单号存在了applicationUsername
上面的处理中、上线后我这里第一个测出了真实支付中的漏单情况、订单号返回的nil,由于对payment.applicationUsername的极度信任、造成自己不得不紧急解决下这个问题发版、不过我事先作的还是有一些功课的、做了埋点、及时的定位到了问题、并且让后台进行了手动补单。相信大家看到这里的时候就不会只是简单的这样做了。
五、解决方案
1、针对掉单的问题、网上的资料讨论的太多了我这里简单说下我的方案吧
我这里进行了对订单号、transactionId、进行对应的钥匙串存储。这样可以解决大部分的异常场景、基本没什么漏单了、并且我对掉单每次启动进行了掉单查询、还有就是再次购买页面也会有相应的提示、让用户自己继续处理、便可以重新提交订单了。方便太多啦
/// 掉单处理
- (void)checkIAPHandle{
_ischecking = YES;
_isShowErrorM = NO;
[self addIAPObserverHandle];
}
- (void)checkTransactionHandle
{
NSArray *transactions = [SKPaymentQueue defaultQueue].transactions;
if (transactions && [transactions isKindOfClass:[NSArray class]] && [transactions count]) {
for (SKPaymentTransaction *transaction in transactions) {
if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
NSString *title = @"您有一笔会员订单未完成,请继续处理";
NYAlertView *alertView = [[NYAlertView alloc] initWithTitle:@"发现未完成订单"
message:title
cancelButtonTitle:nil
otherButtonTitles:@"继续处理", nil];
[alertView setClickButtonBlock:^(NYAlertView * _Nonnull alert, NSInteger index) {
[[ApplePayManager sharedManager] handleCheckPurchaseTransaction:transaction];
}];
alertView.titleTextAlignment = NSTextAlignmentLeft;
alertView.messageTextAlignment = NSTextAlignmentLeft;
[alertView.otherButton setTitleColor:[UIColor wb_colorWithHexString:@"F44A4A"] forState:UIControlStateNormal];
[alertView show];
}
}
}
}
六、附言
大家做内购过程中遇到问题可以随时沟通哈!!! QQ:304517331
有好的建议也记得及时分享哦
祝大家工作顺利!!!!~~~~
七、干货
https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store?language=objc