feat: 增加flutter_keychain来缓存订单号
This commit is contained in:
parent
60de644637
commit
743631c4ce
@ -21,6 +21,8 @@ import 'package:flutter_stripe/flutter_stripe.dart';
|
|||||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:kaer_with_panels/app/model/response/kr_purchase_order_no.dart';
|
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;
|
RxInt _kr_balance = 0.obs;
|
||||||
|
|
||||||
|
// -------------------- IAP 全局监听 --------------------
|
||||||
|
late StreamSubscription<List<PurchaseDetails>> _iapSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() async {
|
void onInit() async {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
@ -80,11 +85,22 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
print('💳 [PurchaseMembership] ❌ 写入调试日志失败: $e');
|
print('💳 [PurchaseMembership] ❌ 写入调试日志失败: $e');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化 IAP 全局监听
|
||||||
|
if (Platform.isIOS) {
|
||||||
|
_iapSubscription = InAppPurchase.instance.purchaseStream.listen(
|
||||||
|
_handleIapUpdates,
|
||||||
|
onError: (error) {
|
||||||
|
print('IAP Stream Error: $error');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
kr_initializeData();
|
kr_initializeData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
|
_iapSubscription.cancel();
|
||||||
_kr_eventWorker?.dispose();
|
_kr_eventWorker?.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
@ -445,6 +461,24 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
print('开始订阅流程,选定支付');
|
print('开始订阅流程,选定支付');
|
||||||
kr_errorMessage.value = '';
|
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 {
|
try {
|
||||||
await kr_processPurchaseAndCheckout();
|
await kr_processPurchaseAndCheckout();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -472,7 +506,7 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
/// 处理购买和结账流程
|
/// 处理购买和结账流程
|
||||||
Future<void> kr_processPurchaseAndCheckout() async {
|
Future<void> kr_processPurchaseAndCheckout() async {
|
||||||
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
|
final selectedPlan = kr_plans[kr_selectedPlanIndex.value];
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// 🔽 支付方式选择优化:iOS 优先使用 Stripe 🔽
|
// 🔽 支付方式选择优化:iOS 优先使用 Stripe 🔽
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
final isIOS = Platform.isIOS;
|
final isIOS = Platform.isIOS;
|
||||||
@ -936,7 +970,7 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
final KRIpaPayment iapParams = (checkoutResponse.ipa == null ||
|
final KRIpaPayment iapParams = (checkoutResponse.ipa == null ||
|
||||||
(checkoutResponse.ipa?.productId.isEmpty ?? true))
|
(checkoutResponse.ipa?.productId.isEmpty ?? true))
|
||||||
? KRIpaPayment(
|
? KRIpaPayment(
|
||||||
productId: 'com.hifastvpn.vip.day${kr_getSelectedQuantity()}')
|
productId: 'com.hifastvpn.plan.day${kr_getSelectedQuantity()}')
|
||||||
: checkoutResponse.ipa!;
|
: checkoutResponse.ipa!;
|
||||||
|
|
||||||
final iap = InAppPurchase.instance;
|
final iap = InAppPurchase.instance;
|
||||||
@ -965,161 +999,27 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
}
|
}
|
||||||
final product = productDetailsResponse.productDetails.first;
|
final product = productDetailsResponse.productDetails.first;
|
||||||
|
|
||||||
|
final kr_userId = KRAppRunData.getInstance().kr_userId.value;
|
||||||
final purchaseParam = PurchaseParam(
|
final purchaseParam = PurchaseParam(
|
||||||
productDetails: product,
|
productDetails: product,
|
||||||
applicationUserName: iapParams.applicationUsername,
|
applicationUserName: 'uid_${kr_userId}|orderNo_${orderNo}',
|
||||||
);
|
);
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// 假设 ProductDetails 已成功获取,并已构造 purchaseParam。
|
// ProductDetails 已成功获取,并已构造 purchaseParam。
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
await IAPPendingOrderService.setPendingOrderNo(orderNo);
|
||||||
late StreamSubscription<List<PurchaseDetails>> subscription;
|
KRCommonUtil.kr_showLoading();
|
||||||
|
|
||||||
// 【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 ---');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result =
|
final result =
|
||||||
await iap.buyNonConsumable(purchaseParam: purchaseParam);
|
await iap.buyNonConsumable(purchaseParam: purchaseParam);
|
||||||
print('--- IAP 流程:buyNonConsumable 调用返回。返回值:$result ---');
|
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
// 如果返回 false,说明本地请求失败
|
KRCommonUtil.kr_hideLoading();
|
||||||
print('IAP 购买请求本地失败:请检查 PurchaseParam 或设备设置。');
|
|
||||||
KRCommonUtil.kr_showToast('购买请求失败,请重试');
|
KRCommonUtil.kr_showToast('购买请求失败,请重试');
|
||||||
// 注意:如果 Stream 此时没有收到 error 事件,这里可能需要手动清理。
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 捕获任何意想不到的、未通过 Stream 报告的本地异常
|
KRCommonUtil.kr_hideLoading();
|
||||||
print('--- IAP 流程:buyNonConsumable 调用发生异常:$e ---');
|
KRCommonUtil.kr_showToast('购买服务异常,请重试$e');
|
||||||
subscription.cancel();
|
|
||||||
KRCommonUtil.kr_showToast('购买服务异常,请重试');
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('⚠️ 未知的支付类型: ${checkoutResponse.type}');
|
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;
|
|
||||||
|
|
||||||
tempSub = iap.purchaseStream.listen((purchases) async {
|
|
||||||
for (final p in purchases) {
|
for (final p in purchases) {
|
||||||
if (p.productID != productId) continue;
|
print('p.status ${p.status}');
|
||||||
|
if (p.status == PurchaseStatus.pending) continue;
|
||||||
|
|
||||||
if (p.pendingCompletePurchase) {
|
if (p.status == PurchaseStatus.error) {
|
||||||
print('🔧 发现 pending 交易,completePurchase');
|
if (p.pendingCompletePurchase) await iap.completePurchase(p);
|
||||||
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 {
|
||||||
await tempSub.cancel();
|
print('重试');
|
||||||
completer.complete();
|
if(isShowLoading) {
|
||||||
});
|
KRCommonUtil.kr_showLoading();
|
||||||
|
|
||||||
// 给 StoreKit 时间推送历史交易
|
|
||||||
await Future.delayed(const Duration(milliseconds: 800));
|
|
||||||
|
|
||||||
if (!completer.isCompleted) {
|
|
||||||
await tempSub.cancel();
|
|
||||||
completer.complete();
|
|
||||||
}
|
}
|
||||||
|
final iap = InAppPurchase.instance;
|
||||||
|
await iap.restorePurchases();
|
||||||
|
|
||||||
print('🔧 IAP 清理 pending 交易完成');
|
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; // 终止后续操作
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,9 @@ import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
|
|||||||
import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart';
|
import 'package:kaer_with_panels/app/widgets/swipe/has_swipe_config.dart';
|
||||||
import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
import 'package:kaer_with_panels/app/widgets/swipe/swipe_config.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:kaer_with_panels/app/widgets/dialogs/hi_dialog.dart';
|
||||||
|
import '../../../routes/app_pages.dart';
|
||||||
|
|
||||||
|
|
||||||
/// 购买会员页面视图
|
/// 购买会员页面视图
|
||||||
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
||||||
@ -29,12 +32,22 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return HIBaseScaffold(
|
// 获取屏幕顶部的安全区域高度(状态栏高度)
|
||||||
|
final double topSafeArea = MediaQuery.of(context).padding.top;
|
||||||
|
final double buttonTopPosition = 80 + topSafeArea;
|
||||||
|
|
||||||
|
// 在最外层包裹一个 Stack
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
// 第一层:整个页面原来的内容
|
||||||
|
HIBaseScaffold(
|
||||||
showBackgroundImage: false,
|
showBackgroundImage: false,
|
||||||
title: '套餐选择',
|
title: '套餐选择',
|
||||||
subtitle: '*所有套餐均不限流量不限速度',
|
subtitle: '*所有套餐均不限流量不限速度',
|
||||||
topContentAreaHeight: 110,
|
topContentAreaHeight: 110,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
|
// 允许内部的 slogan 等元素超出边界
|
||||||
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
@ -56,7 +69,8 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
|||||||
/*_kr_buildAccountSection(context),*/
|
/*_kr_buildAccountSection(context),*/
|
||||||
if (controller.kr_isLoading.value)
|
if (controller.kr_isLoading.value)
|
||||||
Container(
|
Container(
|
||||||
height: MediaQuery.of(context).size.height * 0.5,
|
height:
|
||||||
|
MediaQuery.of(context).size.height * 0.5,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: KRSimpleLoading(
|
child: KRSimpleLoading(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@ -66,10 +80,12 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
|||||||
)
|
)
|
||||||
else if (controller.kr_plans.isEmpty)
|
else if (controller.kr_plans.isEmpty)
|
||||||
Container(
|
Container(
|
||||||
height: MediaQuery.of(context).size.height * 0.5,
|
height:
|
||||||
|
MediaQuery.of(context).size.height * 0.5,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
AppTranslations.kr_purchaseMembership.noData,
|
AppTranslations
|
||||||
|
.kr_purchaseMembership.noData,
|
||||||
style: KrAppTextStyle(
|
style: KrAppTextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
@ -82,8 +98,8 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
|||||||
)
|
)
|
||||||
else
|
else
|
||||||
Container(
|
Container(
|
||||||
margin:
|
margin: EdgeInsets.only(
|
||||||
EdgeInsets.only(left: 40.0 - 20, right: 40.0),
|
left: 40.0 - 20, right: 40.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// 套餐选择部分
|
// 套餐选择部分
|
||||||
@ -96,8 +112,10 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
|||||||
Column(
|
Column(
|
||||||
// 使用 List.generate 动态创建卡片列表
|
// 使用 List.generate 动态创建卡片列表
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
controller.kr_getTotalOptionsCount(
|
controller
|
||||||
controller.kr_plans[controller
|
.kr_getTotalOptionsCount(
|
||||||
|
controller.kr_plans[
|
||||||
|
controller
|
||||||
.kr_selectedPlanIndex
|
.kr_selectedPlanIndex
|
||||||
.value]),
|
.value]),
|
||||||
(index) {
|
(index) {
|
||||||
@ -142,22 +160,91 @@ class KRPurchaseMembershipView extends GetView<KRPurchaseMembershipController>
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 160.0, // 距离顶部的距离
|
top: 160.0,
|
||||||
right: 10.0, // 固定在右侧 20.w 的位置
|
right: 10.0,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
controller.clearPendingOrderNo();
|
||||||
|
},
|
||||||
child: KrLocalImage(
|
child: KrLocalImage(
|
||||||
imageName: 'purchase_slogan',
|
imageName: 'purchase_slogan',
|
||||||
imageType: ImageType.svg,
|
imageType: ImageType.svg,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const HIHelpEntrance(isLight: false)
|
const HIHelpEntrance(isLight: false)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// --- 从这里移除按钮的 Positioned ---
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 第二层:恢复按钮,放在 HIBaseScaffold 的上层
|
||||||
|
Positioned(
|
||||||
|
// top 值需要根据 HIBaseScaffold 的标题栏高度来精确调整
|
||||||
|
// topContentAreaHeight 是 110,按钮高度是 20,让按钮下半部分贴着内容区顶部
|
||||||
|
top: buttonTopPosition, // 110是标题区域高度, 10是按钮高度的一半
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: _buildRestoreButton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 构建“恢复已购项目”按钮
|
||||||
|
Widget _buildRestoreButton() {
|
||||||
|
if (!Platform.isIOS ) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return Center(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// 异步处理补单
|
||||||
|
controller.kr_restorePurchases(isShowLoading: false); // 恢复未完成订单
|
||||||
|
HIDialog.show(
|
||||||
|
title: '温馨提示',
|
||||||
|
message: '购买订单已确认,请耐心等待。若时长未到账,请联系在线客服处理。',
|
||||||
|
confirmText: '联系客服',
|
||||||
|
cancelText: '关闭',
|
||||||
|
autoClose: false,
|
||||||
|
showLoading: true,
|
||||||
|
barrierDismissible: false,
|
||||||
|
preventBackDismiss: true,
|
||||||
|
onConfirm: () async {
|
||||||
|
Get.toNamed(Routes.KR_CRISP);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
height: 20,
|
||||||
|
width: 94, // 保持固定宽度 94
|
||||||
|
// 移除 padding 属性
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
// 这个 Center 组件会使其子组件 Text 在 94x20 的空间内水平和垂直居中
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'恢复已购项目',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 12,
|
||||||
|
height: 1.1,
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 账号部分
|
// 账号部分
|
||||||
Widget _kr_buildAccountSection(BuildContext context) {
|
Widget _kr_buildAccountSection(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
|
|||||||
@ -2,12 +2,14 @@
|
|||||||
abstract class Api {
|
abstract class Api {
|
||||||
/// 游客登录查看是否已经注册
|
/// 游客登录查看是否已经注册
|
||||||
static const String kr_isRegister = "/v1/auth/check";
|
static const String kr_isRegister = "/v1/auth/check";
|
||||||
|
|
||||||
/// 判断邮箱和当前设备是否已存在订阅
|
/// 判断邮箱和当前设备是否已存在订阅
|
||||||
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
|
static const String kr_checkSubscription = "/v1/public/user/subscribe_status";
|
||||||
|
|
||||||
/// 注册
|
/// 注册
|
||||||
// static const String kr_register = "/v1/auth/register";
|
// static const String kr_register = "/v1/auth/register";
|
||||||
static const String kr_register = "/v1/public/user/bind_email_with_verification";
|
static const String kr_register =
|
||||||
|
"/v1/public/user/bind_email_with_verification";
|
||||||
|
|
||||||
/// 验证验证码
|
/// 验证验证码
|
||||||
static const String kr_checkVerificationCode = "/v1/auth/check-code";
|
static const String kr_checkVerificationCode = "/v1/auth/check-code";
|
||||||
@ -56,12 +58,10 @@ abstract class Api {
|
|||||||
static const String kr_getPackageList = "/v1/public/subscribe/list";
|
static const String kr_getPackageList = "/v1/public/subscribe/list";
|
||||||
|
|
||||||
/// 获取用户已订阅套餐(用于判断是否购买过)
|
/// 获取用户已订阅套餐(用于判断是否购买过)
|
||||||
static const String kr_getAlreadySubscribe =
|
static const String kr_getAlreadySubscribe = "/v1/public/user/subscribe";
|
||||||
"/v1/public/user/subscribe";
|
|
||||||
|
|
||||||
/// 获取用户可用订阅(与已订阅接口相同,OmnOem 项目中没有区分)
|
/// 获取用户可用订阅(与已订阅接口相同,OmnOem 项目中没有区分)
|
||||||
static const String kr_userAvailableSubscribe =
|
static const String kr_userAvailableSubscribe = "/v1/public/user/subscribe";
|
||||||
"/v1/public/user/subscribe";
|
|
||||||
|
|
||||||
/// 续费
|
/// 续费
|
||||||
static const String kr_renewal = "/v1/public/order/renewal";
|
static const String kr_renewal = "/v1/public/order/renewal";
|
||||||
@ -111,12 +111,13 @@ abstract class Api {
|
|||||||
/// 获取可用支付方式(公开接口)
|
/// 获取可用支付方式(公开接口)
|
||||||
static const String kr_getPublicPaymentMethods = "/v1/public/payment/methods";
|
static const String kr_getPublicPaymentMethods = "/v1/public/payment/methods";
|
||||||
|
|
||||||
static const String kr_attachAppleIapTransaction = "/v1/public/iap/apple/transactions/attach_by_id";
|
static const String kr_attachAppleIapTransaction =
|
||||||
|
"/v1/public/iap/apple/transactions/attach_by_id";
|
||||||
|
static const String kr_restoreAppleIap = "/v1/public/iap/apple/restore";
|
||||||
|
|
||||||
/// 获取用户信息(用于获取邀请码等)
|
/// 获取用户信息(用于获取邀请码等)
|
||||||
static const String kr_getUserInfo = "/v1/public/user/info";
|
static const String kr_getUserInfo = "/v1/public/user/info";
|
||||||
|
|
||||||
/// 保存邀请码
|
/// 保存邀请码
|
||||||
static const String hi_invite_code = "/v1/public/user/bind_invite_code";
|
static const String hi_invite_code = "/v1/public/user/bind_invite_code";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -271,7 +271,8 @@ class KRSubscribeApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 获取支付地址,跳转到付款地址(新版本:支持多种支付类型)
|
/// 获取支付地址,跳转到付款地址(新版本:支持多种支付类型)
|
||||||
Future<Either<HttpError, KRCheckoutResponse>> kr_checkout(String orderId) async {
|
Future<Either<HttpError, KRCheckoutResponse>> kr_checkout(
|
||||||
|
String orderId) async {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
data['orderNo'] = orderId;
|
data['orderNo'] = orderId;
|
||||||
data['returnUrl'] = AppConfig.getInstance().baseUrl;
|
data['returnUrl'] = AppConfig.getInstance().baseUrl;
|
||||||
@ -330,7 +331,8 @@ class KRSubscribeApi {
|
|||||||
return right(baseResponse.model);
|
return right(baseResponse.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<HttpError, bool>> kr_attachAppleIapTransaction(String transactionId, String orderNo) async {
|
Future<Either<HttpError, bool>> kr_attachAppleIapTransaction(
|
||||||
|
String transactionId, String orderNo) async {
|
||||||
final Map<String, dynamic> data = <String, dynamic>{};
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
data['transaction_id'] = transactionId;
|
data['transaction_id'] = transactionId;
|
||||||
data['order_no'] = orderNo;
|
data['order_no'] = orderNo;
|
||||||
@ -340,10 +342,32 @@ class KRSubscribeApi {
|
|||||||
Api.kr_attachAppleIapTransaction,
|
Api.kr_attachAppleIapTransaction,
|
||||||
data,
|
data,
|
||||||
method: HttpMethod.POST,
|
method: HttpMethod.POST,
|
||||||
isShowLoading: true,
|
isShowLoading: false,
|
||||||
);
|
);
|
||||||
if (!baseResponse.isSuccess) {
|
if (!baseResponse.isSuccess) {
|
||||||
return left(HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
return left(
|
||||||
|
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
return right(baseResponse.model.kr_bl);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Either<HttpError, bool>> kr_restoreAppleIapTransaction(
|
||||||
|
String transactionId, String orderNo) async {
|
||||||
|
final Map<String, dynamic> data = <String, dynamic>{};
|
||||||
|
data['transaction_id'] = [transactionId];
|
||||||
|
data['order_no'] = orderNo;
|
||||||
|
|
||||||
|
BaseResponse<KRStatus> baseResponse =
|
||||||
|
await HttpUtil.getInstance().request<KRStatus>(
|
||||||
|
Api.kr_restoreAppleIap,
|
||||||
|
data,
|
||||||
|
method: HttpMethod.POST,
|
||||||
|
isShowLoading: false,
|
||||||
|
);
|
||||||
|
if (!baseResponse.isSuccess) {
|
||||||
|
return left(
|
||||||
|
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
return right(baseResponse.model.kr_bl);
|
return right(baseResponse.model.kr_bl);
|
||||||
|
|||||||
@ -603,6 +603,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0"
|
version: "0.6.0"
|
||||||
|
flutter_keychain:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_keychain
|
||||||
|
sha256: "0d000c0e9b3c16fdec016df406b4e89e7195bf719ed0882157400f1e16323cf8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.0"
|
||||||
flutter_loggy:
|
flutter_loggy:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -70,6 +70,7 @@ dependencies:
|
|||||||
|
|
||||||
# 存储和安全
|
# 存储和安全
|
||||||
flutter_udid: ^4.0.0
|
flutter_udid: ^4.0.0
|
||||||
|
flutter_keychain: 2.5.0
|
||||||
|
|
||||||
# 平台集成
|
# 平台集成
|
||||||
window_manager: ^0.4.3
|
window_manager: ^0.4.3
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user