petsHero-AI/lib/features/recharge/google_play_purchase_service.dart
2026-03-12 22:16:36 +08:00

171 lines
6.4 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/in_app_purchase_android.dart';
import 'package:pets_hero_ai/core/api/api.dart';
import '../../core/log/app_logger.dart';
import 'models/google_pay_verification_payload.dart';
import 'models/unacknowledged_google_pay_purchase.dart';
/// 调起 Google Play 内购,所有内购均通过本方法发起并返回凭据用于服务端回调。
abstract final class GooglePlayPurchaseService {
static final _log = AppLogger('GooglePlayPurchase');
/// 获取当前未核销(未确认)的谷歌支付订单。
/// 使用 Android [InAppPurchaseAndroidPlatformAddition.queryPastPurchases] 查询本地/缓存中的购买,
/// 筛选 [isAcknowledged == false] 的项。仅 Android 有效,非 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 {
final androidAddition = iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
final response = await androidAddition.queryPastPurchases();
if (response.error != null) {
_log.w('queryPastPurchases 错误: ${response.error!.message}');
return [];
}
final list = <UnacknowledgedGooglePayPurchase>[];
for (final p in response.pastPurchases) {
final b = p.billingClientPurchase;
if (b.isAcknowledged) continue;
list.add(UnacknowledgedGooglePayPurchase(
orderId: b.orderId,
productId: p.productID,
payload: GooglePayVerificationPayload(
purchaseData: b.originalJson,
signature: b.signature,
),
));
}
_log.d('未核销订单数: ${list.length}');
return list;
} catch (e, st) {
_log.w('获取未核销订单失败: $e\n$st');
return [];
}
}
/// 发起购买并返回服务端回调所需凭据purchaseData=originalJson, signature
/// 成功返回 [GooglePayVerificationPayload],取消/失败返回 null。调用方填入 idfederation、userId 后组 merchant 上报。
static Future<GooglePayVerificationPayload?> 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<GooglePayVerificationPayload?>();
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) {
GooglePayVerificationPayload? payload;
if (p is GooglePlayPurchaseDetails) {
final b = p.billingClientPurchase;
payload = GooglePayVerificationPayload(
purchaseData: b.originalJson,
signature: b.signature,
);
}
iap.completePurchase(p);
completer.complete(payload);
}
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),
);
if (!success) {
sub.cancel();
return null;
}
return completer.future.timeout(
const Duration(seconds: 120),
onTimeout: () {
sub?.cancel();
return null;
},
);
}
}