Flutter In App Purchases

本文最后更新于: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) {
// handle error here.
});
}

///支付状态逻辑处理
static void _listenToPurchaseUpdated(
List<PurchaseDetails> purchaseDetailsList) {
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.pending) {
///购买进行中,展示加载框
Loading.show();
} else {
// APPPlugin.logger.d(purchaseDetails.status);
// APPPlugin.logger.d(purchaseDetails.purchaseID);
Loading.dismiss();
if (purchaseDetails.status == PurchaseStatus.error) {
///购买失败,展示失败信息
// Loading.error(purchaseDetails.error!.message!);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
///购买成功,展示成功信息
// print(purchaseDetails.status);
// print('purchaseID:${purchaseDetails.purchaseID}');
// print('productID:${purchaseDetails.productID}');
} else if (purchaseDetails.status == PurchaseStatus.restored) {
///恢复购买
// print(purchaseDetails);
}
if (purchaseDetails.pendingCompletePurchase) {
///这个方法是为了应对购买成功后,app崩溃了,重新打开app,此时需要通知IAP平台,已经完成了购买流程
InAppPurchase.instance.completePurchase(purchaseDetails);
}
// APPPlugin.logger.d(purchaseDetails.status);
///调用回调的后台接口,通知后台购买成功或者失败
if (orderCallback != null) {
orderCallback!(purchaseDetails);
}
}
});
}

///获取ios和android商店里面配置的商品列表
static Future<List<ProductDetails>> getServerProducts(
Set<String> pIds) async {
///根据商品id获取云端商品列表
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 [];
}

///购买商品 type 1:购买 2:订阅
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();
}
}


Flutter In App Purchases
http://bestkele.com/2024/02/10/flutter/flutter-inapp-purchases/
作者
kele
发布于
2024年2月10日
许可协议