diff --git a/docs/googlepay.md b/docs/googlepay.md index d1183a1..7d7b362 100644 --- a/docs/googlepay.md +++ b/docs/googlepay.md @@ -90,7 +90,24 @@ --- -## 5. 代码位置速查 +## 5. 获取未核销订单 + +「未核销」指 Google Play 侧尚未被确认(`isAcknowledged == false`)的购买,通常出现在:上次支付成功后未完成 `completePurchase`、或未成功回调服务端即退出应用。 + +### 5.1 客户端如何获取 + +- **方法**:`GooglePlayPurchaseService.getUnacknowledgedPurchases()` +- **实现**:仅 Android,通过 `InAppPurchaseAndroidPlatformAddition.queryPastPurchases()` 查询本地/缓存的购买,再筛选 `billingClientPurchase.isAcknowledged == false` 的项。 +- **返回**:`List`,每项包含 `orderId`(Google 订单号)、`productId`、`payload`(purchaseData + signature,用于回调 body 的 merchant/sample)。 + +### 5.2 典型用法 + +- **应用启动时**:调用 `getUnacknowledgedPurchases()`,若有未核销订单,可逐条上报 `POST /v1/payment/googlepay`(federation 可用该笔的 `orderId`,若服务端支持;否则需先 createPayment 拿到 federation 再回调),上报成功后对该笔购买调用 `InAppPurchase.instance.completePurchase(purchase)` 完成核销。 +- **注意**:`queryPastPurchases` 不包含已消耗(consumed)的商品;未确认的消耗型商品会一直在列表中直到被确认或消耗。 + +--- + +## 6. 代码位置速查 | 步骤 | 位置 | |------|------| @@ -99,11 +116,13 @@ | 调起内购并拿凭据 | `GooglePlayPurchaseService.launchPurchaseAndReturnData(productId)` | | 回调 googlepay | `PaymentApi.googlepay(...)`,在 `_createOrderAndOpenUrl` 内、内购成功后调用 | | 凭据数据结构 | Android:`GooglePlayPurchaseDetails.billingClientPurchase`(orderId, originalJson, signature) | +| 获取未核销订单 | `GooglePlayPurchaseService.getUnacknowledgedPurchases()`,见第 5 节 | --- -## 6. 小结 +## 7. 小结 1. **创建订单**:仅在选择「Google Pay」且第三方支付开启时调用 createPayment;拿到 **federation** 后才会调起谷歌支付并回调 googlepay。 2. **调起谷歌支付**:productId 固定为当前商品的 **code(helm)**,与 Play 后台产品 ID 一致。 3. **回调 googlepay**:body 为四字段 **sample**(signature)、**merchant**(purchaseData/originalJson)、**federation**(订单 id)、**asset**(userId);federation 为空则不回调,可按策略重试创建订单或提示失败。 +4. **未核销订单**:通过 `getUnacknowledgedPurchases()` 获取 `isAcknowledged == false` 的购买,可用于启动时补发回调或展示待处理订单。 diff --git a/lib/features/recharge/google_play_purchase_service.dart b/lib/features/recharge/google_play_purchase_service.dart index 2356f6a..f5bf61a 100644 --- a/lib/features/recharge/google_play_purchase_service.dart +++ b/lib/features/recharge/google_play_purchase_service.dart @@ -8,11 +8,54 @@ 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> 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(); + final response = await androidAddition.queryPastPurchases(); + if (response.error != null) { + _log.w('queryPastPurchases 错误: ${response.error!.message}'); + return []; + } + final list = []; + 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。调用方填入 id(federation)、userId 后组 merchant 上报。 static Future launchPurchaseAndReturnData( diff --git a/lib/features/recharge/models/unacknowledged_google_pay_purchase.dart b/lib/features/recharge/models/unacknowledged_google_pay_purchase.dart new file mode 100644 index 0000000..cb9caa3 --- /dev/null +++ b/lib/features/recharge/models/unacknowledged_google_pay_purchase.dart @@ -0,0 +1,21 @@ +import 'google_pay_verification_payload.dart'; + +/// 未核销(未确认)的谷歌支付订单。 +/// 通过 [GooglePlayPurchaseService.getUnacknowledgedPurchases] 查询得到, +/// 可用于补发回调或本地展示。上报服务端时 federation 可用 [orderId](若服务端支持)或先 createPayment 再回调。 +class UnacknowledgedGooglePayPurchase { + const UnacknowledgedGooglePayPurchase({ + required this.orderId, + required this.productId, + required this.payload, + }); + + /// Google Play 订单号(purchase 内 orderId) + final String orderId; + + /// 商品 ID(productId) + final String productId; + + /// 凭据,用于 POST /v1/payment/googlepay 的 sample/merchant;federation/asset 由调用方填入。 + final GooglePayVerificationPayload payload; +} diff --git a/lib/features/recharge/recharge_screen.dart b/lib/features/recharge/recharge_screen.dart index bfdbf76..691b4e0 100644 --- a/lib/features/recharge/recharge_screen.dart +++ b/lib/features/recharge/recharge_screen.dart @@ -283,7 +283,16 @@ class _RechargeScreenState extends State ); if (!mounted) return; if (googlepayRes.isSuccess) { - _showSnackBar(context, 'Purchase completed.'); + final resData = googlepayRes.data is Map + ? googlepayRes.data as Map + : null; + final line = (resData?['line']?.toString() ?? '').toUpperCase(); + if (line == 'SUCCESS') { + await refreshAccount(); + } + if (mounted) { + _showSnackBar(context, 'Purchase completed.'); + } AdjustEvents.trackPurchaseSuccess(); } else { _showSnackBar( diff --git a/lib/main.dart b/lib/main.dart index 6bb4b24..490c2e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:adjust_sdk/adjust_attribution.dart'; import 'package:adjust_sdk/adjust_config.dart'; import 'package:adjust_sdk/adjust.dart'; import 'package:flutter/foundation.dart'; @@ -6,6 +7,7 @@ import 'package:flutter/services.dart'; import 'app.dart'; import 'core/auth/auth_service.dart'; +import 'core/log/app_logger.dart'; import 'core/referrer/referrer_service.dart'; import 'core/theme/app_colors.dart'; @@ -34,5 +36,27 @@ void _initAdjust() { if (kDebugMode) { config.logLevel = AdjustLogLevel.verbose; } + config.attributionCallback = _onAdjustAttribution; Adjust.initSdk(config); } + +final _adjustLog = AppLogger('Adjust'); + +void _onAdjustAttribution(AdjustAttribution attribution) { + _adjustLog.d('归因信息: ' + 'trackerToken=${attribution.trackerToken}, ' + 'trackerName=${attribution.trackerName}, ' + 'network=${attribution.network}, ' + 'campaign=${attribution.campaign}, ' + 'adgroup=${attribution.adgroup}, ' + 'creative=${attribution.creative}, ' + 'clickLabel=${attribution.clickLabel}, ' + 'costType=${attribution.costType}, ' + 'costAmount=${attribution.costAmount}, ' + 'costCurrency=${attribution.costCurrency}, ' + 'fbInstallReferrer=${attribution.fbInstallReferrer}'); + if (attribution.jsonResponse != null && + attribution.jsonResponse!.isNotEmpty) { + _adjustLog.d('归因 jsonResponse: ${attribution.jsonResponse}'); + } +}