in_app_purchase: ^3.1.13
用谷歌推出的内购插件,这个插件就是把ios内购api全部翻译一遍
所以原生能实现的功能 这个插件完全可以实现
要在初始化时候先注册下平台,因为安卓是谷歌支付,我们只用ios的内购
我的下面代码有很多异常处理,没完成订单查询等, 连续出错几次 直接finish等,确保丢单率最低,并且写入本地文件日志,有问题可以排查
if (defaultTargetPlatform == TargetPlatform.iOS) {
InAppPurchaseStoreKitPlatform.registerPlatform();
}
Future.delayed(const Duration(seconds: 1), () {
delayAction();
});
delayAction() async {
if (LoginTool.isLogin()) {
if (defaultTargetPlatform == TargetPlatform.iOS) {
//内购监听
await IOSPayment.instance.init();
IOSPayment.instance.startSubscription();
// 查询没完成订单 进入支付页面 调用一下
// TDIOSPayment.instance.checkUnfinishedPayment(() => null);
}
}
}
开始支付时候调用
IOSPayment.instance.isPaying = true;
IOSPayment.instance.iosStartPay('xxxxxx', orderId,
iapCallback: (isSuccess, errorMsg) {
Future.delayed(const Duration(seconds: 1), () {
TDIOSPayment.instance.isPaying = false;
});
});
isPaying设置 主要在支付期间 不让里面进入后台调用检查没完成的订单
typedef IapCallback = void Function(bool isSuccess, String errorMsg);
class IOSPayment with WidgetsBindingObserver {
listener() {
if (LoginTool.isLogin()) {
IOSPayment.instance.startSubscription();
} else {
IOSPayment.instance.stopIapListen();
}
}
/// 单例模式
IOSPayment._();
init() async {
//登录状态监听
kLoginChangeNotifier.addListener(listener);
}
static IOSPayment? _instance;
static IOSPayment get instance => _getOrCreateInstance();
static IOSPayment _getOrCreateInstance() {
if (_instance != null) {
return _instance!;
} else {
_instance = IOSPayment._();
return _instance!;
}
}
bool hasBindAppLife = false;
IapCache iapCache = IapCache();
// 应用内支付实例
final InAppPurchasePlatform iosPurchase = InAppPurchasePlatform.instance;
// iOS订阅监听
StreamSubscription<List<PurchaseDetails>>? _subscription;
/// 判断是否可以使用支付
Future<bool> isAvailable() async => await iosPurchase.isAvailable();
///监听应用生命周期变化
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
jdLog(':didChangeAppLifecycleState:$state');
if (state == AppLifecycleState.resumed) {
//从后台切换前台,界面可见
if (!isPaying) {
checkUnfinishedPayment(() => null);
}
}
}
// 开始订阅 app一启动或者登录就订阅 看是否有支付完成的订单
void startSubscription() async {
if (!LoginTool.isLogin()) {
return;
}
if (_subscription != null) {
return;
}
if (!hasBindAppLife) {
WidgetsBinding.instance.addObserver(this);
hasBindAppLife = true;
}
jdLog('iap 开始订阅 -------->');
// 支付消息订阅
Stream purchaseStream = iosPurchase.purchaseStream;
_subscription = purchaseStream.listen((purchaseDetailsList) {
purchaseDetailsList.forEach(_handleReportedPurchaseState);
}, onDone: () {
jdLog('iap 开始订阅onDone -------->');
// _subscription?.cancel();
jdLog("onDone");
}, onError: (error) {
jdLog("error");
}) as StreamSubscription<List<PurchaseDetails>>?;
}
bool isPaying = false;
IapCallback? currentIapCallBack;
// 开始支付 productId : 商品在苹果后台设置的id orderId: 我们服务端生产的id
void iosStartPay(String productId, String orderId,
{IapCallback? iapCallback}) async {
if (!LoginTool.isLogin() ||
productId.isEmpty ||
orderId.isEmpty ||
LoginTool.instance.userId.isEmpty) {
FileWriteLog.log('iap iosStartPay 参数不对 或没登录 -------->');
iapCallback?.call(false, '参数不对 或没登录');
return;
}
if (!await isAvailable()) {
bool hasNet = await isNetWorkConnected();
if (hasNet) {
alertCanNotBuyDialog();
} else {
TDToast.showToast("没有网络");
}
iapCallback?.call(false, '无法支付');
return;
}
checkUnfinishedPayment(() async {
// 获取商品列表
ProductDetailsResponse appStoreProducts =
await iosPurchase.queryProductDetails({productId});
if (appStoreProducts.productDetails.isNotEmpty) {
// 发起支付 消耗商品的支付
FileWriteLog.log('iap 发起支付productId --------> $productId');
iosPurchase
.buyConsumable(
purchaseParam: PurchaseParam(
productDetails: appStoreProducts.productDetails.first,
applicationUserName: LoginTool.instance.userId,
),
)
.then((value) {
if (value) {
currentIapCallBack = iapCallback;
// 只要能发起,就写入
// status 0 开始支付 1 苹果回调失败 2 苹果支付成功 3服务器校验失败
Map dataMap = {
IapCache.kOrderId: orderId,
IapCache.kProductId: productId,
IapCache.kStatus: 0,
IapCache.kUserId: LoginTool.instance.userId,
};
FileWriteLog.log('iap 开始支付保存的dataMap-----> ${dataMap.toString()}');
iapCache.saveOrUpdateWithMap(dataMap);
}
}).catchError((err) {
FileWriteLog.log('当前商品您有未完成的交易,请等待iOS系统核验后再次发起购买。');
iapCallback?.call(false, '当前商品您有未完成的交易,请等待iOS系统核验后再次发起购买。');
jdLog(err);
});
} else {
iapCallback?.call(false, '没有这个商品');
TDToast.showToast("查询商品信息失败");
}
});
}
//监听状态回调
Future<void> _handleReportedPurchaseState(
AppStorePurchaseDetails purchaseDetail) async {
FileWriteLog.log(
'iap 监听状态回调 purchaseDetail.status --------> ${purchaseDetail.status}');
if (purchaseDetail.status == PurchaseStatus.pending) {
// 有订单开始支付
FileWriteLog.log(
'iap 监听状态回调 有订单开始支付productID--------> ${purchaseDetail.productID}');
JDLoadingTool.showLoading();
} else {
if (purchaseDetail.status == PurchaseStatus.error) {
//错误
FileWriteLog.log(
'iap 监听状态回调 错误1 productID--------> ${purchaseDetail.productID}');
finishTransaction(purchaseDetail);
currentIapCallBack?.call(false, '苹果支付失败');
} else if (purchaseDetail.status == PurchaseStatus.canceled) {
/// 取消订单
FileWriteLog.log(
'iap 监听状态回调 取消订单 productID--------> ${purchaseDetail.productID}');
currentIapCallBack?.call(false, '取消订单');
finishTransaction(purchaseDetail);
} else if (purchaseDetail.status == PurchaseStatus.purchased ||
purchaseDetail.status == PurchaseStatus.restored) {
FileWriteLog.log(
'iap 监听状态回调 支付完成 productID--------> ${purchaseDetail.productID}');
Map? resultMap =
await iapCache.findWithProductID(purchaseDetail.productID);
if (resultMap != null) {
FileWriteLog.log(
'iap 缓存resultMap != null productID--------> ${purchaseDetail.productID}');
resultMap[IapCache.kServerVerificationData] =
purchaseDetail.verificationData.serverVerificationData;
resultMap[IapCache.kTransactionDate] = purchaseDetail.transactionDate;
resultMap[IapCache.kPurchaseID] = purchaseDetail.purchaseID ?? "";
resultMap[IapCache.kStatus] = 2;
iapCache.saveOrUpdateWithMap(resultMap);
FileWriteLog.log(
'iap 支付成功保存的resultMap-----> ${resultMap.toString()}');
//已经购买 purchaseID是苹果服务器的订单id transactionIdentifier
// if (purchaseDetail.applicationUsername.isEmptyNullAble) {
// FileWriteLog.log("applicationUsername null ");
// /*
// 如果某个transaction支付成功但是并没有调用finishTransaction去完成这个交易的时候,
// 下次启动App重新监听支付队列的时候会重新调用paymentQueue:updatedTransactions:重新获取到未完成的交易,
// 这个时候获取applicationUsername会出现nil的情况
//
// 目前可能场景是用户只尝试充值了一笔,收到苹果两次支付回调
// 比如:
// 1、不在常用区域网络或长时间未使用IAP, 则需要进行短信校验. 此时会有两次支付回调
// 2、如果在支付进行时, 我们将App进程销毁, 支付完成再启动App, 也会有连续两次回调
// 备注:第一次回调applicationUsername有值,第二次回调applicationUsername为空
// */
// }
String orderId = resultMap.getStringNotNull(IapCache.kOrderId) ?? '';
if (orderId.isNotEmpty) {
FileWriteLog.log('iap 调用后台接口,发放商品orderId-----> $orderId');
/// 调用后台接口,发放商品
bool success =
await requestVerifyToServer(purchaseDetail, orderId: orderId);
if (success) {
FileWriteLog.log('iap 服务器验证通过-----> ');
deliverProduct(purchaseDetail);
currentIapCallBack?.call(true, '');
finishTransaction(purchaseDetail);
} else {
// 重试几次强制删除 重新请求 或者删除
retryOrFinishPurchase(purchaseDetail, orderId, (success) {
if (success) {
currentIapCallBack?.call(true, '');
finishTransaction(purchaseDetail);
deliverProduct(purchaseDetail);
} else {
int? retryCount = resultMap[IapCache.kRetryCount];
retryCount ??= 0;
if (retryCount >= 5) {
finishTransaction(purchaseDetail);
HintDialog(
title: '服务器校验失败',
content: '请联系客服',
rightButtonFunction: (context) {
//上传日志
},
).show(PreInit.currentContext);
} else {
int currentCount = retryCount++;
resultMap[IapCache.kRetryCount] = currentCount;
iapCache.saveOrUpdateWithMap(resultMap);
HintDialog(
title: '服务器校验失败',
content: '请确保手机网络良好 杀死app后重启app',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
}
}
});
}
} else {
// 没查到订单id
//本地数据没找到 服务端校验
noLocalDataSendServer(purchaseDetail, (success) {
if (success) {
currentIapCallBack?.call(true, '');
deliverProduct(purchaseDetail);
FileWriteLog.log(
'iap finishTransaction222 productID--------> ${purchaseDetail.productID}');
finishTransaction(purchaseDetail);
} else {
currentIapCallBack?.call(false, '');
int? retryCount = resultMap[IapCache.kRetryCount];
retryCount ??= 0;
if (retryCount >= 5) {
finishTransaction(purchaseDetail);
HintDialog(
title: '服务器校验失败',
content: '请联系客服',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
} else {
int currentCount = retryCount++;
resultMap[IapCache.kRetryCount] = currentCount;
iapCache.saveOrUpdateWithMap(resultMap);
HintDialog(
title: '服务器校验失败',
content: '请确保手机网络良好 杀死app后重启app',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
}
FileWriteLog.log(
'iap 本地数据没找到 服务端校验失败 productID--------> ${purchaseDetail.productID}');
}
});
}
} else {
//本地数据没找到 服务端校验
FileWriteLog.log(
'iap 本地数据没找到 服务端校验 productID--------> ${purchaseDetail.productID}');
noLocalDataSendServer(purchaseDetail, (success) {
if (success) {
FileWriteLog.log(
'iap finishTransaction333 productID--------> ${purchaseDetail.productID}');
currentIapCallBack?.call(true, '');
finishTransaction(purchaseDetail);
deliverProduct(purchaseDetail);
} else {
currentIapCallBack?.call(false, '服务端校验失败');
HintDialog(
title: '服务器校验失败',
content: '请联系客服',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
FileWriteLog.log(
'iap 本地数据没找到 服务端校验失败 productID--------> ${purchaseDetail.productID}');
}
});
}
}
}
}
// 支付成功 发送商品
deliverProduct(PurchaseDetails purchaseDetails) {
TDToast.showToast('支付成功');
}
//处理校验失败逻辑 重试还是关闭
retryOrFinishPurchase(PurchaseDetails purchaseDetails, String? orderId,
Function(bool success) complete) async {
bool hasNet = await isNetWorkConnected();
if (hasNet) {
if (orderId.isNotEmptyNullAble) {
bool result = await requestVerifyToServer(purchaseDetails,
orderId: orderId ?? "");
complete(result);
} else {
noLocalDataSendServer(purchaseDetails, (success) {
complete(success);
});
}
} else {
TDToast.showToast('校验支付失败');
complete(false);
}
}
// 本地没数据 发送服务端校验 可以加个参数
noLocalDataSendServer(
PurchaseDetails purchaseDetail, Function(bool success)? complete) async {
bool success = await requestVerifyToServer(purchaseDetail);
complete?.call(success);
}
//向服务器请求校验
Future<bool> requestVerifyToServer(PurchaseDetails purchaseDetail,
{String? orderId}) async {
/*
orderId,
purchaseID //苹果的订单id
purchaseDetail.productID,
purchaseDetail.verificationData.serverVerificationData,
purchaseDetail.transactionDate ?? ""
*/
// 成功后删除 已经发送过商品 也是finish
// finalTransaction(purchaseDetail);
return Future<bool>.value(true);
}
//查询支付完成 没向服务端校验订单
void checkUnfinishedPayment(Function() complete) async {
if (LoginTool.isNotLogin()) {
complete();
return;
}
SKPaymentQueueWrapper()
.transactions()
.then((List<SKPaymentTransactionWrapper> values) async {
if (values.isNotEmpty) {
for (var element in values) {
if (element.transactionState ==
SKPaymentTransactionStateWrapper.purchased) {
String productId = element.payment.productIdentifier;
String receiptData = await SKReceiptManager.retrieveReceiptData();
AppStorePurchaseDetails detail =
AppStorePurchaseDetails.fromSKTransaction(element, receiptData);
// detail.applicationUsername = element.payment.applicationUsername;
FileWriteLog.log('iap 有没完成的交易productID---->${detail.productID}');
if (element.payment.applicationUsername?.isEmpty ?? false) {
FileWriteLog.log('iap---> applicationUsername会出现nil');
// SKPaymentQueueWrapper().finishTransaction(element);
// deleteWithProductID(productId);
// complete();
}
iapCache
.findWithProductID(element.payment.productIdentifier)
.then((Map? resultMap) {
if (resultMap != null && resultMap.keys.isNotEmpty) {
requestVerifyToServer(detail,
orderId: resultMap[IapCache.kOrderId])
.then((value) {
if (value) {
FileWriteLog.log(
'iap 重新校验成功 finishTransaction444 -------->');
finishTransaction(detail);
complete();
} else {
FileWriteLog.log('iap 重新校验失败11 -------->');
retryOrFinishPurchase(detail, resultMap[IapCache.kOrderId],
(success) {
if (success) {
FileWriteLog.log(
'iap 重新校验成6666 inishTransaction-------->');
deliverProduct(detail);
finishTransaction(detail);
} else {
int? retryCount = resultMap[IapCache.kRetryCount];
retryCount ??= 0;
if (retryCount >= 5) {
finishTransaction(detail);
HintDialog(
title: '服务器校验失败',
content: '请联系客服',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
} else {
int currentCount = retryCount++;
resultMap[IapCache.kRetryCount] = currentCount;
iapCache.saveOrUpdateWithMap(resultMap);
HintDialog(
title: '服务器校验失败',
content: '请确保手机网络良好 杀死app后重启app',
rightButtonFunction: (context) {},
).show(PreInit.currentContext);
}
}
complete();
});
}
});
} else {
//本地数据没找到 服务端校验
noLocalDataSendServer(detail, (success) {
if (success) {
deliverProduct(detail);
FileWriteLog.log('iap finishTransaction666 -------->');
finishTransaction(detail);
} else {
FileWriteLog.log('iap 本地数据没找到 服务端校验失败 -------->');
}
complete();
});
}
});
}
}
} else {
complete();
}
});
}
// 关闭交易
void finishTransaction(PurchaseDetails purchaseDetails) async {
await iosPurchase.completePurchase(purchaseDetails);
iapCache.deleteWithProductID(purchaseDetails.productID);
JDLoadingTool.dismissLoading();
FileWriteLog.log('iap 关闭交易 finishTransaction -------->');
}
// 退出登录 关闭监听
stopIapListen() async {
_subscription?.cancel();
_subscription = null;
if (hasBindAppLife) {
WidgetsBinding.instance.removeObserver(this);
hasBindAppLife = false;
}
}
void alertCanNotBuyDialog() {
showDialog(
barrierDismissible: false, //表示点击灰色背景的时候是否消失弹出框
context: PreInit.currentContext,
builder: (context) {
return AlertDialog(
title: const Text("访问受限"),
content: const Text(
"你的手机关闭了“应用内购买”,请在“设置-屏幕使用时间-内容和因素访问限制”里重新打开该选项后尝试。"),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop("ok"); //点击按钮让AlertDialog消失
},
child: const Text("确定")),
],
);
});
}
/// 判断网络是否连接
Future<bool> isNetWorkConnected() async {
var connectResult = await (Connectivity().checkConnectivity());
return connectResult != ConnectivityResult.none;
}
}
demo地址在https://gitee.com/kuaipai/my_app.git
里,你可以下载下来参考