feat: 增加flutter_keychain来缓存订单号
This commit is contained in:
+123
-170
@@ -21,6 +21,8 @@ import 'package:flutter_stripe/flutter_stripe.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
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';
|
||||
|
||||
/// 会员购买控制器
|
||||
/// 负责处理会员套餐选择、支付方式选择和订阅流程
|
||||
@@ -53,6 +55,9 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
/// 当前余额
|
||||
RxInt _kr_balance = 0.obs;
|
||||
|
||||
// -------------------- IAP 全局监听 --------------------
|
||||
late StreamSubscription<List<PurchaseDetails>> _iapSubscription;
|
||||
|
||||
@override
|
||||
void onInit() async {
|
||||
super.onInit();
|
||||
@@ -80,11 +85,22 @@ 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();
|
||||
}
|
||||
@@ -445,6 +461,24 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
print('开始订阅流程,选定支付');
|
||||
kr_errorMessage.value = '';
|
||||
|
||||
// ✅ 仅在 iOS 检查未完成订单
|
||||
if (Platform.isIOS) {
|
||||
final existing = await IAPPendingOrderService.getPendingOrderNo();
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
await HIDialog.show(
|
||||
title: '存在未完成订单',
|
||||
message: '检测到未完成订单,需要恢复购买以完成订阅。',
|
||||
confirmText: '恢复购买',
|
||||
barrierDismissible: false,
|
||||
preventBackDismiss: true,
|
||||
onConfirm: () {
|
||||
kr_restorePurchases(); // 恢复未完成订单
|
||||
},
|
||||
);
|
||||
return; // 等待恢复完成,不再继续发起新订阅
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await kr_processPurchaseAndCheckout();
|
||||
} catch (e) {
|
||||
@@ -472,7 +506,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
/// 处理购买和结账流程
|
||||
Future<void> kr_processPurchaseAndCheckout() async {
|
||||
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
|
||||
// =========================================================================
|
||||
// =========================================================================
|
||||
// 🔽 支付方式选择优化:iOS 优先使用 Stripe 🔽
|
||||
// =========================================================================
|
||||
final isIOS = Platform.isIOS;
|
||||
@@ -936,7 +970,7 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
final KRIpaPayment iapParams = (checkoutResponse.ipa == null ||
|
||||
(checkoutResponse.ipa?.productId.isEmpty ?? true))
|
||||
? KRIpaPayment(
|
||||
productId: 'com.hifastvpn.vip.day${kr_getSelectedQuantity()}')
|
||||
productId: 'com.hifastvpn.plan.day${kr_getSelectedQuantity()}')
|
||||
: checkoutResponse.ipa!;
|
||||
|
||||
final iap = InAppPurchase.instance;
|
||||
@@ -965,161 +999,27 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
}
|
||||
final product = productDetailsResponse.productDetails.first;
|
||||
|
||||
final kr_userId = KRAppRunData.getInstance().kr_userId.value;
|
||||
final purchaseParam = PurchaseParam(
|
||||
productDetails: product,
|
||||
applicationUserName: iapParams.applicationUsername,
|
||||
applicationUserName: 'uid_${kr_userId}|orderNo_${orderNo}',
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// 假设 ProductDetails 已成功获取,并已构造 purchaseParam。
|
||||
// ProductDetails 已成功获取,并已构造 purchaseParam。
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
late StreamSubscription<List<PurchaseDetails>> subscription;
|
||||
|
||||
// 【1】确认购买 Stream 监听启动
|
||||
print('--- IAP 流程:购买 Stream 监听即将启动 ---');
|
||||
|
||||
subscription = iap.purchaseStream.listen(
|
||||
(purchases) async {
|
||||
print('IAP Stream 收到 ${purchases.length} 个交易事件');
|
||||
|
||||
for (final p in purchases) {
|
||||
if (p.productID != iapParams.productId) continue;
|
||||
|
||||
final status = p.status.toString().split('.').last;
|
||||
print('处理交易:产品ID ${p.productID}, 状态: $status');
|
||||
|
||||
// =======================
|
||||
// 1️⃣ Pending 状态
|
||||
// =======================
|
||||
if (p.status == PurchaseStatus.pending) {
|
||||
print('交易状态:Pending,等待 App Store...');
|
||||
continue;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// 2️⃣ Error 状态
|
||||
// =======================
|
||||
if (p.status == PurchaseStatus.error) {
|
||||
print('交易失败');
|
||||
|
||||
if (p.error != null) {
|
||||
print('IAP 错误 code: ${p.error!.code}');
|
||||
print('IAP 错误 message: ${p.error!.message}');
|
||||
}
|
||||
|
||||
// ❗只在需要时 complete
|
||||
if (p.pendingCompletePurchase) {
|
||||
await iap.completePurchase(p);
|
||||
print('Error 状态交易已 complete');
|
||||
}
|
||||
|
||||
KRCommonUtil.kr_showToast('购买失败');
|
||||
continue;
|
||||
}
|
||||
|
||||
// =======================
|
||||
// 3️⃣ Purchased / Restored
|
||||
// =======================
|
||||
if (p.status == PurchaseStatus.purchased ||
|
||||
p.status == PurchaseStatus.restored) {
|
||||
|
||||
// -----------------------
|
||||
// ① 打印凭证(保留你原来的)
|
||||
// -----------------------
|
||||
final receiptData = p.verificationData.serverVerificationData;
|
||||
final transactionId = p.purchaseID;
|
||||
final productID = p.productID;
|
||||
|
||||
print('***************** IAP 交易凭证数据 *****************');
|
||||
print('产品 ID (ProductID): $productID');
|
||||
print('交易 ID (PurchaseID): $transactionId');
|
||||
print('Receipt(Base64):');
|
||||
print(receiptData);
|
||||
print('**************************************************');
|
||||
|
||||
// -----------------------
|
||||
// ② 先 attach 到你服务器
|
||||
// -----------------------
|
||||
final attachEither =
|
||||
await _kr_subscribeApi.kr_attachAppleIapTransaction(
|
||||
transactionId!,
|
||||
orderNo,
|
||||
);
|
||||
|
||||
bool attachSuccess = false;
|
||||
|
||||
attachEither.fold(
|
||||
(error) {
|
||||
print('IAP attach失败 code: ${error.code}');
|
||||
print('IAP attach失败 msg: ${error.msg}');
|
||||
},
|
||||
(ok) {
|
||||
attachSuccess = true;
|
||||
print('IAP attach成功: $ok');
|
||||
},
|
||||
);
|
||||
|
||||
// ❗attach 失败:不要 complete,让 Apple 重发
|
||||
if (!attachSuccess) {
|
||||
print('attach 失败,暂不 complete,等待重试');
|
||||
continue;
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// ③ attach 成功后,才 complete
|
||||
// -----------------------
|
||||
if (p.pendingCompletePurchase) {
|
||||
await iap.completePurchase(p);
|
||||
print('交易已 completePurchase');
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// ④ 页面跳转(保留)
|
||||
// -----------------------
|
||||
Get.toNamed(
|
||||
Routes.KR_ORDER_STATUS,
|
||||
arguments: {
|
||||
'order': orderNo,
|
||||
'payment_type': paymentPlatform,
|
||||
'checkout_type': 'ipa',
|
||||
},
|
||||
);
|
||||
|
||||
// ❌ 不要在这里 cancel subscription
|
||||
// Stream 需要保持监听,用于恢复交易
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
print('--- IAP Stream 监听发生错误: $error ---');
|
||||
},
|
||||
onDone: () {
|
||||
print('--- IAP Stream 已关闭 ---');
|
||||
},
|
||||
);
|
||||
|
||||
// 🔴 关键:buy 之前先清理
|
||||
await _cleanPendingPurchase(iap, iapParams.productId);
|
||||
|
||||
print('--- IAP 流程:即将调用 buyNonConsumable ---');
|
||||
|
||||
await IAPPendingOrderService.setPendingOrderNo(orderNo);
|
||||
KRCommonUtil.kr_showLoading();
|
||||
try {
|
||||
final result =
|
||||
await iap.buyNonConsumable(purchaseParam: purchaseParam);
|
||||
print('--- IAP 流程:buyNonConsumable 调用返回。返回值:$result ---');
|
||||
|
||||
if (!result) {
|
||||
// 如果返回 false,说明本地请求失败
|
||||
print('IAP 购买请求本地失败:请检查 PurchaseParam 或设备设置。');
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
KRCommonUtil.kr_showToast('购买请求失败,请重试');
|
||||
// 注意:如果 Stream 此时没有收到 error 事件,这里可能需要手动清理。
|
||||
}
|
||||
} catch (e) {
|
||||
// 捕获任何意想不到的、未通过 Stream 报告的本地异常
|
||||
print('--- IAP 流程:buyNonConsumable 调用发生异常:$e ---');
|
||||
subscription.cancel();
|
||||
KRCommonUtil.kr_showToast('购买服务异常,请重试');
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
KRCommonUtil.kr_showToast('购买服务异常,请重试$e');
|
||||
}
|
||||
} else {
|
||||
print('⚠️ 未知的支付类型: ${checkoutResponse.type}');
|
||||
@@ -1129,40 +1029,93 @@ class KRPurchaseMembershipController extends GetxController {
|
||||
},
|
||||
);
|
||||
}
|
||||
/// 🔴【关键修复】在发起购买前,清理同一商品的 pending 交易
|
||||
Future<void> _cleanPendingPurchase(
|
||||
InAppPurchase iap,
|
||||
String productId,
|
||||
) async {
|
||||
print('🔧 IAP 清理 pending 交易开始');
|
||||
|
||||
final completer = Completer<void>();
|
||||
// -------------------- 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;
|
||||
|
||||
late StreamSubscription<List<PurchaseDetails>> tempSub;
|
||||
for (final p in purchases) {
|
||||
print('p.status ${p.status}');
|
||||
if (p.status == PurchaseStatus.pending) continue;
|
||||
|
||||
tempSub = iap.purchaseStream.listen((purchases) async {
|
||||
for (final p in purchases) {
|
||||
if (p.productID != productId) continue;
|
||||
|
||||
if (p.pendingCompletePurchase) {
|
||||
print('🔧 发现 pending 交易,completePurchase');
|
||||
await iap.completePurchase(p);
|
||||
}
|
||||
if (p.status == PurchaseStatus.error) {
|
||||
if (p.pendingCompletePurchase) await iap.completePurchase(p);
|
||||
KRCommonUtil.kr_showToast('购买失败');
|
||||
continue;
|
||||
}
|
||||
|
||||
// 👇 只跑一次就够了
|
||||
await tempSub.cancel();
|
||||
completer.complete();
|
||||
});
|
||||
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);
|
||||
// }
|
||||
|
||||
// 给 StoreKit 时间推送历史交易
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
// ❌ 服务端失败,必须给用户反馈
|
||||
if (!ok) {
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
HIDialog.show(
|
||||
title: '激活失败,请稍后重试',
|
||||
message: '检测到未完成订单,需要恢复购买以完成订阅。',
|
||||
confirmText: '恢复购买',
|
||||
cancelText: '关闭',
|
||||
barrierDismissible: false,
|
||||
preventBackDismiss: true,
|
||||
onConfirm: () {
|
||||
kr_restorePurchases(); // 恢复未完成订单
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!completer.isCompleted) {
|
||||
await tempSub.cancel();
|
||||
completer.complete();
|
||||
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',
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print('🔧 IAP 清理 pending 交易完成');
|
||||
Future<void> kr_restorePurchases({bool isShowLoading = true}) async {
|
||||
print('重试');
|
||||
if(isShowLoading) {
|
||||
KRCommonUtil.kr_showLoading();
|
||||
}
|
||||
final iap = InAppPurchase.instance;
|
||||
await iap.restorePurchases();
|
||||
|
||||
if(isShowLoading) {
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
}
|
||||
}
|
||||
// 1. 新增恢复购买点击计数器
|
||||
int _restoreClickCount = 0;
|
||||
Future<void> clearPendingOrderNo() async {
|
||||
_restoreClickCount++;
|
||||
// 2. 检查点击次数是否超过3次
|
||||
if (_restoreClickCount > 3) {
|
||||
print('恢复购买点击次数超过3次,清理待处理订单...');
|
||||
await IAPPendingOrderService.clearPendingOrderNo();
|
||||
_restoreClickCount = 0; // 重置计数器
|
||||
KRCommonUtil.kr_hideLoading();
|
||||
KRCommonUtil.kr_showToast('已清理缓存,请重新尝试购买');
|
||||
return; // 终止后续操作
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user