petsHero-AI/lib/features/recharge/google_play_purchase_service.dart
2026-03-12 14:30:19 +08:00

148 lines
4.8 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 'package:flutter/foundation.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import '../../core/log/app_logger.dart';
/// 调起 Google Play 内购(三方为 false 时使用;三方为 true 且 ceremony==GooglePay 时用 launchPurchaseAndReturnData
abstract final class GooglePlayPurchaseService {
static final _log = AppLogger('GooglePlayPurchase');
/// 发起购买并返回服务器凭据(用于三方支付选 GooglePay 时上报 /v1/payment/googlepay
/// 成功返回 purchaseDataserverVerificationData取消/失败返回 null。
static Future<String?> 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<String?>();
StreamSubscription<List<PurchaseDetails>>? sub;
sub = iap.purchaseStream.listen(
(purchases) {
for (final p in purchases) {
if (p.productID != productId) continue;
if (p.status == PurchaseStatus.purchased ||
p.status == PurchaseStatus.restored) {
if (!completer.isCompleted) {
final data = p.verificationData.serverVerificationData;
iap.completePurchase(p);
completer.complete(data);
}
sub?.cancel();
return;
}
if (p.status == PurchaseStatus.error ||
p.status == PurchaseStatus.canceled) {
if (!completer.isCompleted) completer.complete(null);
sub?.cancel();
return;
}
}
},
onError: (_) {
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;
},
);
}
/// 发起购买,商品 ID 为 [productId](即 ActivityItem.code / helm
/// 返回 true 表示购买完成false 表示取消或失败。
static Future<bool> launchPurchase(String productId) async {
if (defaultTargetPlatform != TargetPlatform.android) {
return false;
}
final iap = InAppPurchase.instance;
final available = await iap.isAvailable();
if (!available) {
return false;
}
final response = await iap.queryProductDetails({productId});
if (response.notFoundIDs.isNotEmpty || response.productDetails.isEmpty) {
_log.w('商品未找到: productId="$productId", notFoundIDs=${response.notFoundIDs}');
return false;
}
final product = response.productDetails.first;
final completer = Completer<bool>();
StreamSubscription<List<PurchaseDetails>>? sub;
sub = iap.purchaseStream.listen(
(purchases) {
for (final p in purchases) {
if (p.productID != productId) continue;
if (p.status == PurchaseStatus.purchased ||
p.status == PurchaseStatus.restored) {
if (!completer.isCompleted) {
iap.completePurchase(p);
completer.complete(true);
}
sub?.cancel();
return;
}
if (p.status == PurchaseStatus.error) {
if (!completer.isCompleted) {
completer.complete(false);
}
sub?.cancel();
return;
}
if (p.status == PurchaseStatus.canceled) {
if (!completer.isCompleted) {
completer.complete(false);
}
sub?.cancel();
return;
}
}
},
onError: (Object e) {
if (!completer.isCompleted) completer.complete(false);
sub?.cancel();
},
);
final purchaseParam = PurchaseParam(productDetails: product);
final success = await iap.buyConsumable(purchaseParam: purchaseParam);
if (!success) {
sub.cancel();
return false;
}
return completer.future.timeout(const Duration(seconds: 120),
onTimeout: () {
sub?.cancel();
return false;
});
}
}