iOS IAP应用内购详细步骤和问题总结指南

最近公司在做APP内购会员功能 遇到了很多问题 总结记录一下 首先一定要区分Apple pay 和IAP内购的区别
可以先去看一下官方文档地址 有每个步骤的详细解释
本篇文章分为:
1、 内购支付流程;
2、开发集成步骤;
3、问题(遇坑)记录解决方式

之前没看官方文档走了很多弯路 网上博客并不系统 强烈建议先过一遍官方文档

先看一下IAP内购支付流程(官方)

官方流程图
  1. 程序向服务器发送请求,获得一份产品列表。
  2. 服务器返回包含产品标识符的列表。
  3. 程序向App Store发送请求,得到产品的信息。
  4. App Store返回产品信息。
  5. 程序把返回的产品信息显示给用户(App的store界面)
  6. 用户选择某个产品
  7. 程序向App Store发送支付请求
  8. App Store处理支付请求并返回交易完成信息。
  9. 程序从信息中获得数据,并发送至服务器。
  10. 服务器纪录数据,并进行审(我们的)查。
  11. 服务器将数据发给App Store来验证该交易的有效性。
  12. App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识。
  13. 服务器读取返回的数据,确定用户购买的内容。
  14. 服务器将购买的内容传递给程序。

第一步:内购账户税务协议、银行卡绑定相关

一般都是运营或者产品经理处理这步 这篇文章图文步骤比较详细 处理税务银行相关设置 IAP,In App Purchases-在APP内部支付

第二步:Xcode设置相关

打开In-App Purchase开关 对应在开发者证书中心的项目证书中显示应该也是可用状态

屏幕快照 2018-08-22 下午6.00.11.png

屏幕快照 2018-08-22 下午6.01.35.png

第三步:在App Store Content -> 我的APP 添加内购项目商品

  1. 首页上,点按“我的 App”,然后选择与该 App 内购买项目相关联的 App。
  2. 在工具栏中,点按“功能”,然后在左列中点按“App 内购买项目”。
  3. 若要添加 App 内购买项目,请前往“App 内购买项目”,并点按“添加”按钮(+)。

屏幕快照 2018-08-23 上午10.06.23.png

选择功能 添加内购项目商品
选择功能Tab

内购商品对应四种类型 消耗型、非消耗型、自动续订订阅型、非续订订阅型
官方文档

  1. 选择“消耗型项目”、“非消耗型项目”或“非续订订阅”,并点按“创建”。有关自动续订订阅的信息,请参见创建自动续期订阅
  2. 添加参考名称、产品 ID 和本地化显示名称。
  3. 点按“存储”或“提交以供审核”。
您可以在创建您的 App 内购买项目时输入所有的元数据,或稍后输入您的 App 内购买项目信息。
屏幕快照 2018-08-23 上午10.09.34.png

添加一个测试商品 其他属性都可以随意填写 产品ID一定要认真填写 项目中需要根据ID获取商品信息 价格有不同的等级可以选 最低备用等级1 == 1元
填写完成之后储存 就完成了一个内购商品的添加

屏幕快照 2018-08-23 上午10.16.31.png

第四步:沙盒环境测试账号

因为涉及到钱相关 总不能直接用money去支付吧 所以需要你去添加一个沙盒技术测试人员的账号 (这个账号是虚拟的) 付款不会扣你
看第三步那张图 在App Store Content 选择用户和职能 进入下面页面 选择沙箱技术测试员 添加测试账号

屏幕快照 2018-08-23 上午11.02.26.png
屏幕快照 2018-08-23 上午11.05.28.png

Tips:Q:为什么添加沙箱技术测试员 注册不成功 Unknown Email xxxxxx
首先这里有个坑 邮箱只要符合格式就可以 虚假邮箱也可以 但密码必须符合正式的要求要有大小写和字符 复杂就好 例如:Lh123456*

第五步:代码实现(初步,未进行优化 有什么问题可以在评论中跟我沟通)

.h文件

typedef void(^XSProductStatusBlock)(BOOL isStatus);

@interface XSApplePayManager : NSObject


+ (instancetype)shareManager;

/** 检测客户端与服务器漏单情况处理*/
+ (void)checkOrderStatus;


/**
  根据商品ID请求支付信息


 @param orderId 订单号
 @param productId 商品号
 @param statusBlock 回掉block
 */
- (void)requestProductWithOrderId:(NSString *)orderId
                        productId:(NSString *)productId
                      statusBlock:(XSProductStatusBlock)statusBlock;

.m文件

