This commit is contained in:
roxy 2026-06-12 14:34:12 +08:00
parent 142dcebaae
commit 3872dc03b1
7 changed files with 183 additions and 35 deletions

View File

@ -524,7 +524,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 161; CURRENT_PROJECT_VERSION = 162;
DEVELOPMENT_TEAM = S9YG87C297; DEVELOPMENT_TEAM = S9YG87C297;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -533,7 +533,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -715,7 +715,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 161; CURRENT_PROJECT_VERSION = 162;
DEVELOPMENT_TEAM = S9YG87C297; DEVELOPMENT_TEAM = S9YG87C297;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -724,7 +724,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -744,7 +744,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 161; CURRENT_PROJECT_VERSION = 162;
DEVELOPMENT_TEAM = S9YG87C297; DEVELOPMENT_TEAM = S9YG87C297;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -753,7 +753,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -6,6 +6,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:fluro/fluro.dart' as fluro; import 'package:fluro/fluro.dart' as fluro;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -299,6 +300,7 @@ class _YumiApplicationState extends State<YumiApplication>
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_initRouter(); _initRouter();
unawaited(_initLink()); unawaited(_initLink());
unawaited(_reconcilePendingGooglePurchases(force: true));
}); });
} }
@ -325,6 +327,24 @@ class _YumiApplicationState extends State<YumiApplication>
context.read<RtcProvider>().releaseRtcEngineForAppTermination(), context.read<RtcProvider>().releaseRtcEngineForAppTermination(),
); );
} }
if (state == AppLifecycleState.resumed) {
unawaited(_reconcilePendingGooglePurchases());
}
}
Future<void> _reconcilePendingGooglePurchases({bool force = false}) async {
if (!Platform.isAndroid) {
return;
}
try {
await context
.read<AndroidPaymentProcessor>()
.reconcilePendingGooglePurchases(context: context, force: force);
} catch (error) {
if (kDebugMode) {
debugPrint('reconcile pending google purchases failed: $error');
}
}
} }
@override @override

View File

