Flutter iOS 苹果IAP(内购)实现步骤及问题总结(全网最全)

IAP内购支付流程

效果图
  • Client向Server发送请求,获得一份产品列表。
  • Server返回包含产品标识符的列表。
  • Client向App Store发送请求,得到产品的信息。
  • App Store返回产品信息。
  • Client把返回的产品信息显示给用户(App的store界面)
  • 用户选择某个产品
  • Client向App Store发送支付请求
  • App Store处理支付请求并返回交易完成信息
  • Client从信息中获得数据,并发送至Server
  • Server纪录数据,并进行审(我们的)查
  • Server将数据发给App Store来验证该交易的有效性
  • App Store对收到的数据进行解析,返回该数据和说明其是否有效的标识
  • Server读取返回的数据,确定用户购买的内容
  • Server将购买的内容传递给Client

配置内购环境

  • 按要求录入该账户银行卡信息和相关用户信息
  • 根据项目需求选择适合的内购类型:消耗项目、非消耗项目、自动续费、非自动 续费;
  • 录入相应的产品内购信息如:名称、ID、价格;
  • 创建 沙盒测试账号
  • Capabilities里面打开Purchase功能

* 录入该账户银行卡信息和相关用户信息

点击 “协议、税务和银行业务”


15755521108887.jpg

内购用的是付费应用程序,先签署《付费应用程序协议》,同意后状态变更为“用户信息待处理”,等待审核。


15755521303042.jpg

状态更改完毕后,点击“开始设置税务、银行业务和联系信息”。
(1)添加银行账户,按照要求填写相关内容即可。


15755521500978.jpg

(2)选择报税表,并填写。所有与 Apple 有商业合作者必选都是美国,若有其他需求,可以多选。

15755521681828.jpg

15755521821480.jpg

继续填写,首先认证公司基本信息,选择所有人类型,确认无误后认证条款处打对勾


15755521934163.jpg
15755522006604.jpg

Part I 部分,继续核对公司相关信息,选填内容可不填。

15755522475582.jpg

Part III 部分,签署税务条约,设置利益限制条款的种类,选填内容可不填。此部分如果需要可勾选上下图勾选框,不需要可不勾选,我们这个项目没有用到part III 部分,所以没有勾选。


15755522609491.jpg

Part XXX 部分,确认之前填写的信息,勾选完毕后,提交

15755522722467.jpg

(3)填写联系信息,共5个。高级管理、财务、技术、法务、营销。只需要提供5个人的基本信息即可。

15755522842121.jpg

添加内购商品信息

https://appstoreconnect.apple.com/apps/1604297713/appstore/addons?m=

image.png

消耗型项目

只可使用一次的产品,使用之后即失效,必须再次购买。

示例:钓鱼 App 中的鱼食。

非消耗型项目

只需购买一次,不会过期或随着使用而减少的产品。

示例:游戏 App 的赛道。

自动续期订阅

允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。

示例:每月订阅提供流媒体服务的 App。

非续期订阅

允许用户购买有时限性服务的产品。此 App 内购买项目的内容可以是静态的。此类订阅不会自动续期。

示例:为期一年的已归档文章目录订阅。

用于 App 审核的商品截屏

App 内购买项目的截屏,即所售项目的示意图。例如,如果 App 内购买项目是一本图书,您可以提交图书的截屏。您也可以提交购买页的截屏。该截屏仅用于 Apple 审核,不会在 App Store 中显示。
截屏要求如下:

iOS 至少需要 640 x 920 像素

Apple tvOS 需要 1920 x 1080 像素

macOS 需要 1280 x 800 像素

