feat: iap支付方式
This commit is contained in:
parent
f888b772c2
commit
60de644637
@ -1,5 +1,5 @@
|
|||||||
class KRPurchaseOrderNo {
|
class KRPurchaseOrderNo {
|
||||||
final String orderNo;
|
final String orderNo;
|
||||||
|
|
||||||
KRPurchaseOrderNo({required this.orderNo});
|
KRPurchaseOrderNo({required this.orderNo});
|
||||||
|
|
||||||
@ -8,8 +8,6 @@ class KRPurchaseOrderNo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class KRPurchaseOrderUrl {
|
class KRPurchaseOrderUrl {
|
||||||
final String url;
|
final String url;
|
||||||
|
|
||||||
@ -22,14 +20,16 @@ class KRPurchaseOrderUrl {
|
|||||||
|
|
||||||
/// Checkout 响应(参考 Tauri 项目)
|
/// Checkout 响应(参考 Tauri 项目)
|
||||||
class KRCheckoutResponse {
|
class KRCheckoutResponse {
|
||||||
final String type; // "url" | "qr" | "stripe"
|
final String type; // "url" | "qr" | "stripe" | "apple_iap"
|
||||||
final String? checkoutUrl;
|
final String? checkoutUrl;
|
||||||
final KRStripePayment? stripe;
|
final KRStripePayment? stripe;
|
||||||
|
final KRIpaPayment? ipa;
|
||||||
|
|
||||||
KRCheckoutResponse({
|
KRCheckoutResponse({
|
||||||
required this.type,
|
required this.type,
|
||||||
this.checkoutUrl,
|
this.checkoutUrl,
|
||||||
this.stripe,
|
this.stripe,
|
||||||
|
this.ipa,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory KRCheckoutResponse.fromJson(Map<String, dynamic> json) {
|
factory KRCheckoutResponse.fromJson(Map<String, dynamic> json) {
|
||||||
@ -39,6 +39,7 @@ class KRCheckoutResponse {
|
|||||||
stripe: json['stripe'] != null
|
stripe: json['stripe'] != null
|
||||||
? KRStripePayment.fromJson(json['stripe'])
|
? KRStripePayment.fromJson(json['stripe'])
|
||||||
: null,
|
: null,
|
||||||
|
ipa: json['ipa'] != null ? KRIpaPayment.fromJson(json['ipa']) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,4 +63,25 @@ class KRStripePayment {
|
|||||||
publishableKey: json['publishable_key'] ?? '',
|
publishableKey: json['publishable_key'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class KRIpaPayment {
|
||||||
|
final String productId;
|
||||||
|
final String? applicationUsername;
|
||||||
|
final String? orderNo;
|
||||||
|
|
||||||
|
KRIpaPayment({
|
||||||
|
required this.productId,
|
||||||
|
this.applicationUsername,
|
||||||
|
this.orderNo,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory KRIpaPayment.fromJson(Map<String, dynamic> json) {
|
||||||
|
return KRIpaPayment(
|
||||||
|
productId: json['product_id'] ?? json['productId'] ?? '',
|
||||||
|
applicationUsername:
|
||||||
|
json['application_username'] ?? json['applicationUsername'],
|
||||||
|
orderNo: json['order_no'] ?? json['orderNo'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -78,7 +78,7 @@ class KRUserAvailableSubscribeList {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory KRUserAvailableSubscribeList.fromJson(Map<String, dynamic> json) {
|
factory KRUserAvailableSubscribeList.fromJson(Map<String, dynamic> json) {
|
||||||
KRLogUtil.kr_i('订阅json列表: ${json}', tag: 'KRUserAvailableSubscribeList');
|
// KRLogUtil.kr_i('订阅json列表: ${json}', tag: 'KRUserAvailableSubscribeList');
|
||||||
final List<dynamic> listData = (json['list'] as List<dynamic>?) ?? const [];
|
final List<dynamic> listData = (json['list'] as List<dynamic>?) ?? const [];
|
||||||
return KRUserAvailableSubscribeList(
|
return KRUserAvailableSubscribeList(
|
||||||
list: listData
|
list: listData
|
||||||
|
|||||||
@ -86,7 +86,7 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
'Hi快VPN-网在我在,网快我快',
|
'Hi快VPN-网在我在,网快我快',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 14.sp,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -102,7 +102,7 @@ class _KRHomeViewState extends State<KRHomeView> {
|
|||||||
// --- 定义统一的文本样式,并设置行高 ---
|
// --- 定义统一的文本样式,并设置行高 ---
|
||||||
final normalStyle = TextStyle(
|
final normalStyle = TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.sp,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
height: 1.2,
|
height: 1.2,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,6 +18,9 @@ import '../../../utils/kr_event_bus.dart';
|
|||||||
import '../../../network/http_util.dart';
|
import '../../../network/http_util.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_stripe/flutter_stripe.dart';
|
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';
|
||||||
|
|
||||||
/// 会员购买控制器
|
/// 会员购买控制器
|
||||||
/// 负责处理会员套餐选择、支付方式选择和订阅流程
|
/// 负责处理会员套餐选择、支付方式选择和订阅流程
|
||||||
@ -53,7 +56,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
@override
|
@override
|
||||||
void onInit() async {
|
void onInit() async {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
print('💳 [PurchaseMembership] ========== Controller.onInit 被调用 ==========');
|
print(
|
||||||
|
'💳 [PurchaseMembership] ========== Controller.onInit 被调用 ==========');
|
||||||
print('💳 [PurchaseMembership] 当前时间: ${DateTime.now()}');
|
print('💳 [PurchaseMembership] 当前时间: ${DateTime.now()}');
|
||||||
|
|
||||||
// 🔧 紧急诊断:写文件确认购买页面Controller被初始化
|
// 🔧 紧急诊断:写文件确认购买页面Controller被初始化
|
||||||
@ -61,11 +65,14 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
final debugFile = File('${dir.path}/PURCHASE_CONTROLLER_DEBUG.txt');
|
final debugFile = File('${dir.path}/PURCHASE_CONTROLLER_DEBUG.txt');
|
||||||
await debugFile.writeAsString(
|
await debugFile.writeAsString(
|
||||||
'=' * 60 + '\n'
|
'=' * 60 +
|
||||||
'💳 PurchaseMembershipController.onInit 被调用!\n'
|
'\n'
|
||||||
'时间: ${DateTime.now()}\n'
|
'💳 PurchaseMembershipController.onInit 被调用!\n'
|
||||||
'版本标识: Android15_Fix_v6_Final\n'
|
'时间: ${DateTime.now()}\n'
|
||||||
'=' * 60 + '\n',
|
'版本标识: Android15_Fix_v6_Final\n'
|
||||||
|
'=' *
|
||||||
|
60 +
|
||||||
|
'\n',
|
||||||
mode: FileMode.append,
|
mode: FileMode.append,
|
||||||
);
|
);
|
||||||
print('💳 [PurchaseMembership] ✅ 调试日志已写入文件: ${debugFile.path}');
|
print('💳 [PurchaseMembership] ✅ 调试日志已写入文件: ${debugFile.path}');
|
||||||
@ -178,7 +185,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
KRLogUtil.kr_w('获取支付方式超时', tag: 'PurchaseMembership');
|
KRLogUtil.kr_w('获取支付方式超时', tag: 'PurchaseMembership');
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
print('💳 [PurchaseMembership] ✓ 步骤4完成,支付方式数量: ${kr_paymentMethods.length}');
|
print(
|
||||||
|
'💳 [PurchaseMembership] ✓ 步骤4完成,支付方式数量: ${kr_paymentMethods.length}');
|
||||||
|
|
||||||
// 根据套餐数量决定是否显示套餐选择器
|
// 根据套餐数量决定是否显示套餐选择器
|
||||||
kr_showPlanSelector.value = kr_plans.length > 1;
|
kr_showPlanSelector.value = kr_plans.length > 1;
|
||||||
@ -193,7 +201,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
bool domainSwitched = await KRDomain.kr_switchToNextDomain();
|
bool domainSwitched = await KRDomain.kr_switchToNextDomain();
|
||||||
|
|
||||||
if (domainSwitched) {
|
if (domainSwitched) {
|
||||||
print('💳 [PurchaseMembership] ✓ 域名切换成功,当前域名: ${KRDomain.kr_currentDomain}');
|
print(
|
||||||
|
'💳 [PurchaseMembership] ✓ 域名切换成功,当前域名: ${KRDomain.kr_currentDomain}');
|
||||||
print('💳 [PurchaseMembership] 🔄 使用新域名重试...');
|
print('💳 [PurchaseMembership] 🔄 使用新域名重试...');
|
||||||
|
|
||||||
// 更新 HttpUtil 的 baseUrl
|
// 更新 HttpUtil 的 baseUrl
|
||||||
@ -300,7 +309,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
final either = await _kr_subscribeApi.kr_getPublicPaymentMethods();
|
final either = await _kr_subscribeApi.kr_getPublicPaymentMethods();
|
||||||
either.fold(
|
either.fold(
|
||||||
(error) {
|
(error) {
|
||||||
KRLogUtil.kr_e('获取公开支付方式失败: ${error.msg}', tag: 'PurchaseMembershipController');
|
KRLogUtil.kr_e('获取公开支付方式失败: ${error.msg}',
|
||||||
|
tag: 'PurchaseMembershipController');
|
||||||
},
|
},
|
||||||
(responseData) {
|
(responseData) {
|
||||||
KRLogUtil.kr_i('✅ 获取公开支付方式成功', tag: 'PurchaseMembershipController');
|
KRLogUtil.kr_i('✅ 获取公开支付方式成功', tag: 'PurchaseMembershipController');
|
||||||
@ -310,14 +320,14 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
final paymentMethodsData = KRPaymentMethods.fromJson(responseData);
|
final paymentMethodsData = KRPaymentMethods.fromJson(responseData);
|
||||||
kr_paymentMethods.value = paymentMethodsData.list;
|
kr_paymentMethods.value = paymentMethodsData.list;
|
||||||
|
|
||||||
KRLogUtil.kr_i('📊 支付方式数据已加载,共 ${kr_paymentMethods.length} 种', tag: 'PurchaseMembershipController');
|
KRLogUtil.kr_i('📊 支付方式数据已加载,共 ${kr_paymentMethods.length} 种',
|
||||||
|
tag: 'PurchaseMembershipController');
|
||||||
|
|
||||||
// 打印支付方式信息
|
// 打印支付方式信息
|
||||||
for (var method in kr_paymentMethods) {
|
for (var method in kr_paymentMethods) {
|
||||||
KRLogUtil.kr_i(
|
KRLogUtil.kr_i(
|
||||||
'💳 支付方式: ${method.name} (ID: ${method.id}, Platform: ${method.platform})',
|
'💳 支付方式: ${method.name} (ID: ${method.id}, Platform: ${method.platform})',
|
||||||
tag: 'PurchaseMembershipController'
|
tag: 'PurchaseMembershipController');
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print('═══════════════════════════════════════');
|
print('═══════════════════════════════════════');
|
||||||
@ -479,7 +489,7 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
if (isIOS) {
|
if (isIOS) {
|
||||||
// 2. iOS 专属:查找 'stripe'
|
// 2. iOS 专属:查找 'stripe'
|
||||||
final stripeMethod = kr_paymentMethods.firstWhere(
|
final stripeMethod = kr_paymentMethods.firstWhere(
|
||||||
(method) => method.platform == 'Stripe',
|
(method) => method.platform == 'apple_iap',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (stripeMethod != null) {
|
if (stripeMethod != null) {
|
||||||
@ -516,11 +526,12 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
|
.any((subscribe) => subscribe.subscribeId == selectedPlan.kr_id);
|
||||||
final subscribeId = isRenewal
|
final subscribeId = isRenewal
|
||||||
? _kr_alreadySubscribe
|
? _kr_alreadySubscribe
|
||||||
.firstWhere(
|
.firstWhere(
|
||||||
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id,
|
(subscribe) => subscribe.subscribeId == selectedPlan.kr_id,
|
||||||
orElse: () => KRAlreadySubscribe(userSubscribeId: 0, subscribeId: 0), // 默认值
|
orElse: () =>
|
||||||
)
|
KRAlreadySubscribe(userSubscribeId: 0, subscribeId: 0), // 默认值
|
||||||
.userSubscribeId
|
)
|
||||||
|
.userSubscribeId
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
// 根据判断结果调用不同的接口
|
// 根据判断结果调用不同的接口
|
||||||
@ -539,7 +550,7 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
purchaseEither.fold(
|
purchaseEither.fold(
|
||||||
(error) {
|
(error) {
|
||||||
print('❌ 请求失败:');
|
print('❌ 请求失败:');
|
||||||
print(' 错误码: ${error.code}');
|
print(' 错误码: ${error.code}');
|
||||||
print(' 错误信息: ${error.msg}');
|
print(' 错误信息: ${error.msg}');
|
||||||
@ -579,7 +590,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
// 计算折扣价格
|
// 计算折扣价格
|
||||||
final discount = plan.kr_discount[discountIndex];
|
final discount = plan.kr_discount[discountIndex];
|
||||||
// 如果 discount 是 0,则表示原价(100%)
|
// 如果 discount 是 0,则表示原价(100%)
|
||||||
final discountRate = discount.kr_discount == 0 ? 100.0 : discount.kr_discount.toDouble();
|
final discountRate =
|
||||||
|
discount.kr_discount == 0 ? 100.0 : discount.kr_discount.toDouble();
|
||||||
return (plan.kr_unitPrice / 100) *
|
return (plan.kr_unitPrice / 100) *
|
||||||
discount.kr_quantity *
|
discount.kr_quantity *
|
||||||
(discountRate / 100);
|
(discountRate / 100);
|
||||||
@ -594,8 +606,7 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
discountIndex < plan.kr_discount.length) {
|
discountIndex < plan.kr_discount.length) {
|
||||||
// 计算折扣价格
|
// 计算折扣价格
|
||||||
final discount = plan.kr_discount[discountIndex];
|
final discount = plan.kr_discount[discountIndex];
|
||||||
return (plan.kr_unitPrice / 100) *
|
return (plan.kr_unitPrice / 100) * (discount.kr_discount / 100);
|
||||||
(discount.kr_discount / 100);
|
|
||||||
}
|
}
|
||||||
return plan.kr_unitPrice / 100;
|
return plan.kr_unitPrice / 100;
|
||||||
}
|
}
|
||||||
@ -776,20 +787,21 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
final checkoutEither = await _kr_subscribeApi.kr_checkout(orderNo);
|
final checkoutEither = await _kr_subscribeApi.kr_checkout(orderNo);
|
||||||
|
|
||||||
checkoutEither.fold(
|
checkoutEither.fold(
|
||||||
(error) {
|
(error) {
|
||||||
print('❌ Checkout 失败:');
|
print('❌ Checkout 失败:');
|
||||||
print(' 错误码: ${error.code}');
|
print(' 错误码: ${error.code}');
|
||||||
print(' 错误信息: ${error.msg}');
|
print(' 错误信息: ${error.msg}');
|
||||||
print('═══════════════════════════════════════');
|
print('═══════════════════════════════════════');
|
||||||
KRCommonUtil.kr_showToast(error.msg);
|
KRCommonUtil.kr_showToast(error.msg);
|
||||||
},
|
},
|
||||||
(checkoutResponse) async {
|
(checkoutResponse) async {
|
||||||
print('✅ Checkout 成功:');
|
print('✅ Checkout 成功:');
|
||||||
print(' 支付类型: ${checkoutResponse.type}');
|
print(' 支付类型: ${checkoutResponse.type}');
|
||||||
|
|
||||||
if (checkoutResponse.type == 'url') {
|
if (checkoutResponse.type == 'url') {
|
||||||
// URL 类型:在浏览器中打开支付链接,同时跳转到订单状态页面
|
// URL 类型:在浏览器中打开支付链接,同时跳转到订单状态页面
|
||||||
if (checkoutResponse.checkoutUrl != null && checkoutResponse.checkoutUrl!.isNotEmpty) {
|
if (checkoutResponse.checkoutUrl != null &&
|
||||||
|
checkoutResponse.checkoutUrl!.isNotEmpty) {
|
||||||
print(' 支付链接: ${checkoutResponse.checkoutUrl}');
|
print(' 支付链接: ${checkoutResponse.checkoutUrl}');
|
||||||
print('🌐 正在用外部浏览器打开支付链接...');
|
print('🌐 正在用外部浏览器打开支付链接...');
|
||||||
print('═══════════════════════════════════════');
|
print('═══════════════════════════════════════');
|
||||||
@ -839,7 +851,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
} else if (checkoutResponse.type == 'stripe') {
|
} else if (checkoutResponse.type == 'stripe') {
|
||||||
// Stripe 类型:显示 Stripe 支付表单
|
// Stripe 类型:显示 Stripe 支付表单
|
||||||
print(' Stripe Method: ${checkoutResponse.stripe?.method}');
|
print(' Stripe Method: ${checkoutResponse.stripe?.method}');
|
||||||
print(' Stripe Client Secret: ${checkoutResponse.stripe?.clientSecret}');
|
print(
|
||||||
|
' Stripe Client Secret: ${checkoutResponse.stripe?.clientSecret}');
|
||||||
print('💳 显示 Stripe 支付表单...');
|
print('💳 显示 Stripe 支付表单...');
|
||||||
print('═══════════════════════════════════════');
|
print('═══════════════════════════════════════');
|
||||||
|
|
||||||
@ -854,7 +867,8 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
KRCommonUtil.kr_showToast('Apple Pay 仅在 iOS 可用');
|
KRCommonUtil.kr_showToast('Apple Pay 仅在 iOS 可用');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
print('Platform: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}');
|
print(
|
||||||
|
'Platform: ${Platform.operatingSystem} ${Platform.operatingSystemVersion}');
|
||||||
print('Merchant identifier: ${Stripe.merchantIdentifier}');
|
print('Merchant identifier: ${Stripe.merchantIdentifier}');
|
||||||
// 检查设备是否支持 Platform Pay(Apple Pay)
|
// 检查设备是否支持 Platform Pay(Apple Pay)
|
||||||
final supported = await Stripe.instance.isPlatformPaySupported();
|
final supported = await Stripe.instance.isPlatformPaySupported();
|
||||||
@ -870,10 +884,9 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
cartItems: [
|
cartItems: [
|
||||||
ApplePayCartSummaryItem.immediate(
|
ApplePayCartSummaryItem.immediate(
|
||||||
label: 'Hi快VPN 服务',
|
label: 'Hi快VPN 服务',
|
||||||
amount: kr_getPlanPrice(
|
amount: kr_getPlanPrice(kr_plans[kr_selectedPlanIndex.value],
|
||||||
kr_plans[kr_selectedPlanIndex.value],
|
discountIndex: kr_selectedDiscountIndex.value)
|
||||||
discountIndex: kr_selectedDiscountIndex.value
|
.toStringAsFixed(2),
|
||||||
).toStringAsFixed(2),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -897,10 +910,11 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
'stripe': checkoutResponse.stripe,
|
'stripe': checkoutResponse.stripe,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else if (checkoutResponse.type == 'balance') {
|
} else if (checkoutResponse.type == 'balance') {
|
||||||
// Stripe 类型:显示 Stripe 支付表单
|
// Stripe 类型:显示 Stripe 支付表单
|
||||||
print(' Stripe Method: ${checkoutResponse.stripe?.method}');
|
print(' Stripe Method: ${checkoutResponse.stripe?.method}');
|
||||||
print(' Stripe Client Secret: ${checkoutResponse.stripe?.clientSecret}');
|
print(
|
||||||
|
' Stripe Client Secret: ${checkoutResponse.stripe?.clientSecret}');
|
||||||
print('💳 显示 Stripe 支付表单...');
|
print('💳 显示 Stripe 支付表单...');
|
||||||
print('═══════════════════════════════════════');
|
print('═══════════════════════════════════════');
|
||||||
|
|
||||||
@ -913,13 +927,242 @@ class KRPurchaseMembershipController extends GetxController {
|
|||||||
'checkout_type': 'balance',
|
'checkout_type': 'balance',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else if (checkoutResponse.type == 'apple_iap') {
|
||||||
|
if (!Platform.isIOS) {
|
||||||
|
KRCommonUtil.kr_showToast('仅限在 iOS 设备上购买');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final KRIpaPayment iapParams = (checkoutResponse.ipa == null ||
|
||||||
|
(checkoutResponse.ipa?.productId.isEmpty ?? true))
|
||||||
|
? KRIpaPayment(
|
||||||
|
productId: 'com.hifastvpn.vip.day${kr_getSelectedQuantity()}')
|
||||||
|
: checkoutResponse.ipa!;
|
||||||
|
|
||||||
|
final iap = InAppPurchase.instance;
|
||||||
|
final available = await iap.isAvailable();
|
||||||
|
if (!available) {
|
||||||
|
print('IAP 不可用');
|
||||||
|
KRCommonUtil.kr_showToast('App Store 不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final productIds = {iapParams.productId};
|
||||||
|
final productDetailsResponse =
|
||||||
|
await iap.queryProductDetails(productIds);
|
||||||
|
if (productDetailsResponse.error != null) {
|
||||||
|
final e = productDetailsResponse.error!;
|
||||||
|
print('IAP 查询商品错误 code: ${e.code}');
|
||||||
|
print('IAP 查询商品错误 message: ${e.message}');
|
||||||
|
print('IAP 查询商品错误 iapParams.productId: ${iapParams.productId}');
|
||||||
|
KRCommonUtil.kr_showToast('无法获取商品信息: ${e.message}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (productDetailsResponse.productDetails.isEmpty) {
|
||||||
|
print('IAP 商品列表为空 productId: ${iapParams.productId}');
|
||||||
|
KRCommonUtil.kr_showToast('未找到对应商品');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final product = productDetailsResponse.productDetails.first;
|
||||||
|
|
||||||
|
final purchaseParam = PurchaseParam(
|
||||||
|
productDetails: product,
|
||||||
|
applicationUserName: iapParams.applicationUsername,
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// 假设 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 ---');
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result =
|
||||||
|
await iap.buyNonConsumable(purchaseParam: purchaseParam);
|
||||||
|
print('--- IAP 流程:buyNonConsumable 调用返回。返回值:$result ---');
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
// 如果返回 false,说明本地请求失败
|
||||||
|
print('IAP 购买请求本地失败:请检查 PurchaseParam 或设备设置。');
|
||||||
|
KRCommonUtil.kr_showToast('购买请求失败,请重试');
|
||||||
|
// 注意:如果 Stream 此时没有收到 error 事件,这里可能需要手动清理。
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 捕获任何意想不到的、未通过 Stream 报告的本地异常
|
||||||
|
print('--- IAP 流程:buyNonConsumable 调用发生异常:$e ---');
|
||||||
|
subscription.cancel();
|
||||||
|
KRCommonUtil.kr_showToast('购买服务异常,请重试');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
print('⚠️ 未知的支付类型: ${checkoutResponse.type}');
|
print('⚠️ 未知的支付类型: ${checkoutResponse.type}');
|
||||||
print('═══════════════════════════════════════');
|
print('═══════════════════════════════════════');
|
||||||
// KRCommonUtil.kr_showToast('不支持的支付类型: ${checkoutResponse.type}');
|
// KRCommonUtil.kr_showToast('不支持的支付类型: ${checkoutResponse.type}');
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/// 🔴【关键修复】在发起购买前,清理同一商品的 pending 交易
|
||||||
|
Future<void> _cleanPendingPurchase(
|
||||||
|
InAppPurchase iap,
|
||||||
|
String productId,
|
||||||
|
) async {
|
||||||
|
print('🔧 IAP 清理 pending 交易开始');
|
||||||
|
|
||||||
|
final completer = Completer<void>();
|
||||||
|
|
||||||
|
late StreamSubscription<List<PurchaseDetails>> tempSub;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 👇 只跑一次就够了
|
||||||
|
await tempSub.cancel();
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 给 StoreKit 时间推送历史交易
|
||||||
|
await Future.delayed(const Duration(milliseconds: 800));
|
||||||
|
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
await tempSub.cancel();
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
print('🔧 IAP 清理 pending 交易完成');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,8 @@ class BaseResponse<T> {
|
|||||||
if (cipherText.isNotEmpty && nonce.isNotEmpty) {
|
if (cipherText.isNotEmpty && nonce.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('═══════════════════════════════════════');
|
// print('═══════════════════════════════════════');
|
||||||
print('🔐 检测到加密响应,开始解密...');
|
// print('🔐 检测到加密响应,开始解密...');
|
||||||
print('📥 加密数据长度: ${cipherText.length} 字符');
|
print('📥 加密数据长度: ${cipherText.length} 字符');
|
||||||
print('⏰ 时间戳: $nonce');
|
print('⏰ 时间戳: $nonce');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -431,9 +431,6 @@ class _KRSimpleHttpInterceptor extends Interceptor {
|
|||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
print('');
|
print('');
|
||||||
}
|
}
|
||||||
if (kDebugMode) {
|
|
||||||
print('🔐 检测到加密请求,正在解密...');
|
|
||||||
}
|
|
||||||
// 尝试解密并打印原始数据
|
// 尝试解密并打印原始数据
|
||||||
final encryptedData = data['data'] as String;
|
final encryptedData = data['data'] as String;
|
||||||
final nonce = data['time'] as String;
|
final nonce = data['time'] as String;
|
||||||
|
|||||||
@ -111,6 +111,8 @@ 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_getUserInfo = "/v1/public/user/info";
|
static const String kr_getUserInfo = "/v1/public/user/info";
|
||||||
|
|
||||||
|
|||||||
@ -329,4 +329,23 @@ class KRSubscribeApi {
|
|||||||
|
|
||||||
return right(baseResponse.model);
|
return right(baseResponse.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Either<HttpError, bool>> kr_attachAppleIapTransaction(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_attachAppleIapTransaction,
|
||||||
|
data,
|
||||||
|
method: HttpMethod.POST,
|
||||||
|
isShowLoading: true,
|
||||||
|
);
|
||||||
|
if (!baseResponse.isSuccess) {
|
||||||
|
return left(HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
return right(baseResponse.model.kr_bl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -250,15 +250,15 @@ class KRSubscribeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kr_currentSubscribe.value = updatedSubscribe;
|
kr_currentSubscribe.value = updatedSubscribe;
|
||||||
KRLogUtil.kr_i('更新当前订阅信息', tag: 'SubscribeService');
|
// KRLogUtil.kr_i('更新当前订阅信息', tag: 'SubscribeService');
|
||||||
|
|
||||||
// 更新可用订阅列表
|
// 更新可用订阅列表
|
||||||
kr_availableSubscribes.assignAll(subscribes);
|
kr_availableSubscribes.assignAll(subscribes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
KRLogUtil.kr_i('获取可用订阅列表成功: ${subscribes.length} 个订阅',
|
// KRLogUtil.kr_i('获取可用订阅列表成功: ${subscribes.length} 个订阅',
|
||||||
tag: 'SubscribeService');
|
// tag: 'SubscribeService');
|
||||||
KRLogUtil.kr_i(
|
KRLogUtil.kr_i(
|
||||||
'订阅列表: ${subscribes.map((s) => '${s.name}(${s.id})').join(', ')}',
|
'订阅列表: ${subscribes.map((s) => '${s.name}(${s.id})').join(', ')}',
|
||||||
tag: 'SubscribeService');
|
tag: 'SubscribeService');
|
||||||
|
|||||||
32
pubspec.lock
32
pubspec.lock
@ -861,6 +861,38 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.1"
|
||||||
|
in_app_purchase:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: in_app_purchase
|
||||||
|
sha256: "11a40f148eeb4f681a0572003e2b33432e110c90c1bbb4f9ef83b81ec0c4f737"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
in_app_purchase_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: in_app_purchase_android
|
||||||
|
sha256: "45ae4fe253f85b4fcc58b421fe137f6e48aca16bf8a618cd760cb0542e7f854e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
|
in_app_purchase_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: in_app_purchase_platform_interface
|
||||||
|
sha256: "1d353d38251da5b9fea6635c0ebfc6bb17a2d28d0e86ea5e083bf64244f1fb4c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
in_app_purchase_storekit:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: in_app_purchase_storekit
|
||||||
|
sha256: "6ce1361278cacc0481508989ba419b2c9f46a2b0dc54b3fe54f5ee63c2718fef"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.22+1"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 0.0.4+107
|
version: 0.0.4+108
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.5.0 <4.0.0"
|
sdk: ">=3.5.0 <4.0.0"
|
||||||
@ -113,6 +113,7 @@ dependencies:
|
|||||||
tray_manager: ^0.2.0
|
tray_manager: ^0.2.0
|
||||||
device_info_plus: ^11.3.0
|
device_info_plus: ^11.3.0
|
||||||
flutter_stripe: ^10.1.0
|
flutter_stripe: ^10.1.0
|
||||||
|
in_app_purchase: 3.2.1
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user