IAPHelper集成文档

当前版本功能支持的功能

  • 内购商品列表请求-基于Block回调风格返回商品请求结果
  • 内购商品预加载- 应用启动后自动向苹果内购服务器请求内购商品列表
  • 支持自动订阅
  • 支持非消耗型内购商品购买 如移除广告,功能解锁等
  • 支持本地内购订单持久化
  • 支持本地会员有效期查询
  • 支持已购商品恢复购买
  • 支持不同权益档次会员,不同档次权益有效期互不干扰
    (如需实现高级会员过期后才生效普通会员,可根据各档次会员有效期自行重新叠加计算有效期)

当前版本暂不支持的功能

  • 新用户折扣优惠购买
  • 老用户折扣优惠购买

集成步骤及模块初始化

1.将IAHelper工程拖入到想要集成内购模块的 Workspace 中


image.png

2.将 IAPHelper.framework 添加到项目工程中

image.png
  1. 在需要用到 IAPHeper 模块的地方,导入 IAPHelper 模块
#import <IAPHelper/IAPHelper.h>

4.在工程的AppDelegate的初始化方法中 设置程序支持的内购项目。
注意:无论内购项是否在售卖状态,只要曾经有用户购买过,均需添加,因为其将会决定用户的相关权益有效期计算

+(void)initialize{
    //NOTE: 商品信息录入必须在内购模块启动前
    //自动订阅组
    IARenewalProductInfo * autoWeekProductInfo = [[IARenewalProductInfo alloc] initWithProductId:Group_First_Auto_Renew_First_Level periodType:ProductPeriodPerWeek probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kVIPGroupId isAutoRenewType:YES];
    
    IARenewalProductInfo * autoMonthProductInfo = [[IARenewalProductInfo alloc] initWithProductId:Group_First_Auto_Renew_Second_Level periodType:ProductPeriodPerMonth probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kVIPGroupId isAutoRenewType:YES];
    
    IARenewalProductInfo * autoYearProductInfo = [[IARenewalProductInfo alloc] initWithProductId:Group_First_Auto_Renew_Third_Level periodType:ProductPeriodPerYear probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kVIPGroupId isAutoRenewType:YES];
    
    //订阅组A
    IARenewalProductInfo * monthProductInfoA = [[IARenewalProductInfo alloc] initWithProductId:Program_A_First_Level_Original periodType:ProductPeriodPerMonth probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kVIPGroupId isAutoRenewType:NO];
    
    IARenewalProductInfo * yearProductInfoA = [[IARenewalProductInfo alloc] initWithProductId:Program_A_Second_Level_Original periodType:ProductPeriodPerYear probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kVIPGroupId isAutoRenewType:NO];
    
    IARenewalProductInfo * foreverProductInfoA = [[IARenewalProductInfo alloc] initWithProductId:Program_A_Third_Level_Original periodType:ProductPeriodForever probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kVIPGroupId isAutoRenewType:NO];
    
    //订阅组B
    IARenewalProductInfo * monthProductInfoB = [[IARenewalProductInfo alloc] initWithProductId:Program_B_First_Level_Original periodType:ProductPeriodPerMonth probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kSVIPGroupId isAutoRenewType:NO];
    
    IARenewalProductInfo * yearProductInfoB = [[IARenewalProductInfo alloc] initWithProductId:Program_B_Second_Level_Original periodType:ProductPeriodPerYear probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kSVIPGroupId isAutoRenewType:NO];
    
    IARenewalProductInfo * foreverProductInfoB= [[IARenewalProductInfo alloc] initWithProductId:Program_B_Third_Level_Original periodType:ProductPeriodForever probabtionPeriodType:ProductProbationPeriodTypeNone groupId:kSVIPGroupId isAutoRenewType:NO];
    
    //配置会员权益等计时型商品内购项。录入黄金月度会员,黄金年度会员,周会员,永久会员等
    [[IAPaymentCommon shareInstance] configRenewalProductInfosFromDeveloper:@[
        autoWeekProductInfo,autoMonthProductInfo,autoYearProductInfo,
        monthProductInfoA,yearProductInfoA,foreverProductInfoA,
        monthProductInfoB,yearProductInfoB,foreverProductInfoB
    ]];
    
    //配置普通非消耗型内购项。例如:移除广告内购,解锁关卡内购
//    IABaseProductInfo * removeAdsProduct = [[IABaseProductInfo alloc] initWithProductId:@"removeAds"];
//    IABaseProductInfo * unlockMoreFuncProduct = [[IABaseProductInfo alloc] initWithProductId:@"unlockMoreFuncs"];
//    [[IAPaymentCommon shareInstance] configNormalProductInfosFromDeveloper:@[removeAdsProduct,unlockMoreFuncProduct]];
}
  1. 在应用启动时,调用 IAPaymentCommon 的 load 方法来完成内购模块加载
    注意:需要传入的参数为该Apple开发者账号下的共享密钥,该密钥将影响本地购买凭据验证,进而影响相关权益有效期计算
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    //内购通用模块加载
    [[IAPaymentCommon shareInstance] load:IAPSecretKey];
    