App 审核图像上传后,可以替换,但无法移除。当您的 App 内购买项目处于审核中时,您无法更新截屏。

  • 在首页上,点按“我的 App”,然后选择与该 App 内购买项目相关联的 App。
  • 在工具栏中,点按“功能”,然后在左列中点按“App 内购买项目”。
  • 若要添加 App 内购买项目,请前往“App 内购买项目”,并点按“添加”按钮(+)。
  • 选择“消耗型项目”、“非消耗型项目”或“非续订订阅”,并点按“创建”。有关自动续订订阅的信息,请参见创建自动续期订阅。
  • 添加参考名称、产品 ID 和本地化显示名称。
  • 点按“存储”或“提交以供审核”。
  • 提交审核前需要上传购买界面截图,供苹果审核
  • 在项目中开启In-App Purchase
15755367143317.jpg
  • 提交新的版本
    我看的文章都是创建好内购项目之后就可能在项目中使用测试了,当时我的结果就是无效productID。绿色框中写的很明白要重新提交二进制文件(.ipa文件),新的版本中添加在步骤3中添加的内购项目。
    15755368021179.jpg
  • 注意:最好选择手动发布,因为本次提交只是为了让创建的内购项目ID生效,项目中可以没有关于内购的逻辑代码

* 沙盒账号创建及使用注意事项

① 沙盒账号创建

https://appstoreconnect.apple.com/access/testers

image.png

  • 登录苹果开发者后台--iTunes Connect--用户和职能--沙箱测试技术员,在这个界面你可以看到当前账号已经创建好的沙盒账号。
    Snip20191205_2.png
  • 点击“+”进行创建
WeChat6429578a0beffd1414cc33a45386f875.png

②注意事项

  • 电子邮件不能是别人已经注册过AppleID的邮箱
  • App Store 地区不要乱选。虽然随便哪个地区都可以用来测试(还没上线之前app并没有地区之分),但是在沙盒测试的时候,弹出的购买提示框会根据当前AppleID(沙盒账号)的地区显示语言的。

沙箱账号怎么登录不成功?

沙箱账号是不能直接在App Store进行登录的,只能在点击了购买商品之后,在弹出的登录框进行登录

验证是否已登录沙箱测试账号:

设置--iTunes Store与App Store,页面拉到最底部,会看到沙箱账户项会列出你已登录的沙箱测试账号!

③沙盒账号使用的前提

  • bundleID别搞错了,开发者账号、证书、bundleID要一致
  • 内购的商品ID,价格等相关信息已经录入到开发者后台了(不然那你买什么)
  • 开发者后台已经创建好沙盒测试账号了(下面我们会讲如何创建)
  • 你要有一部真机(iPhone或iPad都行,别用模拟器就好。而且不能是越狱机)
  • 如果你是第一次在这个开发者账号上集成内购功能,请先将iTune Connect上的税务协议都填写好,否则内购时会发现商品ID无效。

④沙盒账号使用流程

  • 1.在iPhone上安装测试包(必须是adhoc签名证书或者develop签名证书打的包,不能是从App Store上下载的)
  • 2.退出iPhone的App Store账号(因为我们需要使用沙盒账号登录)

操作方法一:打开App Store应用首页滑到最下方--选中AppleID--注销
操作方法二:设置--iTunes Store与App Store--选中AppleID--注销

  • 3.在测试包里面购买商品,系统会让你进行登录,这里我们点击“使用现有的AppleID”就可以输入刚才创建好的沙盒测试账号进行登录了
  • 4.点击购买商品之后,成功的话会出现相应提示
  • 5.我们在iTunes Connect上创建商品了之后,除了需要填商品ID,商品名称,商品描述,价格等之外,还要上传一张图片,图片就是下面这个界面。
15755358556541.jpg

flutter_inapp_purchase 支付插件使用

1.下载依赖

# iOS 内购
flutter_inapp_purchase: ^2.0.5

2.常见用法

2.1初始化配置(initState)

checks if the client can make payments(检测App是否能支付)

