petsHero-AI/lib/features/recharge/google_play_purchase_service.dart
2026-03-13 20:09:50 +08:00

436 lines
19 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: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 上可能不会出现在 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 [];
}
final iap = InAppPurchase.instance;
if (!await iap.isAvailable()) {
_log.w('Billing 不可用');
return [];
}
try {
// 先订阅 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;
orderIdsFromQuery.add(b.orderId);
list.add(UnacknowledgedGooglePayPurchase(
orderId: b.orderId,
productId: p.productID,
payload: GooglePayVerificationPayload(
purchaseData: b.originalJson,
signature: b.signature,
),
purchaseDetails: p,
));
}
// 未确认的购买在 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');
return [];
}
}
/// 对单笔购买执行 completePurchase并在 Android 上显式调用 consumePurchaseautoConsume: 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) {
_log.d('非 Android跳过内购');
return null;
}
final iap = InAppPurchase.instance;
if (!await iap.isAvailable()) {
_log.w('Billing 不可用');
return null;
}
final response = await iap.queryProductDetails({productId});
if (response.notFoundIDs.isNotEmpty || response.productDetails.isEmpty) {
_log.w(
'商品未找到: 请求的 productId="$productId", notFoundIDs=${response.notFoundIDs}, 请核对与 Play 后台配置的「产品 ID」是否完全一致区分大小写');
return null;
}
final product = response.productDetails.first;
final completer = Completer<GooglePayPurchaseResult?>();
StreamSubscription<List<PurchaseDetails>>? sub;
sub = iap.purchaseStream.listen(
(purchases) {
// 把 purchases 转为 JSON 输出,方便调试
try {
final list = purchases.map((p) {
final base = <String, Object?>{
'productID': p.productID,
'status': p.status.toString(),
'transactionDate': p.transactionDate,
'verificationData': {
'serverVerificationData':
p.verificationData.serverVerificationData,
'localVerificationData':
p.verificationData.localVerificationData,
'source': p.verificationData.source,
},
'pendingCompletePurchase': p.pendingCompletePurchase,
'error': p.error?.message,
};
if (p is GooglePlayPurchaseDetails) {
final b = p.billingClientPurchase;
base['googlePlay'] = {
'orderId': b.orderId,
'packageName': b.packageName,
'purchaseTime': b.purchaseTime,
'purchaseToken': b.purchaseToken,
'signature': b.signature,
'originalJson': b.originalJson,
'isAcknowledged': b.isAcknowledged,
'purchaseState': b.purchaseState.toString(),
};
}
return base;
}).toList();
_log.d('Google Play purchases json: ${jsonEncode(list)}');
logWithEmbeddedJson(jsonEncode(list));
} catch (e) {
_log.w('序列化 purchases 失败: $e');
}
for (final p in purchases) {
if (p.productID != productId) continue;
if (p.status == PurchaseStatus.purchased ||
p.status == PurchaseStatus.restored) {
_log.d('购买成功: ${p.toString()}');
if (!completer.isCompleted && p is GooglePlayPurchaseDetails) {
final b = p.billingClientPurchase;
_pendingFromStream[b.orderId] = p;
completer.complete(GooglePayPurchaseResult(
orderId: b.orderId,
payload: GooglePayVerificationPayload(
purchaseData: b.originalJson,
signature: b.signature,
),
purchaseDetails: p,
));
}
sub?.cancel();
return;
}
if (p.status == PurchaseStatus.error ||
p.status == PurchaseStatus.canceled) {
if (!completer.isCompleted) completer.complete(null);
sub?.cancel();
return;
}
}
},
onError: (e) {
if (!completer.isCompleted) completer.complete(null);
sub?.cancel();
},
);
final success = await iap.buyConsumable(
purchaseParam: PurchaseParam(productDetails: product),
autoConsume: false,
);
if (!success) {
sub.cancel();
return null;
}
return completer.future.timeout(
const Duration(seconds: 120),
onTimeout: () {
sub?.cancel();
return null;
},
);
}
}