//    未正常完成的交易
//    NSArray * unfinishedTransactions = [[IAPaymentCommon shareInstance] getUnFinishedPaymentTransactions];
//    if (unfinishedTransactions.count) {
//
//    }
    
    return YES;
}

商品请求、购买、恢复购买

商品请求

商品请求是后台静默执行的,不包含UI显示(系统弹出的除外),故为了交互友好,请在具体业务中添加相关等待提示框,状态提示框UI显示

[[IAPaymentCommon shareInstance] requestAllPreloadProductsWithCompletion:^(NSArray<IABaseProductInfo *> * _Nullable productArray, IAProductRequestStatus status, NSError * _Nullable error) {
        if (!error && productArray) {
            self.allProductInfos = [NSMutableArray arrayWithArray:productArray];
        }
        
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.refreshControl endRefreshing];
            [self.indicatorView stopAnimating];
            self.indicatorView.hidden = YES;
            [self.tableView reloadData];
        });
    }];

另一种商品请求方式,可精确请求指定商品列表

[[IAPaymentCommon shareInstance] requestMultipleProductIds:[IAPaymentCommon shareInstance].allProductIds complateHandler:^(NSArray<IABaseProductInfo *> * _Nullable productArray, IAProductRequestStatus status, NSError * _Nullable error) {
        //code here to refresh UI
    }];

商品购买

商品购买是后台静默执行的,不包含UI显示(系统弹出的除外),故为了交互友好,请在具体业务中添加相关等待提示框,状态提示框UI显示

MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    hud.label.text = NSLocalizedString(@"购买中...", @"HUD loading title");
    
    IABaseProductInfo * productInfo = self.allProductInfos[indexPath.row];
    [[IAPaymentCommon shareInstance] purchaseProductByProductId:productInfo.productId purchasing:^(NSString * _Nonnull productId, IAPurchasingState purchasingState, SKPaymentTransaction * _Nonnull transaction) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            if (purchasingState == IAPurchasingStateOrderGenerating) {
                hud.label.text = @"正在生成支付订单,请稍后...";
            }
            else{
                hud.label.text = @"订单已生成,正在获取订单信息...";
            }
        });
        
    } purchaseComplete:^(NSString * _Nonnull productId, SKPaymentTransaction * _Nullable paymentTransaction, BOOL isSuccess, NSError * _Nullable error) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            [hud hideAnimated:YES];
            
            NSString * message = isSuccess ? @"购买成功!" : @"购买失败!";
            
            MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];

            // Set the custom view mode to show any view.
            hud.mode = MBProgressHUDModeCustomView;
            // Set an image view with a checkmark.
            UIImage *image = [[UIImage imageNamed:@"Checkmark"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
            hud.customView = [[UIImageView alloc] initWithImage:image];
            // Looks a bit nicer if we make it square.
            hud.square = YES;
            // Optional label text.
            hud.label.text = message;

            [hud hideAnimated:YES afterDelay:2.f];
            
            if (isSuccess) {
                [self updateExpireDateInfo];
            }
        });
    } purchaseCancelled:^(NSString * _Nonnull productId, NSError * _Nullable error) {
        
        [hud hideAnimated:YES];
        
        NSString * message = @"购买已取消";
        
        MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];

        // Set the custom view mode to show any view.
        hud.mode = MBProgressHUDModeCustomView;
        // Set an image view with a checkmark.
        UIImage *image = [[UIImage imageNamed:@"Checkmark"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
        hud.customView = [[UIImageView alloc] initWithImage:image];
        // Looks a bit nicer if we make it square.
        hud.square = YES;
        // Optional label text.
        hud.label.text = message;

        [hud hideAnimated:YES afterDelay:2.f];
    }];
