iOS开发 AIP支付总结

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271