|
|
@ -1,8 +1,12 @@ |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'dart:convert'; |
|
|
import 'dart:convert'; |
|
|
|
|
|
import 'dart:async'; |
|
|
|
|
|
import 'package:flutter/foundation.dart'; |
|
|
|
|
|
import 'package:in_app_purchase/in_app_purchase.dart'; |
|
|
import 'package:voitrans/data/services/network/api.dart'; |
|
|
import 'package:voitrans/data/services/network/api.dart'; |
|
|
import 'package:tobias/tobias.dart'; |
|
|
import 'package:tobias/tobias.dart'; |
|
|
import 'package:url_launcher/url_launcher.dart'; |
|
|
import 'package:url_launcher/url_launcher.dart'; |
|
|
|
|
|
import 'package:voitrans/data/models/user_Info.dart'; |
|
|
|
|
|
|
|
|
import '../../modules/login/controllers/weixinLogin.dart'; |
|
|
import '../../modules/login/controllers/weixinLogin.dart'; |
|
|
|
|
|
|
|
|
@ -13,6 +17,11 @@ class PayService extends GetxService { |
|
|
late FlWX _flwx; |
|
|
late FlWX _flwx; |
|
|
late Tobias _tobias; // 添加 Tobias 实例 |
|
|
late Tobias _tobias; // 添加 Tobias 实例 |
|
|
String? _lastOrderId; // 最近一次创建的订单号 |
|
|
String? _lastOrderId; // 最近一次创建的订单号 |
|
|
|
|
|
// 内购 |
|
|
|
|
|
final InAppPurchase _inAppPurchase = InAppPurchase.instance; |
|
|
|
|
|
StreamSubscription<List<PurchaseDetails>>? _iapSubscription; |
|
|
|
|
|
final RxBool iapAvailable = false.obs; |
|
|
|
|
|
PaymentResultCallback? _iapOnResult; |
|
|
|
|
|
|
|
|
String? get lastOrderId => _lastOrderId; |
|
|
String? get lastOrderId => _lastOrderId; |
|
|
|
|
|
|
|
|
@ -33,6 +42,7 @@ class PayService extends GetxService { |
|
|
// 初始化支付宝SDK |
|
|
// 初始化支付宝SDK |
|
|
_tobias = Tobias(); // 初始化 Tobias 实例 |
|
|
_tobias = Tobias(); // 初始化 Tobias 实例 |
|
|
_initAlipay(); |
|
|
_initAlipay(); |
|
|
|
|
|
_initIAP(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 初始化支付宝SDK |
|
|
/// 初始化支付宝SDK |
|
|
@ -46,6 +56,246 @@ class PayService extends GetxService { |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 初始化内购(Apple/Google) |
|
|
|
|
|
Future<void> _initIAP() async { |
|
|
|
|
|
try { |
|
|
|
|
|
// Web 环境不支持内购 |
|
|
|
|
|
if (kIsWeb) { |
|
|
|
|
|
iapAvailable.value = false; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
final available = await _inAppPurchase.isAvailable(); |
|
|
|
|
|
iapAvailable.value = available; |
|
|
|
|
|
if (!available) return; |
|
|
|
|
|
|
|
|
|
|
|
// 监听购买结果流 |
|
|
|
|
|
_iapSubscription?.cancel(); |
|
|
|
|
|
_iapSubscription = _inAppPurchase.purchaseStream.listen( |
|
|
|
|
|
_handlePurchaseUpdates, |
|
|
|
|
|
onDone: () => _iapSubscription?.cancel(), |
|
|
|
|
|
onError: (e) { |
|
|
|
|
|
_iapOnResult?.call(false, '内购异常: $e'); |
|
|
|
|
|
}, |
|
|
|
|
|
); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
print('初始化内购失败: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 调试:批量查询并打印内购商品详情(用于确认商品ID是否存在) |
|
|
|
|
|
Future<void> debugQueryProducts(Set<String> productIds) async { |
|
|
|
|
|
try { |
|
|
|
|
|
// 先原样打印传入的 productIds,便于核对入参 |
|
|
|
|
|
print('[IAP] debugQueryProducts 输入 IDs(Set): $productIds'); |
|
|
|
|
|
final idsList = productIds.toList(); |
|
|
|
|
|
print('[IAP] debugQueryProducts 输入 IDs(List): ${idsList.join(', ')}'); |
|
|
|
|
|
for (final id in productIds) { |
|
|
|
|
|
print('[IAP] 待查询商品ID: $id'); |
|
|
|
|
|
} |
|
|
|
|
|
// 仅在非 Web 平台执行;平台信息用于日志 |
|
|
|
|
|
await _initIAP(); |
|
|
|
|
|
final platform = _platformString(); |
|
|
|
|
|
if (!iapAvailable.value) { |
|
|
|
|
|
print('[IAP-$platform] 当前平台内购不可用,无法查询商品。'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
print('[IAP-$platform] 开始查询商品,IDs=${productIds.join(', ')}'); |
|
|
|
|
|
final resp = await _inAppPurchase.queryProductDetails(productIds); |
|
|
|
|
|
if (resp.error != null) { |
|
|
|
|
|
print('[IAP-$platform] 查询错误: ${resp.error}'); |
|
|
|
|
|
} |
|
|
|
|
|
if (resp.productDetails.isEmpty) { |
|
|
|
|
|
print('[IAP-$platform] 未返回任何商品。notFound=${resp.notFoundIDs}'); |
|
|
|
|
|
} else { |
|
|
|
|
|
for (final p in resp.productDetails) { |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP-$platform] 商品: id=${p.id}, title=${p.title}, price=${p.price}, currency=${p.currencyCode}'); |
|
|
|
|
|
} |
|
|
|
|
|
if (resp.notFoundIDs.isNotEmpty) { |
|
|
|
|
|
print('[IAP-$platform] 未找到的IDs: ${resp.notFoundIDs}'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
print('[IAP] 调试查询异常: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<ProductDetails?> _queryIapProduct(String productId) async { |
|
|
|
|
|
try { |
|
|
|
|
|
final pid = productId.trim(); |
|
|
|
|
|
print('[IAP] 开始查询单个商品: id="$pid"'); |
|
|
|
|
|
final resp = await _inAppPurchase.queryProductDetails({pid}); |
|
|
|
|
|
if (resp.error != null) { |
|
|
|
|
|
print('[IAP] 查询商品失败: ${resp.error}'); |
|
|
|
|
|
} |
|
|
|
|
|
if (resp.productDetails.isEmpty) { |
|
|
|
|
|
print('[IAP] 查询结果为空。notFound=${resp.notFoundIDs}'); |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
for (final p in resp.productDetails) { |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP] 命中商品: id=${p.id}, title=${p.title}, price=${p.price}, currency=${p.currencyCode}'); |
|
|
|
|
|
} |
|
|
|
|
|
// 避免 Dart 泛型推断在 Android 平台出现 orElse 类型不匹配(ProductDetails vs GooglePlayProductDetails) |
|
|
|
|
|
ProductDetails? matched; |
|
|
|
|
|
for (final p in resp.productDetails) { |
|
|
|
|
|
if (p.id == pid) { |
|
|
|
|
|
matched = p; |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
return matched ?? resp.productDetails.first; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
print('[IAP] 查询内购商品异常: $e'); |
|
|
|
|
|
return null; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// Apple/Google 内购统一流程(消耗型) |
|
|
|
|
|
Future<bool> _processIapPayment({ |
|
|
|
|
|
required String productId, |
|
|
|
|
|
PaymentResultCallback? onResult, |
|
|
|
|
|
}) async { |
|
|
|
|
|
_iapOnResult = onResult; |
|
|
|
|
|
print( |
|
|
|
|
|
'[IAP] 准备发起购买: id="${productId.trim()}" iapAvailable=${iapAvailable.value}'); |
|
|
|
|
|
if (!iapAvailable.value) { |
|
|
|
|
|
onResult?.call(false, '内购不可用'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
final product = await _queryIapProduct(productId); |
|
|
|
|
|
if (product == null) { |
|
|
|
|
|
onResult?.call(false, '未找到内购商品: $productId'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
try { |
|
|
|
|
|
final purchaseParam = PurchaseParam(productDetails: product); |
|
|
|
|
|
final ok = await _inAppPurchase.buyConsumable( |
|
|
|
|
|
purchaseParam: purchaseParam, |
|
|
|
|
|
autoConsume: true, |
|
|
|
|
|
); |
|
|
|
|
|
if (!ok) { |
|
|
|
|
|
onResult?.call(false, '无法发起内购'); |
|
|
|
|
|
} else { |
|
|
|
|
|
onResult?.call(true, '已发起内购,请完成支付'); |
|
|
|
|
|
} |
|
|
|
|
|
return ok; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
onResult?.call(false, '发起内购失败: $e'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> _handlePurchaseUpdates(List<PurchaseDetails> detailsList) async { |
|
|
|
|
|
for (final details in detailsList) { |
|
|
|
|
|
switch (details.status) { |
|
|
|
|
|
case PurchaseStatus.pending: |
|
|
|
|
|
_iapOnResult?.call(true, '支付处理中…'); |
|
|
|
|
|
break; |
|
|
|
|
|
case PurchaseStatus.error: |
|
|
|
|
|
_iapOnResult?.call(false, details.error?.message ?? '支付错误'); |
|
|
|
|
|
break; |
|
|
|
|
|
case PurchaseStatus.purchased: |
|
|
|
|
|
case PurchaseStatus.restored: |
|
|
|
|
|
// 完成购买并进行服务端收据验证 |
|
|
|
|
|
try { |
|
|
|
|
|
await _verifyIapWithServer(details); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
_iapOnResult?.call(false, '收据验证失败: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
break; |
|
|
|
|
|
case PurchaseStatus.canceled: |
|
|
|
|
|
_iapOnResult?.call(false, '已取消支付'); |
|
|
|
|
|
break; |
|
|
|
|
|
} |
|
|
|
|
|
if (details.pendingCompletePurchase) { |
|
|
|
|
|
try { |
|
|
|
|
|
await _inAppPurchase.completePurchase(details); |
|
|
|
|
|
} catch (_) {} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> _verifyIapWithServer(PurchaseDetails details) async { |
|
|
|
|
|
final platform = _platformString(); |
|
|
|
|
|
final verificationData = |
|
|
|
|
|
details.verificationData.serverVerificationData ?? ''; |
|
|
|
|
|
final transactionId = details.purchaseID ?? ''; |
|
|
|
|
|
final params = { |
|
|
|
|
|
'platform': platform, |
|
|
|
|
|
'product_id': details.productID, |
|
|
|
|
|
'transaction_id': transactionId, |
|
|
|
|
|
'server_verification_data': verificationData, |
|
|
|
|
|
}; |
|
|
|
|
|
try { |
|
|
|
|
|
final resp = await Api.payiapverify(params); |
|
|
|
|
|
// 兼容多种返回结构,优先拿到用户信息 |
|
|
|
|
|
Map<String, dynamic>? user; |
|
|
|
|
|
if (resp is Map) { |
|
|
|
|
|
if (resp['user'] is Map<String, dynamic>) { |
|
|
|
|
|
user = resp['user'] as Map<String, dynamic>; |
|
|
|
|
|
} else if (resp['data'] is Map && |
|
|
|
|
|
(resp['data']['user'] is Map<String, dynamic>)) { |
|
|
|
|
|
user = resp['data']['user'] as Map<String, dynamic>; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (user != null) { |
|
|
|
|
|
User.instance.updateUserInfo(user); |
|
|
|
|
|
_iapOnResult?.call(true, '支付成功'); |
|
|
|
|
|
} else { |
|
|
|
|
|
_iapOnResult?.call(true, '支付成功,额度将稍后刷新'); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
_iapOnResult?.call(false, '服务器验证异常: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
String _platformString() { |
|
|
|
|
|
if (kIsWeb) return 'web'; |
|
|
|
|
|
switch (defaultTargetPlatform) { |
|
|
|
|
|
case TargetPlatform.iOS: |
|
|
|
|
|
return 'ios'; |
|
|
|
|
|
case TargetPlatform.android: |
|
|
|
|
|
return 'android'; |
|
|
|
|
|
case TargetPlatform.macOS: |
|
|
|
|
|
return 'macos'; |
|
|
|
|
|
case TargetPlatform.windows: |
|
|
|
|
|
return 'windows'; |
|
|
|
|
|
case TargetPlatform.linux: |
|
|
|
|
|
return 'linux'; |
|
|
|
|
|
case TargetPlatform.fuchsia: |
|
|
|
|
|
return 'fuchsia'; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 发起 Apple 内购 |
|
|
|
|
|
Future<bool> processApplePayment({ |
|
|
|
|
|
required String productId, |
|
|
|
|
|
PaymentResultCallback? onResult, |
|
|
|
|
|
}) async { |
|
|
|
|
|
if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) { |
|
|
|
|
|
onResult?.call(false, '当前平台不支持 Apple 内购'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
// 重新初始化内购,避免早期可用性判断为 false |
|
|
|
|
|
await _initIAP(); |
|
|
|
|
|
return _processIapPayment(productId: productId, onResult: onResult); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 发起 Google Play 内购 |
|
|
|
|
|
Future<bool> processGooglePayment({ |
|
|
|
|
|
required String productId, |
|
|
|
|
|
PaymentResultCallback? onResult, |
|
|
|
|
|
}) async { |
|
|
|
|
|
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { |
|
|
|
|
|
onResult?.call(false, '当前平台不支持 Google 内购'); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
// 重新初始化内购,避免早期可用性判断为 false |
|
|
|
|
|
await _initIAP(); |
|
|
|
|
|
return _processIapPayment(productId: productId, onResult: onResult); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
/// 查询支付结果 |
|
|
/// 查询支付结果 |
|
|
/// |
|
|
/// |
|
|
/// [orderId] 订单ID |
|
|
/// [orderId] 订单ID |
|
|
|