优化:谷歌支付补单功能
This commit is contained in:
parent
d2e7c0ac8f
commit
bde8db3673
@ -3,22 +3,106 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:in_app_purchase/in_app_purchase.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:in_app_purchase_android/in_app_purchase_android.dart';
|
||||||
import 'package:pets_hero_ai/core/api/api.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/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/google_pay_verification_payload.dart';
|
||||||
import 'models/unacknowledged_google_pay_purchase.dart';
|
import 'models/unacknowledged_google_pay_purchase.dart';
|
||||||
|
|
||||||
|
const String _kFederationMapKey = 'google_pay_google_order_to_federation';
|
||||||
|
|
||||||
/// 调起 Google Play 内购,所有内购均通过本方法发起并返回凭据用于服务端回调。
|
/// 调起 Google Play 内购,所有内购均通过本方法发起并返回凭据用于服务端回调。
|
||||||
abstract final class GooglePlayPurchaseService {
|
abstract final class GooglePlayPurchaseService {
|
||||||
static final _log = AppLogger('GooglePlayPurchase');
|
static final _log = AppLogger('GooglePlayPurchase');
|
||||||
|
|
||||||
/// 获取当前未核销(未确认)的谷歌支付订单。
|
/// 未确认的购买在 Android 上可能不会出现在 queryPastPurchases 中,但会在应用启动时通过
|
||||||
/// 使用 Android [InAppPurchaseAndroidPlatformAddition.queryPastPurchases] 查询本地/缓存中的购买,
|
/// purchaseStream 重新下发。此处缓存来自 stream 的 pending 购买,供补单合并使用。
|
||||||
/// 筛选 [isAcknowledged == false] 的项。仅 Android 有效,非 Android 返回空列表。
|
static final Map<String, PurchaseDetails> _pendingFromStream = {};
|
||||||
/// 可用于应用启动时补发回调或展示待处理订单。
|
static StreamSubscription<List<PurchaseDetails>>? _pendingStreamSub;
|
||||||
static Future<List<UnacknowledgedGooglePayPurchase>> getUnacknowledgedPurchases() async {
|
|
||||||
|
/// 在应用启动时调用(仅 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) {
|
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||||
_log.d('非 Android,无未核销订单');
|
_log.d('非 Android,无未核销订单');
|
||||||
return [];
|
return [];
|
||||||
@ -29,16 +113,74 @@ abstract final class GooglePlayPurchaseService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
final androidAddition = iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
// 先订阅 stream,这样在 queryPastPurchases 触发 Billing 连接后,未确认订单若通过 stream 下发能被收集
|
||||||
|
startPendingPurchaseListener();
|
||||||
|
final androidAddition =
|
||||||
|
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||||
final response = await androidAddition.queryPastPurchases();
|
final response = await androidAddition.queryPastPurchases();
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
_log.w('queryPastPurchases 错误: ${response.error!.message}');
|
_log.w('queryPastPurchases 错误: ${response.error!.message}');
|
||||||
return [];
|
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 list = <UnacknowledgedGooglePayPurchase>[];
|
||||||
|
final orderIdsFromQuery = <String>{};
|
||||||
for (final p in response.pastPurchases) {
|
for (final p in response.pastPurchases) {
|
||||||
final b = p.billingClientPurchase;
|
final b = p.billingClientPurchase;
|
||||||
if (b.isAcknowledged) continue;
|
orderIdsFromQuery.add(b.orderId);
|
||||||
list.add(UnacknowledgedGooglePayPurchase(
|
list.add(UnacknowledgedGooglePayPurchase(
|
||||||
orderId: b.orderId,
|
orderId: b.orderId,
|
||||||
productId: p.productID,
|
productId: p.productID,
|
||||||
@ -46,9 +188,41 @@ abstract final class GooglePlayPurchaseService {
|
|||||||
purchaseData: b.originalJson,
|
purchaseData: b.originalJson,
|
||||||
signature: b.signature,
|
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;
|
return list;
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
_log.w('获取未核销订单失败: $e\n$st');
|
_log.w('获取未核销订单失败: $e\n$st');
|
||||||
@ -56,9 +230,99 @@ abstract final class GooglePlayPurchaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发起购买并返回服务端回调所需凭据(purchaseData=originalJson, signature)。
|
/// 对单笔购买执行 completePurchase,并在 Android 上显式调用 consumePurchase(autoConsume: false 时必须,否则无法再次购买)。
|
||||||
/// 成功返回 [GooglePayVerificationPayload],取消/失败返回 null。调用方填入 id(federation)、userId 后组 merchant 上报。
|
/// 正常流程回调成功后请调用此方法,传入 [GooglePayPurchaseResult.purchaseDetails]。
|
||||||
static Future<GooglePayVerificationPayload?> launchPurchaseAndReturnData(
|
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 {
|
String productId) async {
|
||||||
_log.d('谷歌支付请求商品 ID(helm): "$productId"');
|
_log.d('谷歌支付请求商品 ID(helm): "$productId"');
|
||||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||||
@ -77,7 +341,7 @@ abstract final class GooglePlayPurchaseService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final product = response.productDetails.first;
|
final product = response.productDetails.first;
|
||||||
final completer = Completer<GooglePayVerificationPayload?>();
|
final completer = Completer<GooglePayPurchaseResult?>();
|
||||||
StreamSubscription<List<PurchaseDetails>>? sub;
|
StreamSubscription<List<PurchaseDetails>>? sub;
|
||||||
sub = iap.purchaseStream.listen(
|
sub = iap.purchaseStream.listen(
|
||||||
(purchases) {
|
(purchases) {
|
||||||
@ -124,17 +388,17 @@ abstract final class GooglePlayPurchaseService {
|
|||||||
if (p.status == PurchaseStatus.purchased ||
|
if (p.status == PurchaseStatus.purchased ||
|
||||||
p.status == PurchaseStatus.restored) {
|
p.status == PurchaseStatus.restored) {
|
||||||
_log.d('购买成功: ${p.toString()}');
|
_log.d('购买成功: ${p.toString()}');
|
||||||
if (!completer.isCompleted) {
|
if (!completer.isCompleted && p is GooglePlayPurchaseDetails) {
|
||||||
GooglePayVerificationPayload? payload;
|
|
||||||
if (p is GooglePlayPurchaseDetails) {
|
|
||||||
final b = p.billingClientPurchase;
|
final b = p.billingClientPurchase;
|
||||||
payload = GooglePayVerificationPayload(
|
_pendingFromStream[b.orderId] = p;
|
||||||
|
completer.complete(GooglePayPurchaseResult(
|
||||||
|
orderId: b.orderId,
|
||||||
|
payload: GooglePayVerificationPayload(
|
||||||
purchaseData: b.originalJson,
|
purchaseData: b.originalJson,
|
||||||
signature: b.signature,
|
signature: b.signature,
|
||||||
);
|
),
|
||||||
}
|
purchaseDetails: p,
|
||||||
iap.completePurchase(p);
|
));
|
||||||
completer.complete(payload);
|
|
||||||
}
|
}
|
||||||
sub?.cancel();
|
sub?.cancel();
|
||||||
return;
|
return;
|
||||||
@ -154,6 +418,7 @@ abstract final class GooglePlayPurchaseService {
|
|||||||
);
|
);
|
||||||
final success = await iap.buyConsumable(
|
final success = await iap.buyConsumable(
|
||||||
purchaseParam: PurchaseParam(productDetails: product),
|
purchaseParam: PurchaseParam(productDetails: product),
|
||||||
|
autoConsume: false,
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
sub.cancel();
|
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';
|
import 'google_pay_verification_payload.dart';
|
||||||
|
|
||||||
/// 未核销(未确认)的谷歌支付订单。
|
/// 未核销(未确认)的谷歌支付订单。
|
||||||
/// 通过 [GooglePlayPurchaseService.getUnacknowledgedPurchases] 查询得到,
|
/// 通过 [GooglePlayPurchaseService.getUnacknowledgedPurchases] 查询得到,
|
||||||
/// 可用于补发回调或本地展示。上报服务端时 federation 可用 [orderId](若服务端支持)或先 createPayment 再回调。
|
/// 可用于补发回调或本地展示。上报服务端时 federation 可用 [orderId](若服务端支持)或先 createPayment 再回调。
|
||||||
|
/// 补单成功后需对 [purchaseDetails] 调用 [InAppPurchase.instance.completePurchase] 完成核销。
|
||||||
class UnacknowledgedGooglePayPurchase {
|
class UnacknowledgedGooglePayPurchase {
|
||||||
const UnacknowledgedGooglePayPurchase({
|
const UnacknowledgedGooglePayPurchase({
|
||||||
required this.orderId,
|
required this.orderId,
|
||||||
required this.productId,
|
required this.productId,
|
||||||
required this.payload,
|
required this.payload,
|
||||||
|
required this.purchaseDetails,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Google Play 订单号(purchase 内 orderId)
|
/// Google Play 订单号(purchase 内 orderId)
|
||||||
@ -18,4 +22,7 @@ class UnacknowledgedGooglePayPurchase {
|
|||||||
|
|
||||||
/// 凭据,用于 POST /v1/payment/googlepay 的 sample/merchant;federation/asset 由调用方填入。
|
/// 凭据,用于 POST /v1/payment/googlepay 的 sample/merchant;federation/asset 由调用方填入。
|
||||||
final GooglePayVerificationPayload payload;
|
final GooglePayVerificationPayload payload;
|
||||||
|
|
||||||
|
/// 原始购买详情,补单成功后用于 [InAppPurchase.instance.completePurchase]。
|
||||||
|
final PurchaseDetails purchaseDetails;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,6 +44,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
refreshAccount();
|
refreshAccount();
|
||||||
_fetchActivities();
|
_fetchActivities();
|
||||||
|
// 进入充值页时执行补单,处理未核销订单
|
||||||
|
GooglePlayPurchaseService.runOrderRecovery();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -206,8 +208,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
|
|
||||||
await _createOrderAndOpenUrl(
|
await _createOrderAndOpenUrl(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
activityId: activityId,
|
item: item,
|
||||||
productId: item.code,
|
|
||||||
paymentMethod: selected.paymentMethod,
|
paymentMethod: selected.paymentMethod,
|
||||||
subPaymentMethod: selected.subPaymentMethod?.isEmpty == true
|
subPaymentMethod: selected.subPaymentMethod?.isEmpty == true
|
||||||
? null
|
? null
|
||||||
@ -225,17 +226,17 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建订单;若为 Google Pay 则调起内购并上报凭据,否则打开支付链接
|
/// 创建订单;若为 Google Pay 则走统一入口 [_launchGooglePlayPurchase],否则打开支付链接
|
||||||
Future<void> _createOrderAndOpenUrl({
|
Future<void> _createOrderAndOpenUrl({
|
||||||
required String userId,
|
required String userId,
|
||||||
required String activityId,
|
required ActivityItem item,
|
||||||
required String productId,
|
|
||||||
required String paymentMethod,
|
required String paymentMethod,
|
||||||
String? subPaymentMethod,
|
String? subPaymentMethod,
|
||||||
}) async {
|
}) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final activityId = item.activityId ?? '';
|
||||||
final createRes = await PaymentApi.createPayment(
|
final createRes = await PaymentApi.createPayment(
|
||||||
sentinel: ApiConfig.appId,
|
sentinel: ApiConfig.appId,
|
||||||
asset: userId,
|
asset: userId,
|
||||||
@ -260,54 +261,8 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
final orderId = data?['federation']?.toString();
|
final orderId = data?['federation']?.toString();
|
||||||
|
|
||||||
if (_isGooglePay(paymentMethod, subPaymentMethod)) {
|
if (_isGooglePay(paymentMethod, subPaymentMethod)) {
|
||||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
await _launchGooglePlayPurchase(item,
|
||||||
_showSnackBar(context, 'Google Pay is only available on Android.',
|
serverOrderId: orderId, userId: userId);
|
||||||
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();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,7 +306,7 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
return r == 'googlepay' || c == 'googlepay';
|
return r == 'googlepay' || c == 'googlepay';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 三方为 false 时走谷歌应用内支付
|
/// 三方为 false 时走谷歌应用内支付:先创建订单再调起内购(与第三方选 Google Pay 一致)
|
||||||
Future<void> _runGooglePay(ActivityItem item) async {
|
Future<void> _runGooglePay(ActivityItem item) async {
|
||||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||||
_showSnackBar(context, 'Google Pay is only available on Android.',
|
_showSnackBar(context, 'Google Pay is only available on Android.',
|
||||||
@ -359,27 +314,118 @@ class _RechargeScreenState extends State<RechargeScreen>
|
|||||||
AdjustEvents.trackPaymentFailed();
|
AdjustEvents.trackPaymentFailed();
|
||||||
return;
|
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),凭据用于服务端回调
|
/// 谷歌内购统一入口:调起内购 → 回调 googlepay → 成功后再核销并刷新账户。
|
||||||
Future<void> _launchGooglePlayPurchase(ActivityItem item) async {
|
/// [serverOrderId] 有值时(来自 createPayment)用作 federation,否则用 Google orderId。
|
||||||
|
Future<void> _launchGooglePlayPurchase(ActivityItem item,
|
||||||
|
{String? serverOrderId, String? userId}) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (mounted) setState(() => _loadingProductId = null);
|
if (mounted) setState(() => _loadingProductId = null);
|
||||||
final payload =
|
final result =
|
||||||
await GooglePlayPurchaseService.launchPurchaseAndReturnData(
|
await GooglePlayPurchaseService.launchPurchaseAndReturnData(
|
||||||
item.code);
|
item.code);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (payload != null) {
|
if (result == null) {
|
||||||
_showSnackBar(context, 'Purchase completed.');
|
|
||||||
AdjustEvents.trackPurchaseSuccess();
|
|
||||||
// 直接谷歌支付路径:如需回调服务端,需先 createPayment 取得 federation,再上报 googlepay
|
|
||||||
} else {
|
|
||||||
_showSnackBar(context, 'Purchase was cancelled or failed.',
|
_showSnackBar(context, 'Purchase was cancelled or failed.',
|
||||||
isError: true);
|
isError: true);
|
||||||
AdjustEvents.trackPaymentFailed();
|
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) {
|
} catch (e) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user