iOS 内购(In-App Purchase)总结

一、简单介绍

iOS 内购即(In-App Purchase)一共分为四种类型:(详细文档参考官网

  1. 消耗型商品:只可使用一次的产品,使用之后即失效,必须再次购买。 示例:钓鱼 App 中的鱼食。

  2. 非消耗型商品:只需购买一次,不会过期或随着使用而减少的产品。 示例:游戏 App 的赛道。

  3. 自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。 示例:每月订阅提供流媒体服务的 App

  4. 非续期订阅:允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。 示例:为期一年的已归档文章目录订阅。

只要在 iOS/iPadOS 设备上的 App 里购买非实物产品 (也就是虚拟产品,如:“金币、qq 币、鱼翅、电子书......”) ,都需要走内购流程,苹果从这里面抽走 30% 分成,实际结算是分成之前还需要先扣除交易税

二、内购前准备

APP内集成IAP代码之前需要先去开发账号的ITunes Connect进行以下三步操作(可参考该配置文档):

  1. 后台填写银行账户信息

  2. 配置商品信息,包括产品ID,产品价格等

这里需要注意的是产品 ID 具有唯一性,建议使用项目的 Bundle Identidier 作为前缀后面拼接自定义的唯一的商品名或者 ID(字母、数字),这里有个坑:一旦新建一个内购商品,它的产品ID将永远被占用,即使该商品已经被删除,已创建的内购商品除了产品 ID 之外的所有信息都可以修改,如果删除了一个内购商品,将无法再创建一个相同产品 ID 的商品,也意味着该产品 ID 永久失效,一般来说产品ID有特定的命名规则,如果命名规则下有某个产品 ID 永久失效,可能会导致整个产品ID命名规则都要修改,这里千万要注意!

  1. 配置用于测试IAP支付功能的沙箱账户。

三、内购流程

内购通用流程


截屏2024-04-01 19.59.06.png
  1. 用户向苹果服务器发起购买请求,收到购买完成的回调(购买完成后会把钱打给申请内购的银行卡内)

  2. 购买成功流程结束后, 向服务器发起验证凭证(app端自己也可以不依靠服务器自行验证)

  3. 自己的服务器工作分 4 步:
    3.1 接收 iOS 端发过来的购买凭证。
    3.2 判断凭证是否已经存在或验证过,然后存储该凭证。
    3.3 将该凭证发送到苹果的服务器(区分沙盒环境还是正式环境)验证,并将验证结果返回给客户端(注意需要传密钥,要不会报错21003)
    sandbox 开发环境: https://sandbox.itunes.apple.com/verifyReceipt
    prod 生产环境: https://buy.itunes.apple.com/verifyReceipt 具体操作可以看这个 通过App Store验证收据。
    3.4 修改用户相应的会员权限或发放虚拟物品。

简单来说就是将该购买凭证用 Base64 编码,然后 POST 给苹果的验证服务器,苹果将验证结果以 JSON 形式返回


image.png

恢复购买

内购有4种:消耗型项目,非消耗型,自动续期订阅,非续期订阅。 其中”非消耗型“和”自动续期订阅“需要提供恢复购买的功能,例如创建一个恢复按钮,不然审核很可能会被拒绝。

//调起苹果内购恢复接口[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

“消耗型项目”和“非续期订阅”苹果不会提供恢复的接口,不要调用上述方法去恢复,否则有可能被拒!!!

“非续期订阅”也是跨设备同步的,所以原则上来说也需要提供恢复购买的功能,但需要依靠app自建的账户体系恢复,不能用上述苹果提供的接口。

四、常见错误

获取不到商品信息:检查itc商品审核以及下线情况,检查设备知否支持内购

无法连接: 网络问题,切记沙盒环境下不可开vpn或者网络代理

截屏2024-04-01 20.06.14.png

五、注意事项

  1. 订阅产品需要验证订阅是否过期,自动续费在购买流程上,与普通购买没有区别,主要的区别:”除了第一次购买行为是用户主动触发的,后续续费都是 Apple 自动完成的,一般在要过期的前24小时开始,苹果会尝试扣费,扣费成功的话,在 App 下次启动的时候主动推送给 App“。
// 订阅特殊处理
if (transaction.originalTransaction) {  
    // 如果是自动续费的订单 originalTransaction 会有内容 
    } else {
    // 普通购买,以及第一次购买自动订阅
    }
  1. 沙盒账号在创建时就已经设置好了地区,中国的只能在中国的 App Store 测试,否则会提示不在此地区,请切回本地的应用商店

  2. 关于掉单的问题 答案:一定要在服务器校验完票据后,客户端收到服务器的反馈结果后再: [[SKPaymentQueue defaultQueue] finishTransaction: transaction];

  3. 自动订阅时间在沙盒环境下时间会缩短

截屏2024-04-01 20.24.53.png

六、自己本地验证收据

有时候可能需要自己本地验证收据来调试一些问题,可以参考以下代码,本地调用https://sandbox.itunes.apple.com/verifyReceipt去验证,传参记得传递密码(即共享密钥),成功后会收到苹果返回的数据

然后对于连续订阅型:

1.第一次对账

遍历latest_receipt_info里的票据,如果transactionIdentifier相同,那么本次交易就是有效的,找到对应的票据后,其中最关键的是expires_date_ms这个时间戳,这个和expires_date字符串不相符, expires_date是GMT时间,少8个小时,而expires_date_ms转换后是正常的,因此不用把这个时间戳再加8小时.

2.之后的对账

由于苹果会在到期前充值,充值失败也会有回调通知,所以当数据库中的时间到期后,再去用之前存的凭据调用对账接口,

然后遍历latest_receipt_info根据product_id,也就是商品id,把连续订阅的票据筛选出来

现在筛选出来的这些就是所有的此商品的订阅票据了,而且顺序是按照时间排序的,新的在后面,如果不放心,可以自己根据里面的expires_date_ms(或者其他时间戳)再排序,然后创建订单,更新过期时间等等操作

-(void)verifyFinishedTransaction:(SKPaymentTransaction *)transaction{
    NSString *str = [[NSString alloc] initWithData:transaction.transactionReceipt encoding:NSUTF8StringEncoding];

    NSString *environment = [self environmentForReceipt:str];
    NSLog(@"------ 完成交易调用的方法completedTransaction 1----------%@", environment);

    // 验证凭据,获取到苹果返回的交易凭据
    // appStoreReceiptURL iOS 7.0 增加的,购买交易完成后,会将凭据存放在该地址
    NSURL *url = [[NSBundle mainBundle] appStoreReceiptURL];
    NSString *receipt = [[NSData dataWithContentsOfURL:url] jk_base64EncodedString];
    [self log:[NSString stringWithFormat:@"sendString %@", receipt]];

    /** 注意:这里可以不用自己去验证,直接调用自己服务器接口,让后台去APP Store 验证*/

    NSURL *storeUrl = nil;
    storeUrl = [NSURL URLWithString:@"https://sandbox.itunes.apple.com/verifyReceipt"];

    NSDictionary *requestContents = @{
            @"receipt-data": receipt,
            @"password":@"共享密钥",
            @"exclude-old-transactions":@(YES)
    };
    NSError *error;
    // 转换为 JSON 格式
    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];
    NSString *verifyUrlString = @"https://sandbox.itunes.apple.com/verifyReceipt";
    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:[[NSURL alloc] initWithString:verifyUrlString] cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10.0f];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];

    // 在后台对列中提交验证请求,并获得官方的验证JSON结果
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:storeRequest completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error) {
            NSLog(@"链接失败");
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            [self log:[NSString stringWithFormat:@"verifyFinishedTransaction error%@", error]];
        } else {
            NSError *error;
            NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
            if (!jsonResponse) {
                NSLog(@"验证失败");
                [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            }
            [self log:[NSString stringWithFormat:@"verifyFinishedTransaction %@", jsonResponse]];

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