@ -15,6 +15,7 @@ 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/dialog/dialog_base.dart';
import 'package:yumi/ui_kit/components/sc_tts.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/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/shared/business_logic/models/res/sc_product_config_res.dart';
import 'package:yumi/modules/wallet/recharge/recharge_page.dart'; import 'package:yumi/modules/wallet/recharge/recharge_page.dart';
@ -33,6 +34,9 @@ class AndroidPaymentProcessor extends ChangeNotifier {
// //
final Map<String, String> _productTypeMap = {}; final Map<String, String> _productTypeMap = {};
final Set<String> _verifyingPurchaseKeys = {};
bool _isReconcilingPastPurchases = false;
DateTime? _lastPastPurchaseReconcileAt;
// 访 // 访
bool get isAvailable => _isAvailable; bool get isAvailable => _isAvailable;
@ -60,12 +64,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
SCTts.show("Google Play payment service is unavailable"); SCTts.show("Google Play payment service is unavailable");
return; return;
} }
_subscription?.cancel(); _ensurePurchaseStreamListener();
//
_subscription = iap.purchaseStream.listen(
_handlePurchase,
onError: (error, stackTrace) => _handleError(error, stackTrace),
);
// //
await fetchProductConfiguration(); await fetchProductConfiguration();
@ -75,6 +74,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
// //
await recoverTransactions(); await recoverTransactions();
await reconcilePendingGooglePurchases(force: true);
} catch (e) { } catch (e) {
SCTts.show("init fail: $e"); SCTts.show("init fail: $e");
_errorMessage = 'init fail: ${e.toString()}'; _errorMessage = 'init fail: ${e.toString()}';
@ -84,6 +84,13 @@ class AndroidPaymentProcessor extends ChangeNotifier {
} }
} }
void _ensurePurchaseStreamListener() {
_subscription ??= iap.purchaseStream.listen(
_handlePurchase,
onError: (error, stackTrace) => _handleError(error, stackTrace),
);
}
/// ///
Future fetchProductConfiguration() async { Future fetchProductConfiguration() async {
productMap.clear(); productMap.clear();
@ -155,6 +162,9 @@ class AndroidPaymentProcessor extends ChangeNotifier {
// //
String _getProductType(String productId) { String _getProductType(String productId) {
if (productId.startsWith('coins.')) {
return 'consumable';
}
return _productTypeMap[productId] ?? 'nonConsumable'; return _productTypeMap[productId] ?? 'nonConsumable';
} }
@ -283,7 +293,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
case PurchaseStatus.restored: case PurchaseStatus.restored:
// //
_verifyPayment(purchase); _verifyPayment(purchase, forceSubmit: true);
break; break;
default: default:
break; break;
@ -304,21 +314,20 @@ class AndroidPaymentProcessor extends ChangeNotifier {
listen: false, listen: false,
); );
try { try {
// await reconcilePendingGooglePurchases(force: true, showToast: true);
if (_getProductType(purchase.productID) == 'consumable') { profileManager.balance();
await consumePurchase(purchase); _refreshFirstRechargeRewardState(firstRechargeManager);
//
profileManager.balance();
_refreshFirstRechargeRewardState(firstRechargeManager);
SCTts.show('outstanding purchases have been reinstated');
}
} catch (e) { } catch (e) {
debugPrint('handle already-owned purchase failed: $e'); debugPrint('handle already-owned purchase failed: $e');
} }
} }
// //
Future<void> _verifyPayment(PurchaseDetails purchase) async { Future<void> _verifyPayment(
PurchaseDetails purchase, {
bool forceSubmit = false,
bool showToast = true,
}) async {
final profileManager = Provider.of<SocialChatUserProfileManager>( final profileManager = Provider.of<SocialChatUserProfileManager>(
context, context,
listen: false, listen: false,
@ -331,12 +340,22 @@ class AndroidPaymentProcessor extends ChangeNotifier {
// 1. // 1.
if (purchase.verificationData.serverVerificationData.isEmpty) { if (purchase.verificationData.serverVerificationData.isEmpty) {
_errorMessage = '无效的支付凭证'; _errorMessage = '无效的支付凭证';
SCTts.show("Invalid payment voucher"); if (showToast) {
SCTts.show("Invalid payment voucher");
}
return; return;
} }
// 2. // 2. pendingCompletePurchase/
if (!purchase.pendingCompletePurchase) { // 退 App
if (!forceSubmit && !purchase.pendingCompletePurchase) {
return;
}
final purchaseKey =
purchase.purchaseID ??
purchase.verificationData.serverVerificationData;
if (purchaseKey.isNotEmpty && !_verifyingPurchaseKeys.add(purchaseKey)) {
return; return;
} }
@ -349,7 +368,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
} }
// 4. // 4.
await SCConfigRepositoryImp().googlePay( await _submitGooglePayVerification(
purchase.productID, purchase.productID,
signature, signature,
purchaseData, purchaseData,
@ -371,16 +390,125 @@ class AndroidPaymentProcessor extends ChangeNotifier {
profileManager.balance(); profileManager.balance();
_refreshFirstRechargeRewardState(firstRechargeManager); _refreshFirstRechargeRewardState(firstRechargeManager);
SCTts.show('purchase successful'); if (showToast) {
SCTts.show('purchase successful');
}
} catch (e) { } catch (e) {
SCTts.show("verification failed: $e"); if (showToast) {
SCTts.show("verification failed: $e");
}
_errorMessage = '验证失败: ${e.toString()}'; _errorMessage = '验证失败: ${e.toString()}';
} finally { } finally {
final purchaseKey =
purchase.purchaseID ??
purchase.verificationData.serverVerificationData;
if (purchaseKey.isNotEmpty) {
_verifyingPurchaseKeys.remove(purchaseKey);
}
SCLoadingManager.hide(); SCLoadingManager.hide();
notifyListeners(); 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 { Future<void> _deliverProduct(String productId) async {
// //
@ -506,7 +634,7 @@ class AndroidPaymentProcessor extends ChangeNotifier {
if (productType == 'consumable') { if (productType == 'consumable') {
success = await iap.buyConsumable( success = await iap.buyConsumable(
purchaseParam: purchaseParam, purchaseParam: purchaseParam,
autoConsume: kReleaseMode, // autoConsume: false,
); );
} else { } else {
success = await iap.buyNonConsumable(purchaseParam: purchaseParam); success = await iap.buyNonConsumable(purchaseParam: purchaseParam);
@ -528,6 +656,8 @@ class AndroidPaymentProcessor extends ChangeNotifier {
// //
Future<void> _checkPendingPurchases() async { Future<void> _checkPendingPurchases() async {
try { try {
await reconcilePendingGooglePurchases();
// //
await iap.restorePurchases(); await iap.restorePurchases();

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -1336,7 +1335,7 @@ class _RoomRedPacketRechargeSheetState
SCLoadingManager.show(); SCLoadingManager.show();
final success = await processor.iap.buyConsumable( final success = await processor.iap.buyConsumable(
purchaseParam: PurchaseParam(productDetails: product.produc), purchaseParam: PurchaseParam(productDetails: product.produc),
autoConsume: kReleaseMode, autoConsume: false,
); );
if (!success) { if (!success) {
SCTts.show('Purchase failed, please check your network connection'); SCTts.show('Purchase failed, please check your network connection');

View File

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -367,7 +366,7 @@ class _RoomRechargeBottomSheetState extends State<RoomRechargeBottomSheet> {
SCLoadingManager.show(); SCLoadingManager.show();
final success = await processor.iap.buyConsumable( final success = await processor.iap.buyConsumable(
purchaseParam: PurchaseParam(productDetails: product.produc), purchaseParam: PurchaseParam(productDetails: product.produc),
autoConsume: kReleaseMode, autoConsume: false,
); );
if (!success) { if (!success) {
SCTts.show('Purchase failed, please check your network connection'); SCTts.show('Purchase failed, please check your network connection');

View File

@ -42,6 +42,6 @@ android {
dependencies { dependencies {
implementation "com.tencent.tav:libpag:4.3.68" implementation "com.tencent.tav:libpag:4.5.70"
implementation 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
} }

View File

@ -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: 1.6.1+161 version: 1.6.2+162
environment: environment: