148 lines
4.8 KiB
Dart
148 lines
4.8 KiB
Dart
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)。
|
||
/// 成功返回 purchaseData(serverVerificationData),取消/失败返回 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;
|
||
});
|
||
}
|
||
}
|