128 lines
4.8 KiB
Dart
128 lines
4.8 KiB
Dart
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';
|
||
|
||
/// 调起 Google Play 内购,所有内购均通过本方法发起并返回凭据用于服务端回调。
|
||
abstract final class GooglePlayPurchaseService {
|
||
static final _log = AppLogger('GooglePlayPurchase');
|
||
|
||
/// 发起购买并返回服务端回调所需凭据(purchaseData=originalJson, signature)。
|
||
/// 成功返回 [GooglePayVerificationPayload],取消/失败返回 null。调用方填入 id(federation)、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;
|
||
},
|
||
);
|
||
}
|
||
}
|