You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
917 lines
34 KiB
917 lines
34 KiB
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import '../../../data/models/user_Info.dart';
|
|
import '../../../data/services/pay_service.dart';
|
|
import '../../../data/services/version_update_service.dart';
|
|
import '../../../data/models/goods.dart';
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
import '../../../data/services/network/dio_manager.dart';
|
|
import '../../../data/services/network/api.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'dart:async';
|
|
import 'package:app_links/app_links.dart';
|
|
|
|
/// goods 模块基础控制器,封装通用的商品加载与支付流程
|
|
abstract class GoodsBaseController extends GetxController {
|
|
final RxList<DBGoods> items = <DBGoods>[].obs;
|
|
final RxBool loading = false.obs;
|
|
final RxString error = ''.obs;
|
|
final RxBool isPaying = false.obs;
|
|
Timer? _payingTimeoutTimer;
|
|
|
|
late final PayService _payService = Get.find<PayService>();
|
|
StreamSubscription<Uri?>? _deeplinkSub;
|
|
late final AppLinks _appLinks = AppLinks();
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
_init();
|
|
// 处理冷启动场景:如果 App 通过深链直接启动
|
|
_handleInitialDeepLink();
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
_deeplinkSub?.cancel();
|
|
_deeplinkSub = null;
|
|
_payingTimeoutTimer?.cancel();
|
|
_payingTimeoutTimer = null;
|
|
super.onClose();
|
|
}
|
|
|
|
Future<void> _init() async {
|
|
loading.value = true;
|
|
error.value = '';
|
|
try {
|
|
final list = await fetchProducts();
|
|
items.assignAll(list);
|
|
// iOS 环境下,在页面加载后批量查询当前商品的 IAP 详情,便于排查 StoreKit 可用商品
|
|
await _debugQueryIapProductsIfIos();
|
|
} catch (e) {
|
|
error.value = e.toString();
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
/// 子类实现:返回商品列表(统一使用 DBGoods)
|
|
Future<List<DBGoods>> fetchProducts();
|
|
|
|
/// iOS 批量查询当前页面商品的 IAP 详情(仅真机 iOS 生效,Web/Android 跳过)
|
|
Future<void> _debugQueryIapProductsIfIos() async {
|
|
try {
|
|
final isIOS = GetPlatform.isIOS && !kIsWeb;
|
|
if (!isIOS) return;
|
|
|
|
// 收集页面商品ID;若为空,使用预设的基础套餐ID进行测试
|
|
final ids = items
|
|
.map((e) => (e.id ?? '').trim())
|
|
.where((id) => id.isNotEmpty)
|
|
.toSet();
|
|
if (ids.isEmpty) {
|
|
ids.addAll({
|
|
'ai_001',
|
|
});
|
|
}
|
|
|
|
print('[IAP-ios] 批量查询页面商品IDs: ${ids.join(', ')}');
|
|
await _payService.debugQueryProducts(ids);
|
|
} catch (e) {
|
|
print('[IAP-ios] 批量查询异常: $e');
|
|
}
|
|
}
|
|
|
|
/// 统一入口:根据后端配置决定是否弹出支付方式选择面板
|
|
Future<void> purchase(DBGoods item) async {
|
|
// 读取渠道配置
|
|
final enabled = _getEnabledPayMethods();
|
|
|
|
// 无配置或多个渠道:弹出选择面板
|
|
_PayMethod? method;
|
|
if (enabled.length != 1) {
|
|
method = await _showPayMethodSheet(enabled);
|
|
if (method == null) return;
|
|
} else {
|
|
// 仅一个渠道:直接走对应支付
|
|
method = enabled.first;
|
|
}
|
|
|
|
bool ok = false;
|
|
String failMsg = '支付失败,请重试';
|
|
isPaying.value = true;
|
|
_startPayingTimeout();
|
|
if (method == _PayMethod.wechat) {
|
|
ok = await payWechat(item, onResult: (success, msg) {
|
|
if (!success) failMsg = msg ?? failMsg;
|
|
});
|
|
} else if (method == _PayMethod.alipay) {
|
|
ok = await payAlipay(item, onResult: (success, msg) {
|
|
if (!success) failMsg = msg ?? failMsg;
|
|
});
|
|
} else if (method == _PayMethod.paypal) {
|
|
ok = await payPaypal(item, onResult: (success, msg) {
|
|
if (!success) failMsg = msg ?? failMsg;
|
|
});
|
|
} else if (method == _PayMethod.apple) {
|
|
ok = await payAppleIAP(item, onResult: (success, msg) {
|
|
if (!success) failMsg = msg ?? failMsg;
|
|
});
|
|
} else if (method == _PayMethod.google) {
|
|
ok = await payGoogleIAP(item, onResult: (success, msg) {
|
|
if (!success) failMsg = msg ?? failMsg;
|
|
});
|
|
}
|
|
|
|
if (!ok) {
|
|
Get.snackbar('提示', failMsg,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(12));
|
|
}
|
|
}
|
|
|
|
/// 解析后端 paychannels 并结合平台过滤可用支付方式
|
|
List<_PayMethod> _getEnabledPayMethods() {
|
|
try {
|
|
final paychannels =
|
|
VersionUpdateService.to.cachedAppInfo?.paychannels ?? '';
|
|
final raw = paychannels
|
|
.split(',')
|
|
.map((e) => e.trim().toLowerCase())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
|
|
final bool isIOS = GetPlatform.isIOS && !kIsWeb;
|
|
final bool isAndroid = GetPlatform.isAndroid && !kIsWeb;
|
|
|
|
final List<_PayMethod> methods = [];
|
|
for (final ch in raw) {
|
|
switch (ch) {
|
|
case 'wxpay':
|
|
case 'wechat':
|
|
methods.add(_PayMethod.wechat);
|
|
break;
|
|
case 'alipay':
|
|
methods.add(_PayMethod.alipay);
|
|
break;
|
|
case 'paypal':
|
|
methods.add(_PayMethod.paypal);
|
|
break;
|
|
case 'apple':
|
|
if (isIOS) methods.add(_PayMethod.apple);
|
|
break;
|
|
case 'google':
|
|
if (isAndroid) methods.add(_PayMethod.google);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 若后端未下发或全部被平台过滤,则回退为展示所有平台支持的方式
|
|
if (methods.isEmpty) {
|
|
final fallback = <_PayMethod>[
|
|
_PayMethod.wechat,
|
|
_PayMethod.alipay,
|
|
_PayMethod.paypal
|
|
];
|
|
if (isIOS) fallback.add(_PayMethod.apple);
|
|
if (isAndroid) fallback.add(_PayMethod.google);
|
|
return fallback;
|
|
}
|
|
return methods;
|
|
} catch (_) {
|
|
// 任意异常则回退为旧逻辑:展示所有
|
|
final bool isIOS = GetPlatform.isIOS && !kIsWeb;
|
|
final bool isAndroid = GetPlatform.isAndroid && !kIsWeb;
|
|
final fallback = <_PayMethod>[
|
|
_PayMethod.wechat,
|
|
_PayMethod.alipay,
|
|
_PayMethod.paypal
|
|
];
|
|
if (isIOS) fallback.add(_PayMethod.apple);
|
|
if (isAndroid) fallback.add(_PayMethod.google);
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
/// 底部弹窗:选择支付方式(仅展示后端允许的渠道)
|
|
Future<_PayMethod?> _showPayMethodSheet(List<_PayMethod> allowed) async {
|
|
final context = Get.context;
|
|
final isDark =
|
|
context != null && Theme.of(context).brightness == Brightness.dark;
|
|
final isIOS = GetPlatform.isIOS && !kIsWeb;
|
|
final isAndroid = GetPlatform.isAndroid && !kIsWeb;
|
|
return await Get.bottomSheet<_PayMethod>(
|
|
SafeArea(
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: isDark ? Colors.grey[900] : Colors.white,
|
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(16)),
|
|
),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
child: Container(
|
|
width: 40,
|
|
height: 4,
|
|
decoration: BoxDecoration(
|
|
color: isDark ? Colors.white24 : Colors.black12,
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'选择支付方式',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
if (allowed.contains(_PayMethod.wechat))
|
|
GestureDetector(
|
|
onTap: () => Get.back(result: _PayMethod.wechat),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isDark
|
|
? Colors.white10
|
|
: Colors.black12,
|
|
border: Border.all(
|
|
color: isDark
|
|
? Colors.white24
|
|
: Colors.black12,
|
|
),
|
|
),
|
|
child: Icon(Icons.wechat,
|
|
color: Colors.green[600]),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'微信支付',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (allowed.contains(_PayMethod.alipay))
|
|
GestureDetector(
|
|
onTap: () => Get.back(result: _PayMethod.alipay),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isDark
|
|
? Colors.white10
|
|
: Colors.black12,
|
|
border: Border.all(
|
|
color: isDark
|
|
? Colors.white24
|
|
: Colors.black12,
|
|
),
|
|
),
|
|
child: Icon(Icons.payment,
|
|
color: Colors.blue[600]),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'支付宝',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (allowed.contains(_PayMethod.paypal))
|
|
GestureDetector(
|
|
onTap: () => Get.back(result: _PayMethod.paypal),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isDark
|
|
? Colors.white10
|
|
: Colors.black12,
|
|
border: Border.all(
|
|
color: isDark
|
|
? Colors.white24
|
|
: Colors.black12,
|
|
),
|
|
),
|
|
child: Icon(
|
|
Icons.account_balance_wallet,
|
|
color: Colors.indigo[600],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'PayPal',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isIOS && allowed.contains(_PayMethod.apple))
|
|
GestureDetector(
|
|
onTap: () => Get.back(result: _PayMethod.apple),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isDark
|
|
? Colors.white10
|
|
: Colors.black12,
|
|
border: Border.all(
|
|
color: isDark
|
|
? Colors.white24
|
|
: Colors.black12,
|
|
),
|
|
),
|
|
child: Icon(
|
|
Icons.apple,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Apple 内购',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (isAndroid && allowed.contains(_PayMethod.google))
|
|
GestureDetector(
|
|
onTap: () => Get.back(result: _PayMethod.google),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
color: isDark
|
|
? Colors.white10
|
|
: Colors.black12,
|
|
border: Border.all(
|
|
color: isDark
|
|
? Colors.white24
|
|
: Colors.black12,
|
|
),
|
|
),
|
|
child: Icon(
|
|
Icons.shop,
|
|
color: Colors.green[700],
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Google 内购',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
isScrollControlled: false,
|
|
ignoreSafeArea: false,
|
|
backgroundColor: Colors.transparent,
|
|
);
|
|
}
|
|
|
|
/// 指定微信支付(直接下单并拉起微信)
|
|
Future<bool> payWechat(DBGoods item,
|
|
{PaymentResultCallback? onResult}) async {
|
|
final cents = item.price; // DBGoods.price 单位:分
|
|
final serverUrl = DioManager().baseUrl;
|
|
final paymentData = await _payService.createWechatPayOrder(
|
|
productId: item.id,
|
|
productName: item.name,
|
|
amount: cents,
|
|
notifyUrl: '$serverUrl/api/home/pay_wechatnotify',
|
|
);
|
|
if (paymentData == null) {
|
|
onResult?.call(false, '创建微信订单失败');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
final orderId =
|
|
paymentData['orderId'] as String? ?? _payService.lastOrderId;
|
|
return _payService.wechatPay(
|
|
paymentData: paymentData,
|
|
onResult: (success, msg) async {
|
|
// 先透传给外部回调
|
|
onResult?.call(success, msg);
|
|
if (success) {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
await _confirmOrderAndShow(orderId,
|
|
channel: '微信', initialMessage: msg);
|
|
} else {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
_showPayResultDialog(false, msg ?? '支付失败');
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 指定支付宝支付(直接下单并拉起支付宝)
|
|
Future<bool> payAlipay(DBGoods item,
|
|
{PaymentResultCallback? onResult}) async {
|
|
final cents = item.price; // DBGoods.price 单位:分
|
|
final orderInfo = await _payService.createAlipayOrder(
|
|
productId: item.id,
|
|
productName: item.name,
|
|
amount: cents,
|
|
notifyUrl: 'https://api.ideapsound.com/api/home/pay_alipaynotify',
|
|
);
|
|
if (orderInfo == null) {
|
|
onResult?.call(false, '创建支付宝订单失败');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
final orderId = _payService.lastOrderId; // 创建订单时已缓存
|
|
return _payService.alipay(
|
|
orderInfo: orderInfo,
|
|
onResult: (success, msg) async {
|
|
onResult?.call(success, msg);
|
|
if (success) {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
await _confirmOrderAndShow(orderId,
|
|
channel: '支付宝', initialMessage: msg);
|
|
} else {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
_showPayResultDialog(false, msg ?? '支付失败');
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
/// 指定 Apple 内购(App Store IAP)
|
|
Future<bool> payAppleIAP(DBGoods item,
|
|
{PaymentResultCallback? onResult}) async {
|
|
try {
|
|
// 先创建服务端订单,记录 order_id 以便后续回查
|
|
final orderId = await _payService.createAppleIapOrder(
|
|
productId: item.id,
|
|
productName: item.name,
|
|
amount: item.price,
|
|
);
|
|
if (orderId == null || orderId.isEmpty) {
|
|
onResult?.call(false, '创建 Apple 订单失败');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
// 使用商品id作为内购的 productId(与后台保持一致)
|
|
final productId = item.id;
|
|
// 解析为 App Store 的真实商品ID(支持前缀/映射)
|
|
final iosProductId = _payService.resolveAppleProductId(productId);
|
|
// 调试:在发起购买前先查询并打印该商品的内购详情/错误,便于定位问题
|
|
await _payService.debugQueryProducts({iosProductId});
|
|
return _payService.processApplePayment(
|
|
productId: productId,
|
|
onResult: (success, msg) {
|
|
// 透传并根据阶段展示
|
|
onResult?.call(success, msg);
|
|
if (!success) {
|
|
_showPayResultDialog(false, msg);
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
} else {
|
|
// 支付成功或额度待刷新时弹窗,其余阶段用轻提示
|
|
final showDialog = msg.contains('支付成功');
|
|
if (showDialog) {
|
|
_showPayResultDialog(true, msg);
|
|
// 仅在支付成功阶段,携带校验参数主动查询订单
|
|
_askOrderWithIapVerification();
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
} else {
|
|
Get.snackbar('提示', msg,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(12));
|
|
}
|
|
}
|
|
},
|
|
);
|
|
} catch (e) {
|
|
onResult?.call(false, 'Apple 内购异常: $e');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 指定 Google Play 内购(Play Billing IAP)
|
|
Future<bool> payGoogleIAP(DBGoods item,
|
|
{PaymentResultCallback? onResult}) async {
|
|
try {
|
|
// 先创建服务端订单,记录 order_id 以便后续回查
|
|
final orderId = await _payService.createGoogleIapOrder(
|
|
productId: item.id,
|
|
productName: item.name,
|
|
amount: item.price,
|
|
);
|
|
if (orderId == null || orderId.isEmpty) {
|
|
onResult?.call(false, '创建 Google 订单失败');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
isPaying.value = true;
|
|
_startPayingTimeout();
|
|
|
|
// 使用商品id作为内购的 productId(与后台保持一致)
|
|
final productId = item.id;
|
|
// 调试:查询并打印当前商品的内购详情,便于确认 ID
|
|
await _payService.debugQueryProducts({productId});
|
|
return _payService.processGooglePayment(
|
|
productId: productId,
|
|
onResult: (success, msg) {
|
|
onResult?.call(success, msg);
|
|
if (!success) {
|
|
_showPayResultDialog(false, msg);
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
} else {
|
|
final showDialog = msg.contains('支付成功');
|
|
if (showDialog) {
|
|
_showPayResultDialog(true, msg);
|
|
// 仅在支付成功阶段,携带校验参数主动查询订单
|
|
_askOrderWithIapVerification();
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
} else {
|
|
Get.snackbar('提示', msg,
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(12));
|
|
}
|
|
}
|
|
},
|
|
);
|
|
} catch (e) {
|
|
onResult?.call(false, 'Google 内购异常: $e');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// 调试:打印当前页面所有商品的 Google IAP 查询结果
|
|
Future<void> debugPrintGoogleIapProducts() async {
|
|
if (!GetPlatform.isAndroid || kIsWeb) {
|
|
print('[IAP] 当前平台非 Android 或为 Web,跳过商品查询调试。');
|
|
return;
|
|
}
|
|
try {
|
|
final products = await fetchProducts();
|
|
final ids = products.map((e) => e.id).toSet();
|
|
await _payService.debugQueryProducts(ids);
|
|
} catch (e) {
|
|
print('[IAP] 调试查询商品异常: $e');
|
|
}
|
|
}
|
|
|
|
Future<void> _confirmOrderAndShow(
|
|
String? orderId, {
|
|
required String channel,
|
|
String? initialMessage,
|
|
int retries = 5,
|
|
int intervalMs = 1500,
|
|
}) async {
|
|
// 若无法获取订单号,直接弹成功提示但提醒稍后刷新
|
|
if (orderId == null || orderId.isEmpty) {
|
|
_showPayResultDialog(true, '${channel}支付成功,订单处理中,请稍后查看。');
|
|
return;
|
|
}
|
|
|
|
String message = initialMessage ?? '${channel}支付成功';
|
|
bool success = false;
|
|
try {
|
|
// 简单轮询回查,避免回调延迟
|
|
for (int i = 0; i < retries; i++) {
|
|
final resp = await Api.payverifyorder({'order_id': orderId});
|
|
print("查询订单 resp: $resp");
|
|
final order = resp?['order'] as Map<String, dynamic>?;
|
|
final user = resp?['user'] as Map<String, dynamic>?;
|
|
if (order != null && user != null) {
|
|
final orderStatus = order["status"];
|
|
if (orderStatus == 1) {
|
|
User.instance.updateUserInfo(user);
|
|
_payService.setVipExptime(User.instance.vipexptime);
|
|
success = true;
|
|
break;
|
|
}
|
|
}
|
|
await Future.delayed(Duration(milliseconds: intervalMs));
|
|
}
|
|
|
|
// 若标记成功但未拿到用户信息,使用token登录接口回拉用户资料,确保额度刷新
|
|
if (success) {
|
|
try {
|
|
final response = await Api.tokenlogin();
|
|
if (response is Map && response['user'] is Map<String, dynamic>) {
|
|
User.instance
|
|
.updateUserInfo(response['user'] as Map<String, dynamic>);
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
} catch (e) {
|
|
message = '查询支付结果异常: $e';
|
|
}
|
|
_showPayResultDialog(success, message);
|
|
}
|
|
|
|
/// Apple/Google 内购:无法监听平台回调时,主动查询订单并携带验证数据
|
|
Future<void> _askOrderWithIapVerification() async {
|
|
try {
|
|
final params = await _payService.buildIapAskParams();
|
|
final resp = await Api.payverifyorder(params);
|
|
print('[IAP] 主动查询订单 resp: $resp');
|
|
final order = resp?['order'] as Map<String, dynamic>?;
|
|
final user = resp?['user'] as Map<String, dynamic>?;
|
|
if (order != null) {
|
|
final orderStatus = order['status'];
|
|
if (orderStatus == 1 && user != null) {
|
|
User.instance.updateUserInfo(user);
|
|
_payService.setVipExptime(User.instance.vipexptime);
|
|
Get.snackbar('提示', '订单已确认,额度已刷新',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(12));
|
|
return;
|
|
}
|
|
}
|
|
// 若未确认成功,尝试回拉用户信息确保额度同步
|
|
try {
|
|
final response = await Api.tokenlogin();
|
|
if (response is Map && response['user'] is Map<String, dynamic>) {
|
|
User.instance
|
|
.updateUserInfo(response['user'] as Map<String, dynamic>);
|
|
}
|
|
} catch (_) {}
|
|
} catch (e) {
|
|
print('[IAP] 主动查询订单异常: $e');
|
|
}
|
|
}
|
|
|
|
void _showPayResultDialog(bool success, String message) {
|
|
final context = Get.context;
|
|
final isDark =
|
|
context != null && Theme.of(context).brightness == Brightness.dark;
|
|
Get.dialog(
|
|
Dialog(
|
|
insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
backgroundColor: isDark ? Colors.grey[900] : Colors.white,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: success
|
|
? (isDark ? Colors.green[800] : Colors.green[100])
|
|
: (isDark ? Colors.red[800] : Colors.red[100]),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(
|
|
success ? Icons.check_circle : Icons.error,
|
|
color:
|
|
success ? Colors.green[600] : Colors.redAccent[200],
|
|
size: 28,
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
success ? '支付成功' : '支付失败',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: isDark ? Colors.white : Colors.black,
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
message,
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: isDark ? Colors.white70 : Colors.grey[700],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Divider(color: isDark ? Colors.white24 : Colors.black12),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => Get.back(),
|
|
child: const Text('知道了'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
barrierDismissible: true,
|
|
);
|
|
}
|
|
|
|
void _startPayingTimeout({int seconds = 30}) {
|
|
_payingTimeoutTimer?.cancel();
|
|
_payingTimeoutTimer = Timer(Duration(seconds: seconds), () {
|
|
isPaying.value = false;
|
|
_payingTimeoutTimer = null;
|
|
});
|
|
}
|
|
|
|
void _cancelPayingTimeout() {
|
|
_payingTimeoutTimer?.cancel();
|
|
_payingTimeoutTimer = null;
|
|
}
|
|
|
|
/// 指定 PayPal 支付(创建订单并打开授权链接)
|
|
Future<bool> payPaypal(DBGoods item,
|
|
{PaymentResultCallback? onResult}) async {
|
|
final cents = item.price; // DBGoods.price 单位:分
|
|
final approvalUrl = await _payService.createPaypalOrder(
|
|
productId: item.id,
|
|
productName: item.name,
|
|
amount: cents,
|
|
notifyUrl: 'https://api.ideapsound.com/api/home/pay_paypalnotify',
|
|
);
|
|
if (approvalUrl == null) {
|
|
onResult?.call(false, '创建 PayPal 订单失败');
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
return false;
|
|
}
|
|
final orderId = _payService.lastOrderId; // 创建订单时已缓存
|
|
print('[PayPal] 创建完成,orderId: $orderId, approvalUrl: $approvalUrl');
|
|
|
|
// 启动深链监听:当浏览器重定向到 deepvoiceclient://paypal/return 或 /cancel 时处理
|
|
_deeplinkSub?.cancel();
|
|
_deeplinkSub = _appLinks.uriLinkStream.listen((uri) async {
|
|
if (uri == null) return;
|
|
print('[PayPal] 深链回跳: ${uri.toString()}');
|
|
final token = uri.queryParameters['token'] ?? '';
|
|
if (token.isNotEmpty) {
|
|
print('[PayPal] 回跳 token(订单ID): $token');
|
|
}
|
|
final scheme = uri.scheme;
|
|
final host = uri.host;
|
|
final path = uri.path;
|
|
if (scheme == 'deepvoiceclient' && host == 'paypal') {
|
|
isPaying.value = false;
|
|
if (path == '/return') {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
print('[PayPal] 检测到 /return,使用 orderId 回查: $orderId');
|
|
await _confirmOrderAndShow(
|
|
orderId,
|
|
channel: 'PayPal',
|
|
initialMessage: '支付完成,正在确认',
|
|
retries: 1,
|
|
intervalMs: 0,
|
|
);
|
|
} else if (path == '/cancel') {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
_showPayResultDialog(false, '已取消支付');
|
|
}
|
|
// 一次性监听,处理后即取消
|
|
_deeplinkSub?.cancel();
|
|
_deeplinkSub = null;
|
|
}
|
|
}, onError: (e) {
|
|
isPaying.value = false;
|
|
_cancelPayingTimeout();
|
|
// 忽略监听错误,保持现有轮询逻辑
|
|
});
|
|
return _payService.paypalPay(
|
|
approvalUrl: approvalUrl,
|
|
onResult: (success, msg) async {
|
|
onResult?.call(success, msg);
|
|
if (success) {
|
|
// 仅提示已打开授权页面,不在此处回查与弹窗,等待深链回跳后再处理
|
|
print('[PayPal] 已打开授权页面,等待回跳后再确认。orderId: $orderId');
|
|
Get.snackbar('提示', msg ?? '已打开 PayPal,请在浏览器完成支付',
|
|
snackPosition: SnackPosition.BOTTOM,
|
|
margin: const EdgeInsets.all(12));
|
|
} else {
|
|
// 无法打开授权页才提示失败
|
|
_showPayResultDialog(false, msg ?? '无法打开 PayPal 支付页面');
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _handleInitialDeepLink() async {
|
|
try {
|
|
final uri = await _appLinks.getInitialLink();
|
|
if (uri == null) return;
|
|
print('[PayPal] 冷启动深链: ${uri.toString()}');
|
|
final token = uri.queryParameters['token'] ?? '';
|
|
if (token.isNotEmpty) {
|
|
print('[PayPal] 冷启动 token(订单ID): $token');
|
|
}
|
|
if (uri.scheme == 'deepvoiceclient' && uri.host == 'paypal') {
|
|
if (uri.path == '/return') {
|
|
final orderId = _payService.lastOrderId;
|
|
if (orderId != null) {
|
|
print('[PayPal] 冷启动 /return,使用 orderId 回查: $orderId');
|
|
await _confirmOrderAndShow(
|
|
orderId,
|
|
channel: 'PayPal',
|
|
initialMessage: '支付完成,正在确认',
|
|
retries: 1,
|
|
intervalMs: 0,
|
|
);
|
|
}
|
|
} else if (uri.path == '/cancel') {
|
|
_showPayResultDialog(false, '已取消支付');
|
|
}
|
|
}
|
|
} catch (_) {
|
|
// 忽略异常,维持原有逻辑
|
|
}
|
|
}
|
|
}
|
|
|
|
enum _PayMethod { wechat, alipay, paypal, apple, google }
|
|
|