#import <StoreKit/StoreKit.h>
#import "APIManager.h"
#import "UIAlertView+AABlock.h"

@interface XSApplePayManager ()<SKProductsRequestDelegate,SKPaymentTransactionObserver>

@property (nonatomic, copy) NSString *orderId;
@property (nonatomic, copy) XSProductStatusBlock statusBlcok;

@end

@implementation XSApplePayManager

+ (instancetype)shareManager
{
    static dispatch_once_t onceToken;
    static XSApplePayManager *manager = nil;
    dispatch_once(&onceToken, ^{
        manager = [[XSApplePayManager alloc]init];
    });
    return manager;
}

/** 检测客户端与服务器漏单情况处理*/
+ (void)checkOrderStatus
{
    NSDictionary *orderInfo = [XSApplePayManager getReceiptData];
    if (orderInfo != nil) {
        
        NSString *orderId = orderInfo[@"orderId"];
        NSString *receipt = orderInfo[@"receipt"];
        
        [[XSApplePayManager shareManager] verifyPurchaseForServiceWithOrderId:orderId receipt:receipt];
    }
}

#pragma mark -- 结束上次未完成的交易
-(void)removeAllUncompleteTransactionsBeforeNewPurchase{
    
    NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
    
    if (transactions.count >= 1) {
        
        for (SKPaymentTransaction* transaction in transactions) {
            if (transaction.transactionState == SKPaymentTransactionStatePurchased ||
                transaction.transactionState == SKPaymentTransactionStateRestored) {
                [[SKPaymentQueue defaultQueue]finishTransaction:transaction];
            }
        }
        
    }else{
        NSLog(@"没有历史未消耗订单");
    }
}


/** 检测权限 添加支付监测 开始支付流程*/
- (void)requestProductWithOrderId:(NSString *)orderId
                        productId:(NSString *)productId
                      statusBlock:(XSProductStatusBlock)statusBlock

{
    
    if (orderId == nil || productId == nil) {
        [AAProgressManager showFinishWithStatus:@"订单号/商品号有误"];
        return;
    }
    
    if ([[XZDeviceManager didRoot] isEqualToString:@"didRoot"]) {//写自己的越狱判断方法
        [AAProgressManager showFinishWithStatus:@"越狱手机不支持内购"];
        return;
    }
    
    
    if([SKPaymentQueue canMakePayments]){
        
        [self removeAllUncompleteTransactionsBeforeNewPurchase];
        
        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

        self.orderId = orderId;
        self.statusBlcok = statusBlock;
        [self requestProductData:productId];
        
    }else{
        [AAProgressManager showFinishWithStatus:L(@"请打开应用内支付功能")];
    }
}

/** 去Apple IAP Service 根据商品ID请求商品信息*/
- (void)requestProductData:(NSString *)type{
    
    [AAProgressManager showWithStatus:@"正在请求..."];
    NSArray *product = [[NSArray alloc] initWithObjects:type,nil];
    
    NSSet *nsset = [NSSet setWithArray:product];
    SKProductsRequest *request = [[SKProductsRequest alloc] initWithProductIdentifiers:nsset];
    request.delegate = self;
    [request start];
}


#pragma mark -- SKProductsRequestDelegate
//收到产品返回信息
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response{
    
    NSArray *product = response.products;
    if([product count] == 0){
        [AAProgressManager showFinishWithStatus:L(@"无法获取商品信息,请重新尝试购买")];
        return;
    }
    
    NSLog(@"产品付费数量:%ld",product.count);
    
    SKProduct *p = product.firstObject;
    
    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:p];
    payment.quantity = (NSInteger)p.price;//购买次数=价钱
    if (payment.quantity == 0) {
        payment.quantity = 1;
    }
    payment.applicationUsername = self.orderId;//[NSString stringWithFormat:@"%@",[[AAUserManager shareManager] getUID]];
    [[SKPaymentQueue defaultQueue] addPayment:payment];

}

//请求失败
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error{
    NSLog(@"------------------错误-----------------:%@", error);
    if (self.statusBlcok) {
        self.statusBlcok(NO);
    }
    [AAProgressManager showFinishWithStatus:L(@"从Apple获取商品信息失败")];

}

- (void)requestDidFinish:(SKRequest *)request{
    NSLog(@"------------反馈信息结束-----------------%@",request);
}

