应用内购iOS

iOS开发 AIP支付总结

2020-04-30  本文已影响0人  方向_4d0d

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

59601586421668_.pic.jpg
上一篇下一篇

猜你喜欢

热点阅读