feat: 全局监听iap,在home初始化。避免过早处理,升级dart和flutter,支持storekit插件,强制指定storekit2版本
This commit is contained in:
@@ -37,6 +37,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_init_log_collector.dart'; // 🔧 新增:导入日志收集器
|
||||
import 'package:kaer_with_panels/app/utils/kr_latency_tester.dart'; // 🔧 新增:导入真实延迟测试工具
|
||||
import 'package:kaer_with_panels/app/utils/account_guard.dart';
|
||||
import 'package:kaer_with_panels/app/services/iap/iap_service.dart';
|
||||
|
||||
|
||||
class KRHomeController extends GetxController with WidgetsBindingObserver {
|
||||
// 🔧 新增:日志收集器实例
|
||||
|
||||
+18
-86
@@ -23,6 +23,7 @@ import 'dart:async';
|
||||
import 'package:kaer_with_panels/app/model/response/kr_purchase_order_no.dart';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/services/iap/iap_pending_order_service.dart';
|
||||
import 'package:kaer_with_panels/app/services/iap/iap_service.dart';
|
||||
|
||||
/// 会员购买控制器
|
||||
/// 负责处理会员套餐选择、支付方式选择和订阅流程
|
||||
@@ -55,9 +56,6 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
/// 当前余额
|
||||
RxInt _kr_balance = 0.obs;
|
||||
|
||||
// -------------------- IAP 全局监听 --------------------
|
||||
late StreamSubscription<List<PurchaseDetails>> _iapSubscription;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
@@ -85,22 +83,11 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
print('💳 [PurchaseMembership] ❌ 写入调试日志失败: $e');
|
||||
}
|
||||
|
||||
// 初始化 IAP 全局监听
|
||||
if (Platform.isIOS) {
|
||||
_iapSubscription = InAppPurchase.instance.purchaseStream.listen(
|
||||
_handleIapUpdates,
|
||||
onError: (error) {
|
||||
print('IAP Stream Error: $error');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
kr_initializeData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_iapSubscription.cancel();
|
||||
_kr_eventWorker?.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
@@ -461,21 +448,26 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
print('开始订阅流程,选定支付');
|
||||
kr_errorMessage.value = '';
|
||||
|
||||
// ✅ 仅在 iOS 检查未完成订单
|
||||
// ✅ 1. iOS 核心拦截:检测本地是否有“挂起”的订单号
|
||||
if (Platform.isIOS) {
|
||||
final existing = await IAPPendingOrderService.getPendingOrderNo();
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
final existingOrderNo = await IAPPendingOrderService.getPendingOrderNo();
|
||||
|
||||
if (existingOrderNo != null && existingOrderNo.isNotEmpty) {
|
||||
print('🛑 [IAP] 拦截成功:发现未结单 $existingOrderNo');
|
||||
|
||||
await HIDialog.show(
|
||||
title: '存在未完成订单',
|
||||
message: '检测到未完成订单,需要恢复购买以完成订阅。',
|
||||
confirmText: '恢复购买',
|
||||
title: '检测到待激活订单',
|
||||
message: '您有一笔订单已支付成功,但尚未同步会员权益。请点击“立即激活”完成同步。',
|
||||
confirmText: '立即激活',
|
||||
cancelText: '取消',
|
||||
barrierDismissible: false,
|
||||
preventBackDismiss: true,
|
||||
onConfirm: () {
|
||||
kr_restorePurchases(); // 恢复未完成订单
|
||||
// 🚀 调用全局 Service 的重试逻辑
|
||||
// 这会触发苹果重新推送 PurchaseStream,进而走验证流程
|
||||
KRIAPService.instance.retryUnfinishedTransactions();
|
||||
},
|
||||
);
|
||||
return; // 等待恢复完成,不再继续发起新订阅
|
||||
return; // 🛑 关键:必须 return,阻止创建重复的新订单
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,9 +1000,10 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
// ----------------------------------------------------------------------
|
||||
// ProductDetails 已成功获取,并已构造 purchaseParam。
|
||||
// ----------------------------------------------------------------------
|
||||
await IAPPendingOrderService.setPendingOrderNo(orderNo);
|
||||
KRCommonUtil.kr_showLoading();
|
||||
|
||||
try {
|
||||
await IAPPendingOrderService.setPendingOrderNo(orderNo);
|
||||
KRCommonUtil.kr_showLoading();
|
||||
final result =
|
||||
await iap.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
if (!result) {
|
||||
@@ -1030,67 +1023,6 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------- IAP 统一处理 --------------------
|
||||
void _handleIapUpdates(List<PurchaseDetails> purchases) async {
|
||||
final iap = InAppPurchase.instance;
|
||||
final pendingOrderNo = await IAPPendingOrderService.getPendingOrderNo();
|
||||
print('p.pendingOrderNo ${pendingOrderNo}');
|
||||
if (pendingOrderNo == null || pendingOrderNo.isEmpty) return;
|
||||
|
||||
for (final p in purchases) {
|
||||
print('p.status ${p.status}');
|
||||
if (p.status == PurchaseStatus.pending) continue;
|
||||
|
||||
if (p.status == PurchaseStatus.error) {
|
||||
if (p.pendingCompletePurchase) await iap.completePurchase(p);
|
||||
KRCommonUtil.kr_showToast('购买失败');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (p.status == PurchaseStatus.purchased || p.status == PurchaseStatus.restored) {
|
||||
KRCommonUtil.kr_showLoading();
|
||||
bool ok = false;
|
||||
final either = await _kr_subscribeApi.kr_attachAppleIapTransaction(p.purchaseID!, pendingOrderNo);
|
||||
either.fold((e) {}, (v) => ok = true);
|
||||
// if (p.status == PurchaseStatus.purchased) {
|
||||
//
|
||||
// either.fold((e) {}, (v) => ok = true);
|
||||
// } else if (p.status == PurchaseStatus.restored) {
|
||||
// final either = await _kr_subscribeApi.kr_restoreAppleIapTransaction(p.purchaseID!, pendingOrderNo);
|
||||
// either.fold((e) {}, (v) => ok = true);
|
||||
// }
|
||||
|
||||
// ❌ 服务端失败,必须给用户反馈
|
||||
if (!ok) {
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
HIDialog.show(
|
||||
title: '激活失败,请稍后重试',
|
||||
message: '检测到未完成订单,需要恢复购买以完成订阅。',
|
||||
confirmText: '恢复购买',
|
||||
cancelText: '关闭',
|
||||
barrierDismissible: false,
|
||||
preventBackDismiss: true,
|
||||
onConfirm: () {
|
||||
kr_restorePurchases(); // 恢复未完成订单
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (p.pendingCompletePurchase) await iap.completePurchase(p);
|
||||
await IAPPendingOrderService.clearPendingOrderNo();
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
Get.toNamed(
|
||||
Routes.KR_ORDER_STATUS,
|
||||
arguments: {
|
||||
'order': pendingOrderNo,
|
||||
'payment_type': 'apple_iap',
|
||||
'checkout_type': 'ipa',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> kr_restorePurchases({bool isShowLoading = true}) async {
|
||||
print('重试');
|
||||
|
||||
@@ -19,6 +19,8 @@ import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import '../../../routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/services/iap/iap_service.dart';
|
||||
|
||||
|
||||
|
||||
/// 购买会员页面视图
|
||||
@@ -201,9 +203,9 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
||||
}
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onTap: () async{
|
||||
// 异步处理补单
|
||||
controller.kr_restorePurchases(isShowLoading: false); // 恢复未完成订单
|
||||
await KRIAPService.instance.retryUnfinishedTransactions();
|
||||
HIDialog.show(
|
||||
title: '温馨提示',
|
||||
message: '购买订单已确认,请耐心等待。若时长未到账,请联系在线客服处理。',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||
import 'package:kaer_with_panels/app/routes/app_pages.dart';
|
||||
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
|
||||
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
|
||||
import 'package:kaer_with_panels/app/services/iap/iap_service.dart';
|
||||
|
||||
Future<bool> ensureAccountExists() async {
|
||||
final app = KRAppRunData.getInstance();
|
||||
@@ -27,6 +28,7 @@ Future<bool> ensureAccountExists() async {
|
||||
return false;
|
||||
}
|
||||
|
||||
KRIAPService.instance.setup();
|
||||
var crispId = config.kr_website_id;
|
||||
var deviceLimitText = config.device_limit;
|
||||
var deviceLimit = int.tryParse(deviceLimitText) ?? 0;
|
||||
|
||||
Reference in New Issue
Block a user