StreamSubscription _purchaseUpdatedSubscription;
StreamSubscription _purchaseErrorSubscription;
List<IAPItem> _items = [];
List<PurchasedItem> _purchases = [];
Future<void> initPlatformState() async {
   
    // prepare
    var result = await FlutterInappPurchase.instance.initConnection;
    print('result: $result');
    
    // 判断容器是否加载
    if (!mounted) return;

    // 更新购买订阅消息
    _purchaseUpdatedSubscription =
        FlutterInappPurchase.purchaseUpdated.listen((productItem) {
      print('purchase-updated: $productItem');
    });
    // 购买报错订阅消息
    _purchaseErrorSubscription =
        FlutterInappPurchase.purchaseError.listen((purchaseError) {
      print('purchase-error: $purchaseError');
    });
  }

2.2结束支付

await FlutterInappPurchase.instance.endConnection;
  _purchaseUpdatedSubscription.cancel();
  _purchaseUpdatedSubscription = null;
  _purchaseErrorSubscription.cancel();
  _purchaseErrorSubscription = null;
  setState(() {
    this._items = [];
    this._purchases = [];
  });

2.3获取商品(_getProduct)

final List<String> _productLists = Platform.isAndroid
  ? [
      'android.test.purchased',
      'point_1000',
      '5000_point',
      'android.test.canceled',
    ]
  : ['com.cooni.point1000', 'com.cooni.point5000'];
List<IAPItem> _items = [];
////////////////////////////////////
Future _getProduct() async {
    List<IAPItem> items =
        await FlutterInappPurchase.instance.getProducts(_productLists);
    for (var item in items) {
      print('item==============>${item.toString()}');
      this._items.add(item);
    }

    setState(() {
      this._items = items;
      this._purchases = [];
    });
  }

2.4获取已购买商品(_getPurchases)

getAvailablePurchases
Get all non-consumed purchases 获取未消费的商品

Future _getPurchases() async {
    List<PurchasedItem> items =
        await FlutterInappPurchase.instance.getAvailablePurchases();
    print('_getPurchases${items}');
    for (var item in items) {
      print('getAvailablePurchases======>${item.toString()}');
      this._purchases.add(item);
    }

    setState(() {
      this._items = [];
      this._purchases = items;
    });
  }

2.5.获取购买历史(getPurchaseHistory)

Future _getPurchaseHistory() async {
    List<PurchasedItem> items =
        await FlutterInappPurchase.instance.getPurchaseHistory();
    print('_getPurchaseHistory${items}');
    for (var item in items) {
      print('${item.toString()}');
      this._purchases.add(item);
    }

    setState(() {
      this._items = [];
      this._purchases = items;
    });
  }

2.6购买商品(requestPurchase)

print("---------- Buy Item Button Pressed");
Map<String, dynamic> json = {
'price': '0.01',
'productId': 'com.games.ztyxs.product_point.1'
};
IAPItem item = IAPItem.fromJSON(json);
this._requestPurchase(item);

---------------------------
void _requestPurchase(IAPItem item) {
    FlutterInappPurchase.instance
        .requestPurchase(item.productId ?? 'com.games.ztyxs.product_point.1');
  }

打印信息查询;

purchase-error: responseCode: null, debugMessage: Invalid product ID., code: E_DEVELOPER_ERROR, message: Invalid product ID.

原因:
没有先执行getProducts,直接执行requestPurchase方法,要先拉取商品列表,再执行购买操作.

2.7.补充

  • 如果用户退款,在recipt字段中会接收到cancel_data字段取消日期对于由Apple客户支持取消的交易,取消的时间和日期

iOS 内购返回商品列表ID为空

问题描述;

response.products商品返回列表为空
response.invalidProductIdentifiers无效产品id有数据
解决方法
  • 1.创建的App ID是否启用了IAP功能。
//允许内购允许iap
if([SKPaymentQueue canMakePayments]){
    [self requestProductData:product];
}else{
    NSLog(@"不允许程序内付费");
}
  • 2.商品信息是否配置到iTurn Connect,并到达“准备提交”状态.
