App内购通关:(二)代码篇

一:内购流程
app内购流程图
二:代码实现:内购工具类的集成
1.导入库
#import <StoreKit/StoreKit.h>
2.遵守协议
<SKPaymentTransactionObserver, SKProductsRequestDelegate>
3.内购工具类的启动与注销

程序启动就开启工具的原因: 简单来说是为了防漏单,详情在下面配合代码来解释。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
   /**启动IAP工具类*/
    [[IAPManager shared] startManager];
    return YES;
}

//程序推出的时候关闭工具
- (void)applicationWillTerminate:(UIApplication *)application {
     /**结束IAP工具类*/
    [[IAPManager shared] stopManager];
}

4.内购工具类的启动与注销

内购支付两个阶段:

  • 阶段一: app直接向苹果服务器请求商品,支付阶段;
  • 阶段二: 苹果服务器返回凭证,app向公司服务器发送验证,公司再向苹果服务器验证阶段。
- (void)startManager { //开启监听
    /*
     阶段一正在进中,app退出。
     在程序启动时,设置监听,监听是否有未完成订单,有的话恢复订单。
     */
    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

    /*
     阶段二正在进行中,app退出。
     在程序启动时,检测本地是否有receipt文件,有的话,去二次验证。
     */
    [self checkIAPFiles];
}