#pragma mark -- 监听AppStore支付状态
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transaction{
    
    NSLog(@"监听AppStore支付状态");
    dispatch_async(dispatch_get_main_queue(), ^{
        for(SKPaymentTransaction *tran in transaction){
            switch (tran.transactionState) {
                case SKPaymentTransactionStatePurchased:
                {
                    // 发送到苹果服务器验证凭证
                    [self verifyPurchaseWithPaymentTransaction];
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStatePurchasing:
                    NSLog(@"商品添加进列表");
                    break;
                case SKPaymentTransactionStateRestored:
                {
                    [AAProgressManager showFinishWithStatus:L(@"已经购买过商品")];
                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStateFailed:
                {
                    if (self.statusBlcok) {
                        self.statusBlcok(NO);
                    }
                    NSLog(@"交易失败");

                    [[SKPaymentQueue defaultQueue] finishTransaction:tran];
                }
                    break;
                case SKPaymentTransactionStateDeferred:
                {
                    [AAProgressManager showFinishWithStatus:L(@"最终状态未确定")];
                }
                    break;
                default:
                    break;
            }
        }
    });
    
}

#pragma mark -- 验证
/**验证购买,避免越狱软件模拟苹果请求达到非法购买问题*/
-(void)verifyPurchaseWithPaymentTransaction{
    
    //从沙盒中获取交易凭证并且拼接成请求体数据
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl];
    NSString *receiptString = [receiptData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    [self saveReceiptData:@{@"receipt":receiptString,
                            @"orderId":self.orderId}];
    
  
    [self verifyPurchaseForServiceWithOrderId:self.orderId
                                      receipt:receiptString];
}

- (void)verifyPurchaseForServiceWithOrderId:(NSString *)orderId
                                    receipt:(NSString *)receiptString
{
    if (orderId == nil && receiptString == nil) {
        if (self.statusBlcok) {
            self.statusBlcok(NO);
        }
        [AAProgressManager showFinishWithStatus:@"订单号/凭证无效"];
        return;
    }
    
    [self removeTransaction];

    [AAProgressManager showWithStatus:@"正在验证服务器..."];
    
    WS(weakSelf);
    [[APIManager sharedInstance] verifyPurchaseWithOrderID:orderId
                                                    params:@{@"ceceipt-data":receiptString}
                                                   success:^(id response)
     {
         dispatch_async(dispatch_get_main_queue(), ^{
             [AAProgressManager dismiss];
             [AAProgressManager showFinishWithStatus:L(@"交易完成")];
             [weakSelf removeLocReceiptData];
             if (weakSelf.statusBlcok) {
                 weakSelf.statusBlcok(YES);
             }
         });
         
     } failure:^(NSError *error) {
         dispatch_async(dispatch_get_main_queue(), ^{
             
             [CommonFunction showError:error];
             [weakSelf verifyPurchaseFail];
         });
     }];
}

- (void)verifyPurchaseFail
{
    WS(weakSelf);
    UIAlertView *altert =[UIAlertView alertViewWithTitle:@"服务器验证失败"
                                                 message:@"账单在验证服务器过程中出现错误,\n请检查网络环境是否可以再次验证\n如果取消可在网络环境良好的情况下重新启动行者可再次继续验证支付"
                                       cancelButtonTitle:L(@"取消")
                                       otherButtonTitles:@[L(@"再次验证")]
                                               onDismiss:^(NSInteger buttonIndex)
                          {
                              dispatch_async(dispatch_get_main_queue(), ^
                                             {
                                                 [XSApplePayManager checkOrderStatus];
                                             });           ;
                              
                          } onCancel:^{
                              dispatch_async(dispatch_get_main_queue(), ^{
                                  
                                  if (weakSelf.statusBlcok) {
                                      weakSelf.statusBlcok(NO);
                                  }
                                  [PromptInfo showWithText:@"可在网络环境良好的情况下重新启动行者可再次继续验证支付"];
                                  
                              });
                          }];
    [altert show];
}

//交易结束
- (void)completeTransaction:(SKPaymentTransaction *)transaction
{
    [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
}

- (void)removeTransaction
{
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}

#pragma mark -- 本地保存一次支付凭证
static NSString *const kSaveReceiptData = @"kSaveReceiptData";

- (void)saveReceiptData:(NSDictionary *)receiptData
{
    [[NSUserDefaults standardUserDefaults] setValue:receiptData forKey:kSaveReceiptData];
    [[NSUserDefaults standardUserDefaults]synchronize];
}

+ (NSDictionary *)getReceiptData
{
    return [[NSUserDefaults standardUserDefaults] valueForKey:kSaveReceiptData];
}

- (void)removeLocReceiptData
{
    [[NSUserDefaults standardUserDefaults] removeObjectForKey:kSaveReceiptData];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
第六步:IAP支付流程 & 服务器验证流程

整个支付流程如下:
1.客户端向Appstore请求购买产品(假设产品信息已经取得),Appstore验证产品成功后,从用户的Apple账户余额中扣费。
2.Appstore向客户端返回一段receipt-data,里面记录了本次交易的证书和签名信息。
3.客户端向我们可以信任的服务器提供receipt-data
4.服务器对receipt-data进行一次base64编码
5.服务器把编码后的receipt-data发往itunes.appstore进行验证
6.itunes.appstore返回验证结果给服务器
7.服务器对商品购买状态以及商品类型,向客户端发放相应的道具与推送数据更新通知

漏单处理 确保receipt-data的成功提交与异常处理

建立在IAP Server Model的基础上,并且我们知道手机网络是不稳定的,在付款成功后不能确保把receipt-data一定提交到服务器。如果出现了这样的情况,那就意味着玩家被appstore扣费了,却没收到服务器发放的道具。
漏单处理:
解决这个问题的方法是在客户端提交receipt-data给我们的服务器,让我们的服务器向苹果服务器发送验证请求,验证这个receipt-data账单的有效性. 在没有收到回复之前,客户端必须要把receipt-data保存好,并且定期或在合理的UI界面触发向服务端发起请求,直至收到服务端的回复后删除客户端的receipt账单记录。
如果是客户端没成功提交receipt-data,那怎么办?就是玩家被扣费了,也收到appstore的消费收据了,却依然没收到游戏道具,于是投诉到游戏客服处。

这种情况在以往的经验中也会出现,常见的玩家和游戏运营商发生的纠纷。游戏客服向玩家索要游戏账号和appstore的收据单号,通过查询itunes-connect看是否确有这笔订单。如果订单存在,则要联系研发方去查询游戏服务器,看订单号与玩家名是否对应,并且是否已经被使用了,做这一点检查的目的是 为了防止恶意玩家利用已经使用过了的订单号进行欺骗(已验证的账单是可以再次请求验证的,曾经为了测试,将账单手动发给服务器处理并成功),谎称自己没收到商品。这就是上面一节IAP Server Model中红字所提到的安全逻辑的目的。当然了,如果查不到这个订单号,就意味着这个订单确实还没使用过,手动给玩家补发商品即可。

更多可以查看这篇博文苹果IAP安全支付与防范 receipt收据验证

遇到的坑

Q:21004 你提供的共享密钥和账户的共享密钥不一致 什么是共享密钥? 共享密钥从哪里获取?

A:先看一下官方文档怎么说生成收据验证代码
为了在验证自动续期订阅时提高您的 App 与 Apple 服务器交易的安全性,您可以在收据中包含一个 32 位随机生成的字母数字字符串,作为共享密钥。
在 App Store Connect 中生成共享密钥。您可以生成一个主共享密钥,作为您所有 App 的单一代码,或作为针对单个 App 的 App 专用共享密钥。您也可以针对您的部分 App 使用主共享密钥,其他 App 使用 App 专用共享密钥。
点击下面展开就可以看到共享密钥生成的方式

Q:沙箱技术测试人员添加不成功 总是提示邮箱错误

A: 沙箱技术测试账号用于付款测试 任意未创建过Apple ID 的邮箱都可以 假的邮箱也可以 重要的是密码格式一定要包含大小写 跟正式账号注册规则一样 (例如:Lh123456*)

Q:自己服务器向苹果服务器验证收据/凭证参数是什么?向status code 验证apple iap sever的状态码代表什么意思?

A:21002、21003、21004、21005、21006、21007... 具体可以查看这篇文档用App Store验证收据

Q:Apple 和IAP的区别

A:IAP是链接App store的内购服务 一般是虚拟商品需要走的通道(比如会员功能)
Apple Pay是苹果跟各大银行合作的卡包形式的类似于刷卡支付服务 一般用于现实场景
这两个一定别搞混了

Q:怎么通过itunes-connect查看具体订单,itunes-connect中无法直接看到订单信息,可以用以下方法来查询

1.可以通过账单向苹果发送账单验证,有效可以手动补发
2 .用自己的服务器的记录账单列表对比
3.利用第三方的TalkingData等交易函数,会自动记录账单数据

还有一些问题可以借鉴一下这篇博文iOS之你一定要看的内购破解-越狱篇 他遇到的实际问题比较多 按需借鉴

觉得有帮助可以关注我 后续继续补充....

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

推荐阅读更多精彩内容