本文最后更新于:2025年5月23日 下午
使用插件:in_app_purchase google play地址:Google Play
ios内购参考链接: 商品配置和测试配置 恢复购买
android内购参考链接: 账号设置
in_app_purchase 是 Flutter 官方团队出的插件,支持 iOS 和 Android 平台的内购功能。
1. 开发者账号设置 Google Play Console 和 App Store Connect 的账号设置都比较简单,按照文档来就行了,其中支付相关的沙盒测试人员,收款账户协议这些都是在应用外层的,只有商品信息是在应用内设置的,这一点两个平台是一致的。
android部分: 1、设置付款资料,必须先设置付款资料才能进行下面的设置(就是收款账号把,为什么叫付款资料?看来是google视角); 2、设置应用内商品; 3、添加测试账号; 4、测试内购必须使用内部测试版本; google play有三个测试类型,开放式测试,封闭式测试,内部测试,开放式测试是可以分发给任何人,封闭式测试是分发给一部分人灰度测试,这些人必须在测试账号里面或者收到邀请,但是封闭式测试是需要发布给google审核的,内部测试是开发人员测试用的。要测试内购需要测试员账号,所以是用内部测试;必须在内部测试的测试人员那个页面的下面点击复制链接,从这里链接下载的app才能获取到商品,否则是空的; 5、android的订阅通知是需要配置Google开发者通知的 参考链接:Google Play Bill Pub/Sub 在这里配置发布订阅信息 创建发布主题 创建订阅者和接口(订阅消息会往这个服务端接口推送) 最后在Google Play Console的设置里面配置一下这个发布主题就行了 订阅信息类型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * notificationType int * 通知的类型。它可以具有以下值: * (1) SUBSCRIPTION_RECOVERED - 从帐号保留状态恢复了订阅。 * (2) SUBSCRIPTION_ RENEWED - 续订了处于活动状态的订阅。 * (3) SUBSCRIPTION_CANCELED - 自愿或非自愿地取消了订阅。如果是自愿取消,在用户取消时发送。 * (4) SUBSCRIPTION_ PURCHASED - 购买了新的订阅。 * (5) SUBSCRIPTION_ON_ HOLD - 订阅已进入帐号保留状态(如已启用)。 * (6) SUBSCRIPTION_IN_ GRACE_PERIOD - 订阅已进入宽限期(如已启用)。 * (7) SUBSCRIPTION_ RESTARTED - 用户已通过“Play”>“帐号”>“订阅”重新激活其订阅(需要选择使用订阅恢复功能)。 * (8) SUBSCRIPTION_PRICE_ CHANGE_CONFIRMED - 用户已成功确认订阅价格变动。 * (9) SUBSCRIPTION_ DEFERRED - 订阅的续订时间点已延期。 * (10) SUBSCRIPTION_PAUSED - 订阅已暂停。 * (11) SUBSCRIPTION_ PAUSE_SCHEDULE_ CHANGED - 订阅暂停计划已更改。 * (12) SUBSCRIPTION_REVOKED - 用户在有效时间结束前已撤消订阅。 * (13) SUBSCRIPTION_ EXPIRED - 订阅已过期。*/
ios部分: 1、开发者证书那一套我就不说了,Identifiers那里的In-app purchases是默认勾选的; 2、添加内购商品信息,在app store connect 里面的 功能-app内购项目 模块,商品有四种:消耗型商品,非消耗型商品,自动续期订阅,非续期订阅。其中非消耗型商品,自动续期订阅这两种是需要恢复购买功能的。 3、在用户和访问模块左下方添加沙盒测试员,测试购买流程,这玩意烦得很,还必须是没注册过苹果账号的邮箱; 4、正式提交审核app的时候需要选择之前创建的商品,测试的时候不用管; 5、协议,税务,银行卡信息,其他信息没什么,那个CNAPS代码是这个发卡银行具体支行的代码;
2. 注意事项 1、其中非消耗型商品,自动续期订阅这两种是需要恢复购买功能的,不然app会被拒绝;
为什么需要恢复购买这个行为???按道理说服务端其实已经存了购买记录并且和App的账号体系绑定,换设备了也可以恢复,由软件提供商自己判断就行了,那么恢复购买是基于什么考虑的呢?
苹果希望支付行为和Apple ID绑定,而不是仅仅是和应用绑定;
并不是所有的App都有账号体系,所以需要恢复购买这个行为;
软件提供商有可能会要求更换设备重新购买商品,恢复购买是为了保护用户的权益;
2、android不需要加com.android.billingclient:billing
这个依赖,因为我们是flutter插件;
3、google play有三个测试类型,开放式测试,封闭式测试,内部测试,开放式测试是可以分发给任何人,封闭式测试是分发给一部分人灰度测试,这些人必须在测试账号里面或者收到邀请,但是封闭式测试是需要发布给google审核的,内部测试是开发人员测试用的。要测试内购需要测试员账号,所以是封闭式测试或者内部测试; 我发布了一个内部测试,发现删不掉了,靠! 新建一个测试轨道,也删不掉,只能暂停,牛逼。
4、google play 商店中appbundle要删除就必须把与他从已经关联的版本中分离出来,这样appbundle详情页才会有删除按钮;
5、我的app并没有集成广告,但是发布封闭式测试总是提示:
此版本包含 com.google.android.gms.permission.AD_ID 权限,但您在 Play 管理中心的声明中指出您的应用未使用广告 ID。您必须更新广告 ID 声明。
原因是我用了google_sign_in这个插件,它是依赖于firebase的,firebase项目开启了google Analytics,这玩意在项目打包阶段自动会加入一个权限,把aab文件后缀改成zip,解压之后在
base/manifest/AndroidManifest.xml文件中你就能看见ads权限声明了,如下:
1 <uses-permission android:name="com.google.android.gms.permission.AD_ID" />
stackoverflow 上的解决方案是下面这段代码,不允许添加这条权限,那么google Analytics也不会生效
1 <uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" />
那么干脆取消google Analytics算了,在firebase项目设置—集成 这里;
好了,我试过了,不管用,去掉设置依然还是会加入这段ad_id权限代码;
尝试用tools:node=”remove”来解决问题,这需要在最上面的<manifest
这里加
xmlns:tools=”http://schemas.android.com/tools “
最后是这样,参考链接 :
1 <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" >
有效!!!,这条权限是去掉了,但是商店里任然说有广告,我很无语,又查了一圈好像是google要求必须加上去广告ID ,哎,说不清道不明那就当我有广告吧,配置改回去。
6、android需要安装google 三件套才能用支付内购,我用的三星手机,三星毕竟是全球商品,内置有三件套其中的两个服务框架,所以只需要安装google play store就行了,先在商店里安装HiGoPlay ,然后用这个安装google play store
3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 import 'dart:async' ;import 'package:soulmate/utils/plugin/plugin.dart' ;import 'package:soulmate/widgets/library/projectLibrary.dart' ;import 'package:in_app_purchase/in_app_purchase.dart' ;class AppPurchase { static late StreamSubscription<dynamic > _subscription; static Function? orderCallback; static initAppPayConfig() async { final Stream purchaseUpdated = InAppPurchase.instance.purchaseStream; _subscription = purchaseUpdated.listen((purchaseDetailsList) { _listenToPurchaseUpdated(purchaseDetailsList); }, onDone: () { _subscription.cancel(); }, onError: (error) { }); } static void _listenToPurchaseUpdated( List <PurchaseDetails> purchaseDetailsList) { purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { if (purchaseDetails.status == PurchaseStatus.pending) { Loading.show (); } else { Loading.dismiss(); if (purchaseDetails.status == PurchaseStatus.error) { } else if (purchaseDetails.status == PurchaseStatus.purchased || purchaseDetails.status == PurchaseStatus.restored) { } else if (purchaseDetails.status == PurchaseStatus.restored) { } if (purchaseDetails.pendingCompletePurchase) { InAppPurchase.instance.completePurchase(purchaseDetails); } if (orderCallback != null ) { orderCallback!(purchaseDetails); } } }); } static Future<List <ProductDetails>> getServerProducts( Set <String > pIds) async { try { final bool isAvailable = await InAppPurchase.instance.isAvailable(); if (!isAvailable) { return []; } final ProductDetailsResponse response = await InAppPurchase.instance.queryProductDetails(pIds); if (response.notFoundIDs.isNotEmpty) { return []; } return response.productDetails; } catch (err) { APPPlugin.logger.e(err); } return []; } static payProductNow( ProductDetails productDetails, int type, String orderId) { if (productDetails == null ) { exSnackBar("please check you product" , type: ExSnackBarType.warning); return ; } final PurchaseParam purchaseParam = PurchaseParam( productDetails: productDetails, applicationUserName: orderId); if (type == 1 ) { InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam); } else { InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); } } static restorePuchases() async { await InAppPurchase.instance.restorePurchases(); } static disposeSubscription() { _subscription.cancel(); } }