feat: 全局监听iap,在home初始化。避免过早处理,升级dart和flutter,支持storekit插件,强制指定storekit2版本

This commit is contained in:
2025-12-17 16:47:35 -08:00
parent 743631c4ce
commit 05d2c71cd0
10 changed files with 271 additions and 161 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ abstract class Api {
static const String kr_getPublicPaymentMethods = "/v1/public/payment/methods";
static const String kr_attachAppleIapTransaction =
"/v1/public/iap/apple/transactions/attach_by_id";
"/v1/public/iap/apple/transactions/attach";
static const String kr_restoreAppleIap = "/v1/public/iap/apple/restore";
/// 获取用户信息(用于获取邀请码等)
@@ -332,9 +332,9 @@ class KRSubscribeApi {
}
Future<Either<HttpError, bool>> kr_attachAppleIapTransaction(
String transactionId, String orderNo) async {
String jwsData, String orderNo) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['transaction_id'] = transactionId;
data['signed_transaction_jws'] = jwsData;
data['order_no'] = orderNo;
BaseResponse<KRStatus> baseResponse =
@@ -0,0 +1,17 @@
import 'package:flutter_keychain/flutter_keychain.dart';
class IAPPendingOrderService {
static const _key = 'hi_iap_pending_order_no';
static Future<void> setPendingOrderNo(String orderNo) async {
await FlutterKeychain.put(key: _key, value: orderNo);
}
static Future<String?> getPendingOrderNo() async {
return await FlutterKeychain.get(key: _key);
}
static Future<void> clearPendingOrderNo() async {
await FlutterKeychain.remove(key: _key);
}
}
+175
View File
@@ -0,0 +1,175 @@
import 'dart:async';
import 'dart:io';
import 'package:get/get.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:kaer_with_panels/app/services/iap/iap_pending_order_service.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_subscribe_api.dart';
import 'package:kaer_with_panels/app/routes/app_pages.dart';
class KRIAPService extends GetxService {
static KRIAPService get instance => Get.find<KRIAPService>();
final KRSubscribeApi _kr_subscribeApi = KRSubscribeApi();
final InAppPurchase _iap = InAppPurchase.instance;
StreamSubscription<List<PurchaseDetails>>? _subscription;
// 增加标志位,防止多次初始化
bool _hasStarted = false;
@override
void onInit() {
super.onInit();
// 不再这里自动启动,等待 Home 页面手动开启
}
/// 【外部调用】在 Home 页面加载完成后调用此方法
void setup() {
if (!Platform.isIOS) return;
if (_hasStarted) return;
_hasStarted = true;
_startGlobalListening();
}
void _startGlobalListening() {
print('💳 [IAPService] ========== 🚀 全局监听流已就绪 ==========');
// 确保不会重复监听
_subscription?.cancel();
_subscription = _iap.purchaseStream.listen(
_handleIapUpdates,
onDone: () => _subscription?.cancel(),
onError: (error) {
print('💳 [IAPService] ❌ Stream Error: $error');
// 如果发生错误,可以尝试重启流
},
);
}
// 1. 监听回调:只负责状态分流
void _handleIapUpdates(List<PurchaseDetails> purchases) async {
final pendingOrderNo = await IAPPendingOrderService.getPendingOrderNo();
// 如果没有本地挂起的订单号,说明不是通过 App 正常发起的支付(或是已处理完的补单)
if (pendingOrderNo == null || pendingOrderNo.isEmpty) return;
for (final p in purchases) {
print('🔄 [IAPService] 处理状态: ${p.status} | ID: ${p.purchaseID}');
if (p.status == PurchaseStatus.pending) continue;
if (p.status == PurchaseStatus.error) {
if (p.pendingCompletePurchase) await _iap.completePurchase(p);
KRCommonUtil.kr_showToast('购买失败');
KRCommonUtil.kr_hideLoading();
continue;
}
if (p.status == PurchaseStatus.purchased || p.status == PurchaseStatus.restored) {
// 核心:跳转到统一验证逻辑
_verifyWithServer(p, pendingOrderNo);
}
}
}
// 2. 统一验证逻辑:负责与后端交互、补单、UI 引导
Future<void> _verifyWithServer(PurchaseDetails p, String orderNo) async {
final String jwsData = p.verificationData.serverVerificationData;
if (jwsData.isEmpty) {
print('❌ [IAPService] JWS 数据为空,跳过验证');
if (p.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(p);
}
KRCommonUtil.kr_hideLoading();
return;
}
KRCommonUtil.kr_showLoading(message: '正在激活会员权益...');
bool ok = false;
try {
// 根据状态选择对应的后端接口
if (p.status == PurchaseStatus.purchased || p.status == PurchaseStatus.restored) {
final either = await _kr_subscribeApi.kr_attachAppleIapTransaction(jwsData, orderNo);
either.fold((e) => print('❌ [IAPService] 支付激活失败: ${e.msg}'), (v) => ok = true);
} else {
// 注意:restore 接口如果后端要求传 JWS,这里保持一致
// final either = await _kr_subscribeApi.kr_restoreAppleIapTransaction(jwsData, orderNo);
// either.fold((e) => print('❌ [IAPService] 恢复激活失败: ${e.msg}'), (v) => ok = true);
}
} catch (e) {
print('🌐 [IAPService] 网络请求异常');
}
if (ok) {
await _finalizeTransaction(p, orderNo);
} else {
_handleVerificationFailure(p, orderNo);
}
}
// 3. 验证成功后的收尾工作
Future<void> _finalizeTransaction(PurchaseDetails p, String orderNo) async {
print('✅ [IAPService] 验证成功,清理流程');
// I. 结束苹果事务(关键:防止重复推送)
if (p.pendingCompletePurchase) {
await _iap.completePurchase(p);
}
// II. 清理本地拦截订单号
await IAPPendingOrderService.clearPendingOrderNo();
KRCommonUtil.kr_hideLoading();
// III. 跳转结果页
// 如果已经在结果页了就不跳,或者根据路由判断
if (!Get.currentRoute.contains(Routes.KR_ORDER_STATUS)) {
Get.offNamed(
Routes.KR_ORDER_STATUS,
arguments: {
'order': orderNo,
'payment_type': 'apple_iap',
'checkout_type': 'ipa',
},
);
}
}
// 4. 验证失败后的重试引导
void _handleVerificationFailure(PurchaseDetails p, String orderNo) {
KRCommonUtil.kr_hideLoading();
HIDialog.show(
title: '激活失败',
message: '您的支付已成功,但由于网络原因未能开启权益。',
confirmText: '重试激活',
cancelText: '关闭',
barrierDismissible: false,
preventBackDismiss: true,
onConfirm: () {
// 💡 重点:直接重新调用验证逻辑,不重复触发监听流
_verifyWithServer(p, orderNo);
},
);
}
// 5. 手动触发(用于 UI 上的恢复购买按钮)
Future<void> retryUnfinishedTransactions() async {
try {
KRCommonUtil.kr_showLoading(message: '正在查找未完成订单...');
await _iap.restorePurchases();
// 如果 5 秒后没触发流,隐藏 loading
Future.delayed(const Duration(seconds: 5), () => KRCommonUtil.kr_hideLoading());
} catch (e) {
KRCommonUtil.kr_hideLoading();
KRCommonUtil.kr_showToast("请求失败,请稍后重试");
}
}
@override
void onClose() {
_subscription?.cancel();
super.onClose();
}
}