优化:谷歌支付补单功能
This commit is contained in:
parent
d2e7c0ac8f
commit
bde8db3673
@ -3,22 +3,106 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
|
||||
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
|
||||
import 'package:pets_hero_ai/core/api/api.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../../core/api/services/payment_api.dart';
|
||||
import '../../core/log/app_logger.dart';
|
||||
import '../../core/user/account_refresh.dart';
|
||||
import '../../core/user/user_state.dart';
|
||||
import 'models/google_pay_purchase_result.dart';
|
||||
import 'models/google_pay_verification_payload.dart';
|
||||
import 'models/unacknowledged_google_pay_purchase.dart';
|
||||
|
||||
const String _kFederationMapKey = 'google_pay_google_order_to_federation';
|
||||
|
||||
/// 调起 Google Play 内购,所有内购均通过本方法发起并返回凭据用于服务端回调。
|
||||
abstract final class GooglePlayPurchaseService {
|
||||
static final _log = AppLogger('GooglePlayPurchase');
|
||||
|
||||
/// 获取当前未核销(未确认)的谷歌支付订单。
|
||||
/// 使用 Android [InAppPurchaseAndroidPlatformAddition.queryPastPurchases] 查询本地/缓存中的购买,
|
||||
/// 筛选 [isAcknowledged == false] 的项。仅 Android 有效,非 Android 返回空列表。
|
||||
/// 可用于应用启动时补发回调或展示待处理订单。
|
||||
static Future<List<UnacknowledgedGooglePayPurchase>> getUnacknowledgedPurchases() async {
|
||||
/// 未确认的购买在 Android 上可能不会出现在 queryPastPurchases 中,但会在应用启动时通过
|
||||
/// purchaseStream 重新下发。此处缓存来自 stream 的 pending 购买,供补单合并使用。
|
||||
static final Map<String, PurchaseDetails> _pendingFromStream = {};
|
||||
static StreamSubscription<List<PurchaseDetails>>? _pendingStreamSub;
|
||||
|
||||
/// 在应用启动时调用(仅 Android),订阅 purchaseStream 以接收「未 complete 的购买」的重新下发。
|
||||
/// 否则 queryPastPurchases 可能查不到未确认订单,导致补单为空。
|
||||
static void startPendingPurchaseListener() {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) return;
|
||||
if (_pendingStreamSub != null) return;
|
||||
final iap = InAppPurchase.instance;
|
||||
_pendingStreamSub =
|
||||
iap.purchaseStream.listen((List<PurchaseDetails> purchases) {
|
||||
for (final p in purchases) {
|
||||
if (p is! GooglePlayPurchaseDetails) continue;
|
||||
if (!p.pendingCompletePurchase) continue;
|
||||
final orderId = p.billingClientPurchase.orderId;
|
||||
if (orderId.isEmpty) continue;
|
||||
_pendingFromStream[orderId] = p;
|
||||
_log.d('purchaseStream 收到待处理订单 orderId=$orderId,已加入补单候选');
|
||||
}
|
||||
}, onError: (e) {
|
||||
_log.w('purchaseStream 错误: $e');
|
||||
});
|
||||
_log.d('已订阅 purchaseStream,用于补单时获取未确认订单');
|
||||
}
|
||||
|
||||
/// 保存「Google orderId → 创建订单时的 federation」,供补单时使用。
|
||||
static Future<void> saveFederationForGoogleOrderId(
|
||||
String googleOrderId, String federation) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = prefs.getString(_kFederationMapKey);
|
||||
final map = json != null
|
||||
? Map<String, String>.from((jsonDecode(json) as Map)
|
||||
.map((k, v) => MapEntry(k.toString(), v.toString())))
|
||||
: <String, String>{};
|
||||
map[googleOrderId] = federation;
|
||||
await prefs.setString(_kFederationMapKey, jsonEncode(map));
|
||||
} catch (e) {
|
||||
_log.w('保存 federation 映射失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 补单时根据 Google orderId 取回创建订单时的 federation,无则返回 null。
|
||||
static Future<String?> getFederationForGoogleOrderId(
|
||||
String googleOrderId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = prefs.getString(_kFederationMapKey);
|
||||
if (json == null) return null;
|
||||
final map = (jsonDecode(json) as Map)
|
||||
.map((k, v) => MapEntry(k.toString(), v.toString()));
|
||||
final v = map[googleOrderId]?.toString();
|
||||
return (v != null && v.isNotEmpty) ? v : null;
|
||||
} catch (e) {
|
||||
_log.w('读取 federation 映射失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 回调成功或补单成功后移除映射。
|
||||
static Future<void> removeFederationForGoogleOrderId(
|
||||
String googleOrderId) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final json = prefs.getString(_kFederationMapKey);
|
||||
if (json == null) return;
|
||||
final map = Map<String, String>.from((jsonDecode(json) as Map)
|
||||
.map((k, v) => MapEntry(k.toString(), v.toString())));
|
||||
map.remove(googleOrderId);
|
||||
await prefs.setString(_kFederationMapKey, jsonEncode(map));
|
||||
} catch (e) {
|
||||
_log.w('移除 federation 映射失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前未消耗的谷歌支付订单(含已确认未 consume 的,用于解除「已拥有此内容」)。
|
||||
/// 使用 [queryPastPurchases] 与 purchaseStream 待处理合并,不区分 isAcknowledged。仅 Android 有效。
|
||||
static Future<List<UnacknowledgedGooglePayPurchase>>
|
||||
getUnacknowledgedPurchases() async {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
_log.d('非 Android,无未核销订单');
|
||||
return [];
|
||||
@ -29,16 +113,74 @@ abstract final class GooglePlayPurchaseService {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
final androidAddition = iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
// 先订阅 stream,这样在 queryPastPurchases 触发 Billing 连接后,未确认订单若通过 stream 下发能被收集
|
||||
startPendingPurchaseListener();
|
||||
final androidAddition =
|
||||
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
final response = await androidAddition.queryPastPurchases();
|
||||
if (response.error != null) {
|
||||
_log.w('queryPastPurchases 错误: ${response.error!.message}');
|
||||
return [];
|
||||
}
|
||||
// response 没有实现 toString,这里用手动遍历的方式打印所有内容
|
||||
_log.d(
|
||||
'queryPastPurchases response contains ${response.pastPurchases.length} pastPurchases.');
|
||||
for (var i = 0; i < response.pastPurchases.length; i++) {
|
||||
final purchase = response.pastPurchases[i];
|
||||
final b = purchase.billingClientPurchase;
|
||||
_log.d('pastPurchase[$i]:');
|
||||
_log.d(' productID: ${purchase.productID}');
|
||||
_log.d(' purchaseID: ${purchase.purchaseID}');
|
||||
_log.d(' transactionDate: ${purchase.transactionDate}');
|
||||
_log.d(' status: ${purchase.status}');
|
||||
_log.d(' error: ${purchase.error}');
|
||||
_log.d(
|
||||
' pendingCompletePurchase: ${purchase.pendingCompletePurchase}');
|
||||
_log.d(' billingClientPurchase:');
|
||||
_log.d(' orderId: ${b.orderId}');
|
||||
_log.d(' purchaseToken: ${b.purchaseToken}');
|
||||
_log.d(' packageName: ${b.packageName}');
|
||||
_log.d(' productID: ${purchase.productID}');
|
||||
_log.d(' purchaseState: ${b.purchaseState}');
|
||||
_log.d(' isAcknowledged: ${b.isAcknowledged}');
|
||||
_log.d(' isAutoRenewing: ${b.isAutoRenewing}');
|
||||
_log.d(' originalJson (length): ${b.originalJson.length}');
|
||||
_log.d(' signature (length): ${b.signature.length}');
|
||||
}
|
||||
for (final purchase in response.pastPurchases) {
|
||||
final b = purchase.billingClientPurchase;
|
||||
final transactionDate = purchase.transactionDate;
|
||||
String? formattedDate;
|
||||
if (transactionDate != null && transactionDate.isNotEmpty) {
|
||||
try {
|
||||
// transactionDate is milliseconds since epoch (string)
|
||||
final millis = int.tryParse(transactionDate);
|
||||
if (millis != null) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(millis,
|
||||
isUtc: false); // Android is local?
|
||||
// Format as yyyy-MM-dd HH:mm:ss
|
||||
formattedDate =
|
||||
"${dt.year.toString().padLeft(4, '0')}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} "
|
||||
"${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}";
|
||||
_log.d('订单日期: $formattedDate');
|
||||
} else {
|
||||
_log.d('订单日期解析失败: $transactionDate');
|
||||
}
|
||||
} catch (e) {
|
||||
_log.d('订单日期格式化异常: $e, 原数据: $transactionDate');
|
||||
}
|
||||
} else {
|
||||
_log.d('订单日期为空');
|
||||
}
|
||||
_log.d(
|
||||
'订单: orderId=${b.orderId}, productId=${purchase.productID}, isAcknowledged=${b.isAcknowledged}, purchaseDataLength=${b.originalJson.length}, signatureLength=${b.signature.length}');
|
||||
}
|
||||
// 包含所有未消耗的购买(不区分 isAcknowledged),避免「已拥有此内容」因已确认未 consume 被漏掉
|
||||
final list = <UnacknowledgedGooglePayPurchase>[];
|
||||
final orderIdsFromQuery = <String>{};
|
||||
for (final p in response.pastPurchases) {
|
||||
final b = p.billingClientPurchase;
|
||||
if (b.isAcknowledged) continue;
|
||||
orderIdsFromQuery.add(b.orderId);
|
||||
list.add(UnacknowledgedGooglePayPurchase(
|
||||
orderId: b.orderId,
|
||||
productId: p.productID,
|
||||
@ -46,9 +188,41 @@ abstract final class GooglePlayPurchaseService {
|
||||
purchaseData: b.originalJson,
|
||||
signature: b.signature,
|
||||
),
|
||||
purchaseDetails: p,
|
||||
));
|
||||
}
|
||||
_log.d('未核销订单数: ${list.length}');
|
||||
// 未确认的购买在 Android 上常不会出现在 queryPastPurchases 中,需合并来自 purchaseStream 的待处理订单。
|
||||
// 等片刻让 stream 在 Billing 连接后有机会收到重新下发的待处理订单。
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1500));
|
||||
for (final entry in _pendingFromStream.entries) {
|
||||
if (orderIdsFromQuery.contains(entry.key)) continue;
|
||||
final p = entry.value;
|
||||
if (p is! GooglePlayPurchaseDetails) continue;
|
||||
final b = p.billingClientPurchase;
|
||||
list.add(UnacknowledgedGooglePayPurchase(
|
||||
orderId: b.orderId,
|
||||
productId: p.productID,
|
||||
payload: GooglePayVerificationPayload(
|
||||
purchaseData: b.originalJson,
|
||||
signature: b.signature,
|
||||
),
|
||||
purchaseDetails: p,
|
||||
));
|
||||
_log.d('未核销订单(来自 stream) orderId=${b.orderId} 已合并');
|
||||
}
|
||||
_log.d(
|
||||
'未核销订单数: ${list.length} (query: ${orderIdsFromQuery.length}, stream: ${_pendingFromStream.length})');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
final u = list[i];
|
||||
_log.d('未核销[$i] orderId=${u.orderId} productId=${u.productId} '
|
||||
'purchaseDataLength=${u.payload.purchaseData.length}');
|
||||
logWithEmbeddedJson(jsonEncode({
|
||||
'orderId': u.orderId,
|
||||
'productId': u.productId,
|
||||
'purchaseData': u.payload.purchaseData,
|
||||
'signatureLength': u.payload.signature.length,
|
||||
}));
|
||||
}
|
||||
return list;
|
||||
} catch (e, st) {
|
||||
_log.w('获取未核销订单失败: $e\n$st');
|
||||
@ -56,9 +230,99 @@ abstract final class GooglePlayPurchaseService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 发起购买并返回服务端回调所需凭据(purchaseData=originalJson, signature)。
|
||||
/// 成功返回 [GooglePayVerificationPayload],取消/失败返回 null。调用方填入 id(federation)、userId 后组 merchant 上报。
|
||||
static Future<GooglePayVerificationPayload?> launchPurchaseAndReturnData(
|
||||
/// 对单笔购买执行 completePurchase,并在 Android 上显式调用 consumePurchase(autoConsume: false 时必须,否则无法再次购买)。
|
||||
/// 正常流程回调成功后请调用此方法,传入 [GooglePayPurchaseResult.purchaseDetails]。
|
||||
static Future<bool> completeAndConsumePurchase(PurchaseDetails purchaseDetails) async {
|
||||
final iap = InAppPurchase.instance;
|
||||
try {
|
||||
iap.completePurchase(purchaseDetails);
|
||||
_log.d('completePurchase 已执行');
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
final androidAddition = iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||
final result = await androidAddition.consumePurchase(purchaseDetails);
|
||||
final ok = result.responseCode == BillingResponse.ok;
|
||||
if (ok) {
|
||||
_log.d('consumePurchase 已执行,可再次购买');
|
||||
} else {
|
||||
_log.w('consumePurchase 未成功 responseCode=${result.responseCode}');
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
return true;
|
||||
} catch (e, st) {
|
||||
_log.w('completePurchase/consumePurchase 异常: $e\n$st');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 执行 completePurchase + consumePurchase(补单内部用)。
|
||||
static Future<bool> _consumePurchase(InAppPurchase iap, UnacknowledgedGooglePayPurchase p) async {
|
||||
return completeAndConsumePurchase(p.purchaseDetails);
|
||||
}
|
||||
|
||||
/// 补单流程:拉取未消耗订单 → 有 federation 则回调后 completePurchase,无 federation 也执行 completePurchase 以解除「已拥有此内容」。
|
||||
/// 仅 Android 且已登录时执行。
|
||||
static Future<void> runOrderRecovery() async {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) return;
|
||||
final userId = UserState.userId.value;
|
||||
if (userId == null || userId.isEmpty) {
|
||||
_log.d('补单跳过:未登录');
|
||||
return;
|
||||
}
|
||||
final pending = await getUnacknowledgedPurchases();
|
||||
if (pending.isEmpty) return;
|
||||
_log.d('补单开始,待处理 ${pending.length} 笔');
|
||||
final iap = InAppPurchase.instance;
|
||||
bool needRefresh = false;
|
||||
for (final p in pending) {
|
||||
try {
|
||||
final federation = await getFederationForGoogleOrderId(p.orderId);
|
||||
if (federation != null && federation.isNotEmpty) {
|
||||
final res = await PaymentApi.googlepay(
|
||||
sample: p.payload.signature,
|
||||
merchant: p.payload.purchaseData,
|
||||
federation: federation,
|
||||
asset: userId,
|
||||
);
|
||||
if (!res.isSuccess) {
|
||||
_log.w('补单失败 orderId=${p.orderId}: ${res.msg}');
|
||||
continue;
|
||||
}
|
||||
final data = res.data is Map<String, dynamic>
|
||||
? res.data as Map<String, dynamic>
|
||||
: null;
|
||||
final line = (data?['line']?.toString() ?? '').toUpperCase();
|
||||
final status = (data?['status']?.toString() ?? '').toUpperCase();
|
||||
final isSuccess = line == 'SUCCESS' || status == 'SUCCESS';
|
||||
_log.d('补单响应 orderId=${p.orderId} data=$data line=$line status=$status isSuccess=$isSuccess');
|
||||
if (isSuccess) {
|
||||
if (await _consumePurchase(iap, p)) {
|
||||
_pendingFromStream.remove(p.orderId);
|
||||
await removeFederationForGoogleOrderId(p.orderId);
|
||||
needRefresh = true;
|
||||
_log.d('补单成功 orderId=${p.orderId} federation=$federation');
|
||||
}
|
||||
} else {
|
||||
_log.w('补单服务端未成功 orderId=${p.orderId} line=$line status=$status');
|
||||
}
|
||||
} else {
|
||||
// 无 federation(如网络失败后重进):仍执行 completePurchase + consumePurchase 以解除「已拥有此内容」
|
||||
_log.d('补单无 federation,仅执行 consume 以解除「已拥有此内容」orderId=${p.orderId}');
|
||||
if (await _consumePurchase(iap, p)) {
|
||||
_pendingFromStream.remove(p.orderId);
|
||||
needRefresh = true;
|
||||
}
|
||||
}
|
||||
} catch (e, st) {
|
||||
_log.w('补单异常 orderId=${p.orderId}: $e\n$st');
|
||||
}
|
||||
}
|
||||
if (needRefresh) await refreshAccount();
|
||||
}
|
||||
|
||||
/// 发起购买并返回服务端回调所需凭据与 [PurchaseDetails]。
|
||||
/// 成功返回 [GooglePayPurchaseResult];调用方应在回调接口返回成功后再对 result.purchaseDetails 执行 [InAppPurchase.instance.completePurchase]。
|
||||
static Future<GooglePayPurchaseResult?> launchPurchaseAndReturnData(
|
||||
String productId) async {
|
||||
_log.d('谷歌支付请求商品 ID(helm): "$productId"');
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
@ -77,7 +341,7 @@ abstract final class GooglePlayPurchaseService {
|
||||
return null;
|
||||
}
|
||||
final product = response.productDetails.first;
|
||||
final completer = Completer<GooglePayVerificationPayload?>();
|
||||
final completer = Completer<GooglePayPurchaseResult?>();
|
||||
StreamSubscription<List<PurchaseDetails>>? sub;
|
||||
sub = iap.purchaseStream.listen(
|
||||
(purchases) {
|
||||
@ -124,17 +388,17 @@ abstract final class GooglePlayPurchaseService {
|
||||
if (p.status == PurchaseStatus.purchased ||
|
||||
p.status == PurchaseStatus.restored) {
|
||||
_log.d('购买成功: ${p.toString()}');
|
||||
if (!completer.isCompleted) {
|
||||
GooglePayVerificationPayload? payload;
|
||||
if (p is GooglePlayPurchaseDetails) {
|
||||
if (!completer.isCompleted && p is GooglePlayPurchaseDetails) {
|
||||
final b = p.billingClientPurchase;
|
||||
payload = GooglePayVerificationPayload(
|
||||
_pendingFromStream[b.orderId] = p;
|
||||
completer.complete(GooglePayPurchaseResult(
|
||||
orderId: b.orderId,
|
||||
payload: GooglePayVerificationPayload(
|
||||
purchaseData: b.originalJson,
|
||||
signature: b.signature,
|
||||
);
|
||||
}
|
||||
iap.completePurchase(p);
|
||||
completer.complete(payload);
|
||||
),
|
||||
purchaseDetails: p,
|
||||
));
|
||||
}
|
||||
sub?.cancel();
|
||||
return;
|
||||
@ -154,6 +418,7 @@ abstract final class GooglePlayPurchaseService {
|
||||
);
|
||||
final success = await iap.buyConsumable(
|
||||
purchaseParam: PurchaseParam(productDetails: product),
|
||||
autoConsume: false,
|
||||
);
|
||||
if (!success) {
|
||||
sub.cancel();
|
||||
|
||||
23
lib/features/recharge/models/google_pay_purchase_result.dart
Normal file
23
lib/features/recharge/models/google_pay_purchase_result.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
|
||||
import 'google_pay_verification_payload.dart';
|
||||
|
||||
/// 谷歌支付发起成功后的结果,含凭据与原始购买详情。
|
||||
/// 调用方应在服务端回调成功(如 line == 'SUCCESS')后再对 [purchaseDetails] 执行
|
||||
/// [InAppPurchase.instance.completePurchase]。
|
||||
class GooglePayPurchaseResult {
|
||||
const GooglePayPurchaseResult({
|
||||
required this.orderId,
|
||||
required this.payload,
|
||||
required this.purchaseDetails,
|
||||
});
|
||||
|
||||
/// Google Play 订单号,用于 googlepay 回调的 federation(直接内购等无 createPayment 时)
|
||||
final String orderId;
|
||||
|
||||
/// 用于 POST /v1/payment/googlepay 的 sample/merchant
|
||||
final GooglePayVerificationPayload payload;
|
||||
|
||||
/// 回调成功后再 [InAppPurchase.instance.completePurchase]
|
||||
final PurchaseDetails purchaseDetails;
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
|
||||
import 'google_pay_verification_payload.dart';
|
||||
|
||||
/// 未核销(未确认)的谷歌支付订单。
|
||||
/// 通过 [GooglePlayPurchaseService.getUnacknowledgedPurchases] 查询得到,
|
||||
/// 可用于补发回调或本地展示。上报服务端时 federation 可用 [orderId](若服务端支持)或先 createPayment 再回调。
|
||||
/// 补单成功后需对 [purchaseDetails] 调用 [InAppPurchase.instance.completePurchase] 完成核销。
|
||||
class UnacknowledgedGooglePayPurchase {
|
||||
const UnacknowledgedGooglePayPurchase({
|
||||
required this.orderId,
|
||||
required this.productId,
|
||||
required this.payload,
|
||||
required this.purchaseDetails,
|
||||
});
|
||||
|
||||
/// Google Play 订单号(purchase 内 orderId)
|
||||
@ -18,4 +22,7 @@ class UnacknowledgedGooglePayPurchase {
|
||||
|
||||
/// 凭据,用于 POST /v1/payment/googlepay 的 sample/merchant;federation/asset 由调用方填入。
|
||||
final GooglePayVerificationPayload payload;
|
||||
|
||||
/// 原始购买详情,补单成功后用于 [InAppPurchase.instance.completePurchase]。
|
||||
final PurchaseDetails purchaseDetails;
|
||||
}
|
||||
|
||||
@ -44,6 +44,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
refreshAccount();
|
||||
_fetchActivities();
|
||||
// 进入充值页时执行补单,处理未核销订单
|
||||
GooglePlayPurchaseService.runOrderRecovery();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -206,8 +208,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
|
||||
await _createOrderAndOpenUrl(
|
||||
userId: userId,
|
||||
activityId: activityId,
|
||||
productId: item.code,
|
||||
item: item,
|
||||
paymentMethod: selected.paymentMethod,
|
||||
subPaymentMethod: selected.subPaymentMethod?.isEmpty == true
|
||||
? null
|
||||
@ -225,17 +226,17 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建订单;若为 Google Pay 则调起内购并上报凭据,否则打开支付链接
|
||||
/// 创建订单;若为 Google Pay 则走统一入口 [_launchGooglePlayPurchase],否则打开支付链接
|
||||
Future<void> _createOrderAndOpenUrl({
|
||||
required String userId,
|
||||
required String activityId,
|
||||
required String productId,
|
||||
required ActivityItem item,
|
||||
required String paymentMethod,
|
||||
String? subPaymentMethod,
|
||||
}) async {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final activityId = item.activityId ?? '';
|
||||
final createRes = await PaymentApi.createPayment(
|
||||
sentinel: ApiConfig.appId,
|
||||
asset: userId,
|
||||
@ -260,54 +261,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
final orderId = data?['federation']?.toString();
|
||||
|
||||
if (_isGooglePay(paymentMethod, subPaymentMethod)) {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
_showSnackBar(context, 'Google Pay is only available on Android.',
|
||||
isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
if (mounted) setState(() => _loadingProductId = null);
|
||||
final payload =
|
||||
await GooglePlayPurchaseService.launchPurchaseAndReturnData(
|
||||
productId);
|
||||
if (!mounted) return;
|
||||
if (payload != null &&
|
||||
orderId != null &&
|
||||
orderId.isNotEmpty) {
|
||||
RechargeScreen._log.d('googlepay 入参: federation=$orderId');
|
||||
final googlepayRes = await PaymentApi.googlepay(
|
||||
sample: payload.signature,
|
||||
merchant: payload.purchaseData,
|
||||
federation: orderId,
|
||||
asset: userId,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (googlepayRes.isSuccess) {
|
||||
final resData = googlepayRes.data is Map<String, dynamic>
|
||||
? googlepayRes.data as Map<String, dynamic>
|
||||
: null;
|
||||
final line = (resData?['line']?.toString() ?? '').toUpperCase();
|
||||
if (line == 'SUCCESS') {
|
||||
await refreshAccount();
|
||||
}
|
||||
if (mounted) {
|
||||
_showSnackBar(context, 'Purchase completed.');
|
||||
}
|
||||
AdjustEvents.trackPurchaseSuccess();
|
||||
} else {
|
||||
_showSnackBar(
|
||||
context,
|
||||
googlepayRes.msg.isNotEmpty
|
||||
? googlepayRes.msg
|
||||
: 'Payment verification failed.',
|
||||
isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
}
|
||||
} else {
|
||||
_showSnackBar(context, 'Purchase was cancelled or failed.',
|
||||
isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
}
|
||||
await _launchGooglePlayPurchase(item,
|
||||
serverOrderId: orderId, userId: userId);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -351,7 +306,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
return r == 'googlepay' || c == 'googlepay';
|
||||
}
|
||||
|
||||
/// 三方为 false 时走谷歌应用内支付
|
||||
/// 三方为 false 时走谷歌应用内支付:先创建订单再调起内购(与第三方选 Google Pay 一致)
|
||||
Future<void> _runGooglePay(ActivityItem item) async {
|
||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||
_showSnackBar(context, 'Google Pay is only available on Android.',
|
||||
@ -359,27 +314,118 @@ class _RechargeScreenState extends State<RechargeScreen>
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
await _launchGooglePlayPurchase(item);
|
||||
final userId = UserState.userId.value;
|
||||
if (userId == null || userId.isEmpty) {
|
||||
_showSnackBar(context, 'Please sign in to continue.', isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
final activityId = item.activityId;
|
||||
if (activityId == null || activityId.isEmpty) {
|
||||
if (mounted) _showSnackBar(context, 'Invalid product', isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final createRes = await PaymentApi.createPayment(
|
||||
sentinel: ApiConfig.appId,
|
||||
asset: userId,
|
||||
warrior: activityId,
|
||||
resource: 'GooglePay',
|
||||
ceremony: 'GooglePay',
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (!createRes.isSuccess) {
|
||||
_showSnackBar(
|
||||
context,
|
||||
createRes.msg.isNotEmpty ? createRes.msg : 'Failed to create order',
|
||||
isError: true,
|
||||
);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
final data = createRes.data is Map<String, dynamic>
|
||||
? createRes.data as Map<String, dynamic>
|
||||
: null;
|
||||
final orderId = data?['federation']?.toString();
|
||||
await _launchGooglePlayPurchase(item,
|
||||
serverOrderId: orderId, userId: userId);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
_showSnackBar(context, 'Payment error: ${e.toString()}', isError: true);
|
||||
}
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
} finally {
|
||||
if (mounted) setState(() => _loadingProductId = null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 调起 Google Play 内购(商品 ID = item.code / helm),凭据用于服务端回调
|
||||
Future<void> _launchGooglePlayPurchase(ActivityItem item) async {
|
||||
/// 谷歌内购统一入口:调起内购 → 回调 googlepay → 成功后再核销并刷新账户。
|
||||
/// [serverOrderId] 有值时(来自 createPayment)用作 federation,否则用 Google orderId。
|
||||
Future<void> _launchGooglePlayPurchase(ActivityItem item,
|
||||
{String? serverOrderId, String? userId}) async {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
if (mounted) setState(() => _loadingProductId = null);
|
||||
final payload =
|
||||
final result =
|
||||
await GooglePlayPurchaseService.launchPurchaseAndReturnData(
|
||||
item.code);
|
||||
if (!mounted) return;
|
||||
if (payload != null) {
|
||||
_showSnackBar(context, 'Purchase completed.');
|
||||
AdjustEvents.trackPurchaseSuccess();
|
||||
// 直接谷歌支付路径:如需回调服务端,需先 createPayment 取得 federation,再上报 googlepay
|
||||
} else {
|
||||
if (result == null) {
|
||||
_showSnackBar(context, 'Purchase was cancelled or failed.',
|
||||
isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
final uid = userId ?? UserState.userId.value;
|
||||
if (uid == null || uid.isEmpty) {
|
||||
_showSnackBar(context, 'Please sign in to confirm your purchase.',
|
||||
isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
return;
|
||||
}
|
||||
final federation = (serverOrderId != null && serverOrderId.isNotEmpty)
|
||||
? serverOrderId
|
||||
: result.orderId;
|
||||
if (serverOrderId != null && serverOrderId.isNotEmpty) {
|
||||
await GooglePlayPurchaseService.saveFederationForGoogleOrderId(
|
||||
result.orderId, serverOrderId);
|
||||
}
|
||||
RechargeScreen._log.d('googlepay 入参: federation=$federation');
|
||||
final googlepayRes = await PaymentApi.googlepay(
|
||||
sample: result.payload.signature,
|
||||
merchant: result.payload.purchaseData,
|
||||
federation: federation,
|
||||
asset: uid,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (googlepayRes.isSuccess) {
|
||||
final resData = googlepayRes.data is Map<String, dynamic>
|
||||
? googlepayRes.data as Map<String, dynamic>
|
||||
: null;
|
||||
final line = (resData?['line']?.toString() ?? '').toUpperCase();
|
||||
if (line == 'SUCCESS') {
|
||||
await GooglePlayPurchaseService.completeAndConsumePurchase(
|
||||
result.purchaseDetails);
|
||||
if (serverOrderId != null && serverOrderId.isNotEmpty) {
|
||||
await GooglePlayPurchaseService.removeFederationForGoogleOrderId(
|
||||
result.orderId);
|
||||
}
|
||||
await refreshAccount();
|
||||
}
|
||||
if (mounted) {
|
||||
_showSnackBar(context, 'Purchase completed.');
|
||||
}
|
||||
AdjustEvents.trackPurchaseSuccess();
|
||||
} else {
|
||||
_showSnackBar(
|
||||
context,
|
||||
googlepayRes.msg.isNotEmpty
|
||||
? googlepayRes.msg
|
||||
: 'Payment verification failed.',
|
||||
isError: true);
|
||||
AdjustEvents.trackPaymentFailed();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user