本文最后更新于:2026年3月16日 下午
使用插件: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
| 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(); } }
|