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 _pendingFromStream = {}; static StreamSubscription>? _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 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 saveFederationForGoogleOrderId( String googleOrderId, String federation) async { try { final prefs = await SharedPreferences.getInstance(); final json = prefs.getString(_kFederationMapKey); final map = json != null ? Map.from((jsonDecode(json) as Map) .map((k, v) => MapEntry(k.toString(), v.toString()))) : {}; map[googleOrderId] = federation; await prefs.setString(_kFederationMapKey, jsonEncode(map)); } catch (e) { _log.w('保存 federation 映射失败: $e'); } } /// 补单时根据 Google orderId 取回创建订单时的 federation,无则返回 null。 static Future 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 removeFederationForGoogleOrderId( String googleOrderId) async { try { final prefs = await SharedPreferences.getInstance(); final json = prefs.getString(_kFederationMapKey); if (json == null) return; final map = Map.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> 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(); 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 = []; final orderIdsFromQuery = {}; 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.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 上显式调用 consumePurchase(autoConsume: false 时必须,否则无法再次购买)。 /// 正常流程回调成功后请调用此方法,传入 [GooglePayPurchaseResult.purchaseDetails]。 static Future completeAndConsumePurchase(PurchaseDetails purchaseDetails) async { final iap = InAppPurchase.instance; try { iap.completePurchase(purchaseDetails); _log.d('completePurchase 已执行'); if (defaultTargetPlatform == TargetPlatform.android) { final androidAddition = iap.getPlatformAddition(); 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 _consumePurchase(InAppPurchase iap, UnacknowledgedGooglePayPurchase p) async { return completeAndConsumePurchase(p.purchaseDetails); } /// 补单流程:拉取未消耗订单 → 有 federation 则回调后 completePurchase,无 federation 也执行 completePurchase 以解除「已拥有此内容」。 /// 仅 Android 且已登录时执行。 static Future 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 ? res.data as Map : 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 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(); StreamSubscription>? sub; sub = iap.purchaseStream.listen( (purchases) { // 把 purchases 转为 JSON 输出,方便调试 try { final list = purchases.map((p) { final base = { '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; }, ); } }