image.png
  • 3.在iTurn Connect中创建沙盒测试员,并收取邮件激活。之后登录到测试用手机的设置页面中(Store选项)。

  • 4.是否创建相应的provisioning profile,并用此签名App。

  • 5.iTurn Connect后台配置完商品信息后,是否等待若干小时生效。

  • 6.SKProductsRequest请求的商品Id必须和iTurn Connect中配置的一致。(如:com.test.product.xxx)

  • 7.iTunes Connect中配置的银行、税务信息是否正确。

  • 8.是否先删除旧App,再重新编译生成新的。

漏单等情况预防与处理方案

1.漏单必须要处理,玩家花RMB购买的东西却丢失了,是绝对不能容忍的。所谓的漏单就是玩家已经正常付费,却没有拿到该拿的道具。

解决:只要购买成功,便将购买记录(receipt等账单信息)保存下来,然后将账单信息传送给我们游戏服务器,游戏服务器获得账单后,和苹果服务器验证,账单有效的话,回馈给游戏服务器处理,游戏服务器处理后,返回给游戏客户端处理,处理完毕,将本地保存的购买记录删除。

receipt-data 支付凭证校验

https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html 官方文档:向苹果校验支付凭证

苹果反馈的状态码

21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证 【请求sandbox校验支付凭证】
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证

被苹果拒绝:内购类型问题

消耗类型: 例如:金币、道具等。
非续订订阅: non-renewable subscription 例如:VIP

注意

您的首个 App 内购买项目必须以新的 App 版本提交。请创建您的 App 内购买项目,然后前往 App 的“App Store”页,从“App 内购买项目”中进行选择,点按“提交”。 了解更多

在上传二进制文件并提交首个 App 内购买项目以供审核后,您可以使用下表提交其他 App 内购买项目。

参考

唐巧-iOS应用内付费(IAP)开发步骤列表

未完~待续

iOS内购问题总结

1.沙盒测试账号在支付成功后,再次购买相同的商品,会提示“您已购买此App内购买项目。此项目将免费恢复。”

image.png

问题分析

当使用内购购买过商品之后没有把这个交易关闭,所以再次去购买商品后就会调用以前已经购买成功的交易去购买因为已经购买过,才会有这个提示

解决方法

  • 使用[[SKPaymentQueue defaultQueue] addPayment:payment];这个方法进行支付请求后,因为我们已经把支付所需要的信息都添加到苹果的支付队列,苹果会自动完成后续的购买请求
  • 用户购买成功或者点击取消购买的后会回调
// 该方法返回响应的结果信息
- (void)paymentQueue:(SKPaymentQueue )queue updatedTransactions:(NSArray )transaction;
  • 在该方法内除了得到响应的支付信息编写自身的业务的代码外还要记得调用[[SKPaymentQueue defaultQueue] finishTransaction:transaction];方法通知苹果的支付队列该交易已经完成,否者就会调用已经购买成功的支付队列,就会出现您以购买过此APP内购项目,此项目将免费恢复这句提示。
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions{
    NSLog(@"调用了几次这个方法?");
    SKPaymentTransaction *transaction = transactions.lastObject;
    switch (transaction.transactionState) {
        case SKPaymentTransactionStatePurchased: {
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];//记得关闭交易事件

            NSLog(@"购买完成,向自己的服务器验证 ---- %@", transaction.payment.applicationUsername);
            NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
            NSString *receipt = [data base64EncodedStringWithOptions:0];
//            [self buySuccessWithReceipt:receipt transaction:transaction];
        }
            break;
        case SKPaymentTransactionStateFailed: {
            NSLog(@"交易失败");
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        }
            break;
        case SKPaymentTransactionStateRestored: {
            NSLog(@"已经购买过该商品");
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
        }
            break;
        case SKPaymentTransactionStatePurchasing: {
            NSLog(@"商品添加进列表");
        }
            break;
        default: {
        }
            break;
    }
}
  • 在买次购买之前检测是否有未完成的交易如果有就关闭
