yumi-flutter/lib/services/payment/google_payment_manager.dart
2026-06-12 14:34:12 +08:00

691 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:provider/provider.dart';
import 'package:yumi/shared/tools/sc_string_utils.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/ui_kit/components/dialog/dialog_base.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart';
import 'package:yumi/shared/business_logic/models/res/sc_google_pay_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_product_config_res.dart';
import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
class AndroidPaymentProcessor extends ChangeNotifier {
StreamSubscription<List<PurchaseDetails>>? _subscription;
final InAppPurchase iap = InAppPurchase.instance;
// 状态管理变量
bool _isAvailable = false;
String _errorMessage = '';
final List<SelecteProductConfig> _products = [];
List<PurchaseDetails> _purchases = [];
///商品列表
List<SCProductConfigRes> productConfigs = [];
// 维护商品类型映射
final Map<String, String> _productTypeMap = {};
final Set<String> _verifyingPurchaseKeys = {};
bool _isReconcilingPastPurchases = false;
DateTime? _lastPastPurchaseReconcileAt;
// 公开访问器
bool get isAvailable => _isAvailable;
String get errorMessage => _errorMessage;
List<SelecteProductConfig> get products => _products;
List<PurchaseDetails> get purchases => _purchases;
late BuildContext context;
Map<String, SCProductConfigRes> productMap = {};
Map<String, String> productTypes = {};
// 初始化支付系统
Future<void> initializePaymentProcessor(BuildContext context) async {
this.context = context;
try {
SCLoadingManager.show(context: context);
// 检查支付是否可用
_isAvailable = await iap.isAvailable();
if (!_isAvailable) {
_errorMessage = 'Google Play payment service is unavailable';
SCTts.show("Google Play payment service is unavailable");
return;
}
_ensurePurchaseStreamListener();
// 先获取商品配置
await fetchProductConfiguration();
// 然后获取谷歌商品信息
await _fetchProducts();
// 最后恢复购买,处理未完成交易
await recoverTransactions();
await reconcilePendingGooglePurchases(force: true);
} catch (e) {
SCTts.show("init fail: $e");
_errorMessage = 'init fail: ${e.toString()}';
} finally {
SCLoadingManager.hide();
notifyListeners();
}
}
void _ensurePurchaseStreamListener() {
_subscription ??= iap.purchaseStream.listen(
_handlePurchase,
onError: (error, stackTrace) => _handleError(error, stackTrace),
);
}
///用户获取商店配置的商品列表
Future fetchProductConfiguration() async {
productMap.clear();
productConfigs = await SCConfigRepositoryImp().productConfig();
int index = 0;
for (var value in productConfigs) {
productMap[value.productPackage!] = value;
productTypes[value.productPackage!] = "consumable";
index = index + 1;
}
notifyListeners();
}
// 获取商品信息
Future<void> _fetchProducts() async {
try {
SCLoadingManager.show(context: context);
notifyListeners();
Set<String> productIds = productTypes.keys.toSet();
ProductDetailsResponse response = await iap.queryProductDetails(
productIds,
);
if (response.notFoundIDs.isNotEmpty) {
_errorMessage = '未找到商品: ${response.notFoundIDs.join(', ')}';
}
var dtails = response.productDetails;
if (dtails.isNotEmpty) {
dtails.sort((a, b) {
int ia = 0;
int ib = 0;
if (a.id.contains(".")) {
ia = SCStringUtils.convertToInteger(a.id.split(".").last);
}
if (b.id.contains(".")) {
ib = SCStringUtils.convertToInteger(b.id.split(".").last);
}
return ia.compareTo(ib);
});
}
int index = 0;
_products.clear();
for (var v in dtails) {
if (index == 0) {
_products.add(SelecteProductConfig(v, true));
} else {
_products.add(SelecteProductConfig(v, false));
}
index++;
}
// 初始化商品类型映射
for (var product in _products) {
if (productTypes.containsKey(product.produc.id)) {
_productTypeMap[product.produc.id] = productTypes[product.produc.id]!;
} else {
_productTypeMap[product.produc.id] = 'nonConsumable'; // 默认类型
}
}
} catch (e) {
SCTts.show("Failed to retrieve the product: $e");
_errorMessage = '获取商品失败: ${e.toString()}';
} finally {
SCLoadingManager.hide();
notifyListeners();
}
}
// 获取商品类型
String _getProductType(String productId) {
if (productId.startsWith('coins.')) {
return 'consumable';
}
return _productTypeMap[productId] ?? 'nonConsumable';
}
// 发起购买
Future<void> processPurchase() async {
ProductDetails? product;
for (final d in _products) {
if (d.isSelecte) {
product = d.produc;
}
}
if (product == null) {
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectaItem);
return;
}
SmartDialog.show(
tag: "showConfirmDialog",
alignment: Alignment.center,
debounce: true,
animationType: SmartAnimationType.fade,
builder: (_) {
return MsgDialog(
title: SCAppLocalizations.of(context)!.tips,
msg: SCAppLocalizations.of(context)!.areYouRureRoRecharge,
btnText: SCAppLocalizations.of(context)!.confirm,
onEnsure: () {
_goBuy(product!);
},
);
},
);
}
Future<void> processFirstRechargePurchase({
required BuildContext context,
required int rechargeAmountCents,
String productPackage = '',
}) async {
this.context = context;
if (!Platform.isAndroid) {
SCTts.show('Google Play payment is only available on Android');
return;
}
if (!_isAvailable || _products.isEmpty || productMap.isEmpty) {
await initializePaymentProcessor(context);
}
if (!_isAvailable) {
return;
}
final product = _findFirstRechargeProduct(
rechargeAmountCents: rechargeAmountCents,
productPackage: productPackage,
);
if (product == null) {
SCTts.show('No matching Google Play product was found');
return;
}
for (final item in _products) {
item.isSelecte = identical(item, product);
}
notifyListeners();
await _goBuy(product.produc);
}
SelecteProductConfig? _findFirstRechargeProduct({
required int rechargeAmountCents,
required String productPackage,
}) {
final targetProduct = productPackage.trim();
if (targetProduct.isNotEmpty) {
for (final product in _products) {
if (product.produc.id == targetProduct) {
return product;
}
}
}
if (rechargeAmountCents <= 0) {
return null;
}
for (final product in _products) {
final config = productMap[product.produc.id];
if (_matchesAmountCents(config?.unitPrice, rechargeAmountCents)) {
return product;
}
if (_matchesAmountCents(product.produc.rawPrice, rechargeAmountCents)) {
return product;
}
}
return null;
}
bool _matchesAmountCents(num? value, int targetCents) {
if (value == null) {
return false;
}
final cents = value >= 20 ? value.round() : (value * 100).round();
return (cents - targetCents).abs() <= 1;
}
// 处理购买结果
void _handlePurchase(List<PurchaseDetails> purchases) {
_purchases = purchases;
for (var purchase in purchases) {
switch (purchase.status) {
case PurchaseStatus.purchased:
_verifyPayment(purchase);
break;
case PurchaseStatus.error:
if (purchase.error != null) {
_handleIAPError(purchase.error!);
// 特别处理 "item-already-owned" 错误
if (purchase.error!.code == 'item-already-owned') {
_handleAlreadyOwnedItem(purchase);
}
}
break;
case PurchaseStatus.pending:
break;
case PurchaseStatus.restored:
// 处理恢复的购买
_verifyPayment(purchase, forceSubmit: true);
break;
default:
break;
}
}
notifyListeners();
}
// 新增:处理已拥有商品的情况
Future<void> _handleAlreadyOwnedItem(PurchaseDetails purchase) async {
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
context,
listen: false,
);
try {
await reconcilePendingGooglePurchases(force: true, showToast: true);
profileManager.balance();
_refreshFirstRechargeRewardState(firstRechargeManager);
} catch (e) {
debugPrint('handle already-owned purchase failed: $e');
}
}
// 验证支付凭证
Future<void> _verifyPayment(
PurchaseDetails purchase, {
bool forceSubmit = false,
bool showToast = true,
}) async {
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
final firstRechargeManager = Provider.of<SCAppGeneralManager>(
context,
listen: false,
);
try {
// 1. 基本验证
if (purchase.verificationData.serverVerificationData.isEmpty) {
_errorMessage = '无效的支付凭证';
if (showToast) {
SCTts.show("Invalid payment voucher");
}
return;
}
// 2. 正常购买流依赖 pendingCompletePurchase启动/回前台补偿流需要允许
// 已确认但仍未消费的一次性商品再次上报后端,否则用户付款后退出 App 会漏单。
if (!forceSubmit && !purchase.pendingCompletePurchase) {
return;
}
final purchaseKey =
purchase.purchaseID ??
purchase.verificationData.serverVerificationData;
if (purchaseKey.isNotEmpty && !_verifyingPurchaseKeys.add(purchaseKey)) {
return;
}
// 3. 获取购买数据
String purchaseData = purchase.verificationData.localVerificationData;
String signature = "";
if (purchase is GooglePlayPurchaseDetails) {
signature = purchase.billingClientPurchase.signature;
}
// 4. 服务器验证
await _submitGooglePayVerification(
purchase.productID,
signature,
purchaseData,
);
// 5. 交付商品
await _deliverProduct(purchase.productID);
// 6. 完成购买
await iap.completePurchase(purchase);
// 7. 如果是消耗型商品,确保消耗
if (_getProductType(purchase.productID) == 'consumable') {
await _ensurePurchaseConsumed(purchase);
}
// 8. 更新用户余额
profileManager.fetchUserProfileData();
profileManager.balance();
_refreshFirstRechargeRewardState(firstRechargeManager);
if (showToast) {
SCTts.show('purchase successful');
}
} catch (e) {
if (showToast) {
SCTts.show("verification failed: $e");
}
_errorMessage = '验证失败: ${e.toString()}';
} finally {
final purchaseKey =
purchase.purchaseID ??
purchase.verificationData.serverVerificationData;
if (purchaseKey.isNotEmpty) {
_verifyingPurchaseKeys.remove(purchaseKey);
}
SCLoadingManager.hide();
notifyListeners();
}
}
Future<void> reconcilePendingGooglePurchases({
BuildContext? context,
bool force = false,
bool showToast = false,
}) async {
if (!Platform.isAndroid) {
return;
}
if (context != null) {
this.context = context;
}
if (_isReconcilingPastPurchases) {
return;
}
final lastCheck = _lastPastPurchaseReconcileAt;
if (!force &&
lastCheck != null &&
DateTime.now().difference(lastCheck) < const Duration(seconds: 20)) {
return;
}
_isReconcilingPastPurchases = true;
_lastPastPurchaseReconcileAt = DateTime.now();
try {
_ensurePurchaseStreamListener();
_isAvailable = await iap.isAvailable();
if (!_isAvailable) {
return;
}
final InAppPurchaseAndroidPlatformAddition androidAddition =
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
final response = await androidAddition.queryPastPurchases();
if (response.error != null) {
debugPrint('query past google purchases failed: ${response.error}');
return;
}
final purchases =
response.pastPurchases.where((purchase) {
final isPurchased =
purchase.status == PurchaseStatus.purchased ||
purchase.status == PurchaseStatus.restored;
final productType = _getProductType(purchase.productID);
final isUnacknowledged =
purchase.pendingCompletePurchase ||
!purchase.billingClientPurchase.isAcknowledged;
// Google 只会返回未消费的一次性商品;即使它已确认,也说明仍可补上报后端。
return isPurchased &&
(isUnacknowledged || productType == 'consumable');
}).toList();
if (purchases.isEmpty) {
return;
}
_purchases = purchases;
for (final purchase in purchases) {
await _verifyPayment(purchase, forceSubmit: true, showToast: showToast);
}
} catch (e) {
debugPrint('reconcile pending google purchases failed: $e');
} finally {
_isReconcilingPastPurchases = false;
notifyListeners();
}
}
Future<SCGooglePayRes> _submitGooglePayVerification(
String product,
String signature,
String purchaseData,
) async {
const api = 'order/purchase-pay/google';
final paramsForLog = <String, dynamic>{
'product': product,
'signature': _clipForDebugLog(signature),
'purchaseData': _clipForDebugLog(purchaseData),
};
if (kDebugMode) {
debugPrint('[GooglePay] request api=$api params=$paramsForLog');
}
final result = await SCConfigRepositoryImp().googlePay(
product,
signature,
purchaseData,
);
if (kDebugMode) {
debugPrint('[GooglePay] response api=$api result=${result.toJson()}');
}
return result;
}
String _clipForDebugLog(String value) {
if (value.length <= 240) {
return value;
}
return '${value.substring(0, 120)}...${value.substring(value.length - 80)}';
}
// 交付商品
Future<void> _deliverProduct(String productId) async {
// 实现您的业务逻辑
// 例如:增加用户余额、解锁功能等
}
void _refreshFirstRechargeRewardState(SCAppGeneralManager manager) {
try {
unawaited(manager.refreshFirstRechargeRewardHome(forceRefresh: true));
} catch (error) {
if (kDebugMode) {
debugPrint('[FirstRecharge][payment] refresh state failed: $error');
}
}
}
// 新增:确保购买被消耗
Future<void> _ensurePurchaseConsumed(PurchaseDetails purchase) async {
try {
// 等待一段时间确保购买完成
await Future.delayed(Duration(seconds: 1));
// 尝试消耗购买
await consumePurchase(purchase);
} catch (e) {
debugPrint('consume purchase failed: $e');
}
}
void _handleError(Object error, StackTrace stackTrace) {
SCTts.show("Payment error: $error");
if (error is IAPError) {
// 处理IAP特定错误
_handleIAPError(error);
} else {
// 处理其他类型的错误
_errorMessage = '未知错误: ${error.toString()}';
SCTts.show(_errorMessage);
}
}
// 添加了专门的IAP错误处理方法
void _handleIAPError(IAPError error) {
_errorMessage = '支付错误: ${error.message} (${error.code})';
// 处理特定错误代码
switch (error.code) {
case 'payment-invalid':
SCTts.show(
'Payment configuration error, please contact customer service',
);
break;
case 'item-already-owned':
SCTts.show('Unfinished purchase detected, processing...');
break;
case 'user-cancelled':
SCTts.show('Purchase cancelled');
break;
case 'service-timeout':
case 'service-unavailable':
SCTts.show(
'Google Play services are temporarily unavailable, please try again later',
);
break;
default:
SCTts.show('Payment failed: ${error.message}');
break;
}
SCLoadingManager.hide();
notifyListeners();
}
// 恢复购买
Future<void> recoverTransactions() async {
try {
SCLoadingManager.show(context: context);
notifyListeners();
await iap.restorePurchases();
// 给恢复操作一些时间
await Future.delayed(Duration(milliseconds: 1000));
} catch (e) {
_errorMessage = '恢复购买失败: ${e.toString()}';
SCTts.show("Failed to restore purchase: ${e.toString()}");
} finally {
SCLoadingManager.hide();
notifyListeners();
}
}
// 消耗型商品手动消耗
Future<void> consumePurchase(PurchaseDetails purchase) async {
try {
if (Platform.isAndroid) {
final InAppPurchaseAndroidPlatformAddition androidAddition =
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
await androidAddition.consumePurchase(purchase);
}
} catch (e) {
_errorMessage = '消耗商品失败: ${e.toString()}';
}
}
// 购买流程
Future<void> _goBuy(ProductDetails product) async {
try {
SCLoadingManager.show(context: context);
notifyListeners();
// 购买前检查是否有未完成的购买
await _checkPendingPurchases();
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: product,
applicationUserName: null,
);
String productType = _getProductType(product.id);
bool success;
if (productType == 'consumable') {
success = await iap.buyConsumable(
purchaseParam: purchaseParam,
autoConsume: false,
);
} else {
success = await iap.buyNonConsumable(purchaseParam: purchaseParam);
}
if (!success) {
_errorMessage = '购买启动失败,请检查网络连接';
SCTts.show(_errorMessage);
}
} catch (e) {
SCTts.show("Purchase failed: $e");
_errorMessage = '购买失败: ${e.toString()}';
} finally {
SCLoadingManager.hide();
notifyListeners();
}
}
// 新增:检查未完成的购买
Future<void> _checkPendingPurchases() async {
try {
await reconcilePendingGooglePurchases();
// 恢复购买以获取所有未完成交易
await iap.restorePurchases();
// 给一点时间处理恢复的购买
await Future.delayed(Duration(milliseconds: 550));
} catch (e) {
debugPrint('check pending purchases failed: $e');
}
}
// 添加调试方法
void logCurrentPurchaseStatus() {
debugPrint('current purchase count: ${_purchases.length}');
}
// 释放资源
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
void chooseProductConfig(int index) {
for (var v in _products) {
v.isSelecte = false;
}
_products[index].isSelecte = true;
notifyListeners();
}
}