- (void)stopManager{ //移除监听 
    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
}
5.通过产品ID发起查询商品请求
- (void)requestProductWithId:(NSString *)productId {
    if ([SKPaymentQueue canMakePayments]) { //用户允许app内购
        if (productId.length) {
            NSArray *product = [[NSArray alloc] initWithObjects:productId, nil];
            NSSet *set = [NSSet setWithArray:product];
            SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:set];
            productRequest.delegate = self;
            [productRequest start];
        } else {
            NSLog(@"商品为空");   
        }
    } else { 
          NSLog(@"没有权限");
    }
}
6.查询成功
#pragma mark SKProductsRequestDelegate 查询成功后的回调
- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    NSArray *product = response.products; 
    if (product.count == 0) { 
        NSLog(@"无法获取商品信息,请重试"); 
    } else {
        //发起购买请求
        SKPayment * payment = [SKPayment paymentWithProduct:product[0]];
        [[SKPaymentQueue defaultQueue] addPayment:payment];
    }
}
7.查询失败
#pragma mark SKProductsRequestDelegate 查询失败后的回调
- (void)request:(SKRequest *)request didFailWithError:(NSError *)error {
   NSLog(@"查询失败:%@",[error localizedDescription]);
}
8.步骤6中查询成功后发起了购买请求,用户操作付款后的回调
#pragma Mark 购买操作后的回调
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(nonnull NSArray<SKPaymentTransaction *> *)transactions {

    for (SKPaymentTransaction *transaction in transactions) {
        switch (transaction.transactionState) {
            case SKPaymentTransactionStatePurchasing://正在交易
                break;
            
            case SKPaymentTransactionStatePurchased://交易完成
                
               //获取苹果订单号
               //self.transaction_id = transaction.transactionIdentifier;
                [self getReceipt]; //获取交易成功后的购买凭证
                [self saveReceipt]; //存储交易凭证
                [self checkIAPFiles];//把self.receipt发送到服务器验证是否有效
                [self completeTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateFailed://交易失败
                [self failedTransaction:transaction];
                break;
                
            case SKPaymentTransactionStateRestored://已经购买过该商品
                [self restoreTransaction:transaction];
                break;
            default:
            break;
        }
    }
}
9.获取交易成功后的购买凭证

注:验证用的receipt,不管是你处理,还是让服务器处理,发给苹果验证的时候,必须是一个base64编码的字符串。

- (void)getReceipt {
    NSURL *receiptUrl = [[NSBundle mainBundle] appStoreReceiptURL]; 
    NSData *receiptData = [NSData dataWithContentsOfURL:receiptUrl]; 
    self.receipt = [receiptData base64EncodedStringWithOptions:0];
}
10.先将购买凭证存到本地

目的:防止用户付款拿到receipt后,app发送给公司服务器的过程中,程序闪退等原因致使凭证丢失。

#pragma mark  持久化存储用户购买凭证(这里最好还要存储当前日期,用户id等信息,用于区分不同的凭证)
-(void)saveReceipt {
    self.date = [NSDate chindDateFormate:[NSDate date]];
    NSString *fileName = [NSString uuid];
    self.userId = @"UserID";
    NSString *savedPath = [NSString stringWithFormat:@"%@/%@.plist", [SandBoxHelper iapReceiptPath], fileName];
    NSDictionary *dic =[NSDictionary dictionaryWithObjectsAndKeys:
                        self.receipt,                           receiptKey,
                        self.date,                              dateKey,
                        self.userId,                            userIdKey,
                        nil];  
    [dic writeToFile:savedPath atomically:YES];
}
11.检查本地是否存在凭证
  • 步骤10中将凭证存到了本地,下面的方法就是查询本地找到凭证,发送给服务器;
  • 同时这个方法也会在程序启动即:内购工具类启动的时候调用,如果能找到本地文件,说明上次因为闪退等原因导致凭证没发送给服务器, 将会再次发送。(后面有验证成功后凭证的处理方式)
- (void)checkIAPFiles{
    NSFileManager *fileManager = [NSFileManager defaultManager];  
    NSError *error = nil;   
    //搜索该目录下的所有文件和目录
    NSArray *cacheFileNameArray = [fileManager contentsOfDirectoryAtPath:[SandBoxHelper iapReceiptPath] error:&error];    
     if (error == nil) {
        for (NSString *name in cacheFileNameArray) {    
            if ([name hasSuffix:@".plist"]){ //如果有plist后缀的文件,说明就是存储的购买凭证      
                NSString *filePath = [NSString stringWithFormat:@"%@/%@", [SandBoxHelper iapReceiptPath], name];      
                [self sendAppStoreRequestBuyPlist:filePath];
            }
        } 
    } else {
        NSLog(@"AppStoreInfoLocalFilePath error:%@", [error domain]);
    }
}
12.将购买凭证发送到公司服务器,根据服务器向苹果验证返回的结果做相应处理
  • 如果凭证有效,及此次交易完成,删除本地的此次凭证。
-(void)sendAppStoreRequestBuyPlist:(NSString *)plistPath {

    NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:plistPath];
    //这里的参数请根据自己公司后台服务器接口定制,但是必须发送的是持久化保存购买凭证
    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                                   [dic objectForKey:receiptKey],          receiptKey,
                                   [dic objectForKey:dateKey],             dateKey,
                                   [dic objectForKey:userIdKey],           userIdKey,
                                   nil];

#warning 在这里将凭证发送给服务器
    
    if(@"凭证有效"){    
        [self removeReceipt];    
    } else {//凭证无效     
        //做你想做的
    }
}
13.删除凭证
-(void)removeReceipt{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:[SandBoxHelper iapReceiptPath]]) {
        [fileManager removeItemAtPath:[SandBoxHelper iapReceiptPath] error:nil];
    }
}

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


15.如果交易失败,做相应的提示,并在将交易结束
- (void)failedTransaction:(SKPaymentTransaction *)transaction {
    if(transaction.error.code != SKErrorPaymentCancelled) {
       NSLog(@"购买失败");
    } else {
       NSLog(@"用户取消了交易");
    }
    //将交易结束
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 
}

16.恢复已经购买过的产品
- (void)restoreTransaction:(SKPaymentTransaction *)transaction {
    [[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}

17.封装成工具后的使用方法

一句代码搞定

- (void)payClick {
    [[IAPManager shared] requestProductWithId:productId];
}
以上便为内购的全部流程,这里为代码地址:

GitHub:https://github.com/YZQ-Nine/IAPDemo

App内购通关:(一)非代码准备篇

注:大坑

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

推荐阅读更多精彩内容