NSArray* transactions = [SKPaymentQueue defaultQueue].transactions;
   if (transactions.count > 0) {
       //检测是否有未完成的交易
       SKPaymentTransaction* transaction = [transactions firstObject];
       if (transaction.transactionState == SKPaymentTransactionStatePurchased) {
           [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
           return;
       }  
   }

2.iOS后台添加内购项目,提交显示元数据丢失

原因:添加内购项目时,信息填写不完整,app审核图像未上传

处理方法:上传app审核图片(合适的尺寸),点击提交,状态改为正在准备审核中。

3.您已经购买了此项目,您想免费再获取一次吗

这个是内购选择类型不匹配原因导致。

  • 非消耗型就是消耗一次后在该appid下都能使用。
  • 消耗型比如一些直播平台的货币 使用完以后可以在充值。

4.Purchase二次验证

购买成功之后,Apple会返回以下四个数据给应用

  • 产品标识符:
  • 交易状态: state
  • Receipt:很长的一段字符串,大概49行,作为二次验证的重要依据
  • 交易标识符: transaction Identifier 我们需要把Receipt发送給苹果的苹果的服务器验证,用户的购买信息是否真实

交易状态

  • Purchased 购买成功
  • Restored 恢复购买
  • Failed 失败
  • Deferred 等待确认,儿童模式需要询问家长同意

苹果返回状态码

Reference

  • 21000 App Store不能读取你提供的JSON对象
  • 21002 receipt-data域的数据有问题
  • 21003 receipt无法通过验证
  • 21004 提供的shared secret不匹配你账号中的shared secret
  • 21005 receipt服务器当前不可用
  • 21006 receipt合法,但是订阅已过期。服务器接收到这个状态码时,receipt数据仍然会解码并一起发送
  • 21007 receipt是Sandbox receipt,但却发送至生产系统的验证服务
  • 21008 receipt是生产receipt,但却发送至Sandbox环境的验证服务

5.苹果后台添加内购商品报问题

Review the updated Paid Applications Schedule.

image.png

解决办法

  • 先打开 https://itunesconnect.apple.com 首页

  • 然后找到这个协议的位置


    image.png
  • 点进去之后,如下所示


    image.png
  • 右上角有个Request按钮,好了,点击这个按钮,然后点击Submit提交


    image.png
  • 同意之后就OK了,然后到首页重新添加商品可以看到提醒消息已经消失了

5.iOS内购匿名购买-内购游客模式解决方案

游客身份解决方案:即不登录也要能购买

1)服务器端做一个苹果审核机制,审核期间游客身份可以进行一切行为,一旦审核通过,修改服务端即可达到强制用户登录进行内购买的目的(这个有点。。。)

2)游客可以进行内购买,购买时以设备UUID为准,生成一个游客账号,将购买信息保存在服务器和本地,当用户登录正式账户后判断此设备是否进行过内购,有的话提示用户将游客身份购买的权益与现有账号绑定,如果绑定,游客权益则迁移到正式账户,如果不迁移,则游客身份和正是账户是两个独立账户,正式账户不享有游客身份的权益(我用的这个)

内购游客模式解决方案
iOS内购规则

6.审核注意事项

  • 1.沙盒/正式环境:后台提供开关控制
  • 2.内购支付建议添加开关,因为截至上线前审核模式只能通过沙盒测试,上线后才能看到正式功能的支付。所以建议手动发布,发布前先关闭开关,待测试完成后再打开显示支付开关。
  • 3.支付流程
    • ①.先下单获取订单号
    • ②.获取支付参数(商品ID)
    • ③.发起支付,到支付回调
    • ④.通知上报transactionReceiptServer【此处Server验证较慢】
    • ⑤调用订单状态查询(轮询),添加友好提示
      问题描述:
  • 4.支付收据验证比较慢
  • 先快速通知,不做业务处理只上报:之后调取查询订单状态接口
  • 添加重试机制(比如5次或者5秒)
  • 5.漏单处理(断网,Server未收到支付凭证,Server无返回)

支付成功回调后,存储订单号和支付凭证,Server验证成功后remove,其他情况(例如:Server5秒无返回)发送事件通知(上报给Server),然后直接走订单查询,并且下次启动App时上报给Server。

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

推荐阅读更多精彩内容