恢复购买

商品恢复购买是后台静默执行的,不包含UI显示(系统弹出的除外),故为了交互友好,请在具体业务中添加相关等待提示框,状态提示框UI显示

恢复购买提供有两个 api 来实现,分别是

调用苹果提供的恢复内购Api实现

MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    hud.label.text = NSLocalizedString(@"恢复购买中,请稍后...", @"HUD loading title");
    
    [[IAPaymentCommon shareInstance] restorePurchaseWithComplete:^(NSArray * _Nonnull productIds, BOOL isSuccess, NSError * _Nullable error) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            [hud hideAnimated:YES];
        });
        [self onRestoreComplete:productIds restoreState:isSuccess error:error];
    }];

调用苹果提供的刷新购买凭据Api实现

MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];
    hud.label.text = NSLocalizedString(@"恢复购买中,请稍后...", @"HUD loading title");
    
    [[IAPaymentCommon shareInstance] refreshReceiptWithComplete:^(NSArray * _Nullable productIds, BOOL isSuccess, NSError * _Nullable error) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            [hud hideAnimated:YES];
        });
        [self onRestoreComplete:productIds restoreState:isSuccess error:error];
    }];
//恢复购买完成
- (void)onRestoreComplete:(NSArray *)productIds restoreState:(BOOL)isSuccess error:(NSError *)error{
    
    dispatch_async(dispatch_get_main_queue(), ^{
        
        NSString * message = isSuccess ? @"恢复购买成功!" : @"恢复购买失败!";
        
        MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];

        // Set the custom view mode to show any view.
        hud.mode = MBProgressHUDModeCustomView;
        // Set an image view with a checkmark.
        UIImage *image = [[UIImage imageNamed:@"Checkmark"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
        hud.customView = [[UIImageView alloc] initWithImage:image];
        // Looks a bit nicer if we make it square.
        hud.square = YES;
        // Optional label text.
        hud.label.text = message;

        [hud hideAnimated:YES afterDelay:2.f];
        
        if (isSuccess) {
            [self updateExpireDateInfo];
        }
    });
}

目前并未发现两种方式有质的区别

刷新会员有效期

刷新普通会员有效期

//更新VIP有效期
- (void)updateVIPExpireInfo{
    
    //普通vip
    ProductGroupInfo * vipProductGroupInfo =  [[IAPaymentCommon shareInstance] getProductGroupWithGroupId:kVIPGroupId];
    
    NSTimeInterval timeInterval = vipProductGroupInfo.expireDateMs;
    NSDate * expireDate = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"yyyy-MM-dd";
    NSString * expireDateString = [formatter stringFromDate:expireDate];
    
    NSDate * relativetime = [[IATimeManager defaultManager] getRelativetime];//获取当前时间
    NSTimeInterval relativeTimeinterval = [relativetime timeIntervalSince1970];
    
    self.vipExpireDateLabel.textColor = [UIColor systemBlueColor];
    //当前是否有已购买且在有效期内的自动订阅内购项
    if (vipProductGroupInfo.hasActiveAutoRenewOrder) {
        self.vipExpireDateLabel.text = @"VIP到期时间:自动续订中";
    }else{
        if (relativeTimeinterval > timeInterval) {
            self.vipExpireDateLabel.textColor = [UIColor systemRedColor];
            self.vipExpireDateLabel.text = [NSString stringWithFormat:@"VIP会员已于%@过期",expireDateString];
            if (timeInterval == 0) {
                self.vipExpireDateLabel.text = @"未开通";
            }
        }
        else{
            if (timeInterval >= LONG_MAX) {
                self.vipExpireDateLabel.textColor = [UIColor systemPinkColor];
                self.vipExpireDateLabel.text = @"VIP到期时间:永不过期";
            }else{
                self.vipExpireDateLabel.text = [NSString stringWithFormat:@"VIP会员到期时间:%@",expireDateString];
            }
        }
    }
}

刷新超级会员有效期(没有可不处理)

//更新SVIP有效期
- (void)updatesSVIPExpireInfo{
    
    //超级vip
    ProductGroupInfo * svipProductGroupInfo =  [[IAPaymentCommon shareInstance] getProductGroupWithGroupId:kSVIPGroupId];
    
    NSTimeInterval timeInterval = svipProductGroupInfo.expireDateMs;
    NSDate * expireDate = [NSDate dateWithTimeIntervalSince1970:timeInterval];
    NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
    formatter.dateFormat = @"yyyy-MM-dd";
    NSString * expireDateString = [formatter stringFromDate:expireDate];
    
    NSDate * relativetime = [[IATimeManager defaultManager] getRelativetime];//获取当前时间
    NSTimeInterval relativeTimeinterval = [relativetime timeIntervalSince1970];
    
    self.svipExpireDateLabel.textColor = [UIColor systemBlueColor];
    //当前是否有已购买且在有效期内的自动订阅内购项
    if (svipProductGroupInfo.hasActiveAutoRenewOrder) {
        self.svipExpireDateLabel.text = @"SVIP到期时间:自动续订中";
    }else{
        if (relativeTimeinterval > timeInterval) {
            self.svipExpireDateLabel.textColor = [UIColor systemRedColor];
            self.svipExpireDateLabel.text = [NSString stringWithFormat:@"SVIP会员已于%@过期",expireDateString];
            if (timeInterval == 0) {
                self.svipExpireDateLabel.text = @"未开通";
            }
        }
        else{
            if (timeInterval >= LONG_MAX) {
                self.svipExpireDateLabel.textColor = [UIColor systemPinkColor];
                self.svipExpireDateLabel.text = @"SVIP到期时间:永不过期";
            }else{
                self.svipExpireDateLabel.text = [NSString stringWithFormat:@"SVIP会员到期时间:%@",expireDateString];
            }
        }
    }
}
凭据验证成功通知

因为本内购模块中权益有效期计算是基于本地购买凭据验证得来的,所以最真实的有效期信息应该来源于凭据验证,故建议在需要展示vip或记录vip有效期信息的地方注册本地凭据验证成功的通知, 并于业务中刷新权益有效期信息

//已购订单凭据验证完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onReceiptValidCompleteNotification:) name:kReceiptValidCompleteNotification object:nil];
/* 内购商品凭据本地校验完成通知 */
- (void)onReceiptValidCompleteNotification:(NSNotification *)notification{
    [self updateExpireDateInfo];;
}
商品请求成功通知
//商品请求完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onProductsRequestCompleteNotification:) name:kProductInfosRequestSucceedNotification object:nil];
来自AppStore商品购买通知
//来自AppStore商品购买通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAppStorePaymentNotification:) name:kPaymentFromAppStoreShouldHandleNotification object:nil];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 通过撰写产品需求文档(PRD),能够锻炼到基本的产品能力,同时也是提高axure操作能力的重要途径。笔者将以网易严...
    银海系阅读 3,748评论 4 31
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,040评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,030评论 0 4