diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 5b81ffb..185907f 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,12 @@
android:maxSdkVersion="32"
tools:replace="android:maxSdkVersion" />
+
+
+
+
+
+
$(FLUTTER_BUILD_NUMBER)
FacebookAutoLogAppEventsEnabled
+ LSApplicationQueriesSchemes
+
+ fb
+
LSRequiresIPhoneOS
NSCameraUsageDescription
diff --git a/lib/app.dart b/lib/app.dart
index d9536e3..bbf9c4c 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:flutter/material.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
+import 'package:url_launcher/url_launcher.dart';
import 'core/auth/auth_service.dart';
import 'core/nav/app_route_observer.dart';
@@ -46,6 +47,7 @@ class _AppState extends State with WidgetsBindingObserver {
WidgetsBinding.instance.addPostFrameCallback((_) {
// Remove native splash; Flutter login overlay takes over if startup is still running.
FlutterNativeSplash.remove();
+ unawaited(_logFacebookInstallState('first_frame'));
_reportFacebookActivateApp('first_frame');
});
}
@@ -65,6 +67,22 @@ class _AppState extends State with WidgetsBindingObserver {
});
}
+ /// Detect whether a local app can handle Facebook's URL scheme.
+ ///
+ /// iOS requires `LSApplicationQueriesSchemes` to include `fb`; Android 11+
+ /// requires a matching `` entry in AndroidManifest.
+ Future _logFacebookInstallState(String reason) async {
+ const scheme = 'fb://';
+ try {
+ final installed = await canLaunchUrl(Uri.parse(scheme));
+ _fbLog.d(
+ 'Facebook app install check ($reason): installed=$installed scheme=$scheme',
+ );
+ } catch (e, st) {
+ _fbLog.e('Facebook app install check failed ($reason)', e, st);
+ }
+ }
+
/// Foreground resume; cold start also uses addPostFrameCallback in initState.
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
diff --git a/lib/core/auth/auth_service.dart b/lib/core/auth/auth_service.dart
index 77b7a81..461ca61 100644
--- a/lib/core/auth/auth_service.dart
+++ b/lib/core/auth/auth_service.dart
@@ -553,6 +553,20 @@ class AuthService {
static Future _runPostLoginReferrerWork(String uid, String deviceId) async {
try {
await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
+ // 首次归因可能是“延迟到的自然量”:开启 5s 轮询、最长 5min。
+ // 轮询结束后,无论是否检测到非自然量都重新上报归因 + 刷新 common_info,
+ // 再触发首页完整重载(_loadCategories 会重拉分类数据)。
+ unawaited(ReferrerService.startOrganicPolling(
+ onStopped: (detectedNonOrganic) async {
+ try {
+ await _reportBothReferrersAndRefreshCommonInfo(uid, deviceId);
+ } catch (e, st) {
+ _logMsg('organic poll: 重新上报归因/common_info 异常: $e');
+ _logMsg('organic poll: 堆栈 $st');
+ }
+ UserState.requestHomeFullReload();
+ },
+ ));
} catch (e, st) {
_logMsg('referrer/common_info 后台任务异常: $e');
_logMsg('referrer/common_info 堆栈: $st');
diff --git a/lib/core/referrer/referrer_service.dart b/lib/core/referrer/referrer_service.dart
index 86ab86f..fdce89f 100644
--- a/lib/core/referrer/referrer_service.dart
+++ b/lib/core/referrer/referrer_service.dart
@@ -6,10 +6,14 @@ import 'package:adjust_sdk/adjust.dart';
import 'package:flutter/foundation.dart';
import 'package:play_install_referrer/play_install_referrer.dart';
+import '../log/app_logger.dart';
+
/// 归因信息服务(优先从 Adjust 获取,fallback 使用 Play Install Referrer)
class ReferrerService {
ReferrerService._();
+ static final _log = AppLogger('ReferrerService');
+
static String? _cachedReferrer;
static String _referrerSource = 'gg';
static final Completer _completer = Completer();
@@ -27,6 +31,15 @@ class ReferrerService {
/// Play Install Referrer 读取超时(部分机型/系统上可能极慢)
static const Duration _playInstallReferrerTimeout = Duration(seconds: 10);
+ /// 自然量轮询:每次检查间隔
+ static const Duration _organicPollInterval = Duration(seconds: 5);
+
+ /// 自然量轮询:最长持续时间(达到后无论是否仍是自然量都停止)
+ static const Duration _organicPollMaxDuration = Duration(minutes: 5);
+
+ static bool _organicPollRunning = false;
+ static Timer? _organicPollTimer;
+
/// 由 Adjust attributionCallback 调用,首次安装时归因往往通过此回调返回(晚于 getAttributionWithTimeout)
static void receiveAttributionFromCallback(AdjustAttribution attribution) {
if (!_attributionCallbackCompleter.isCompleted) {
@@ -143,4 +156,133 @@ class ReferrerService {
return '';
}
}
+
+ /// 启动“自然量→渠道”轮询:仅当 Adjust 与 Play Install Referrer 当前都判定为自然量时开始;
+ /// 之后每 [_organicPollInterval] 重新判定一次,直到任一渠道变为非自然量或超过
+ /// [_organicPollMaxDuration]。结束时调用 [onStopped],参数表示是否检测到非自然量。
+ ///
+ /// 多次调用是幂等的:已在轮询时直接返回。
+ static Future startOrganicPolling({
+ required Future Function(bool detectedNonOrganic) onStopped,
+ }) async {
+ if (_organicPollRunning) {
+ _log.d('startOrganicPolling: already running, skip');
+ return;
+ }
+
+ final initiallyOrganic = await _isCurrentlyOrganic();
+ if (!initiallyOrganic) {
+ _log.d('startOrganicPolling: 初始归因即为渠道,无需轮询');
+ return;
+ }
+
+ _organicPollRunning = true;
+ final startedAt = DateTime.now();
+ var tickCount = 0;
+ _log.d(
+ 'startOrganicPolling: 启动自然量轮询,每 ${_organicPollInterval.inSeconds}s 一次,最长 ${_organicPollMaxDuration.inMinutes}min');
+
+ Future finish(bool detectedNonOrganic, {required String reason}) async {
+ _stopOrganicPoll();
+ _log.d('startOrganicPolling: 结束,原因=$reason changed=$detectedNonOrganic');
+ try {
+ await onStopped(detectedNonOrganic);
+ } catch (e, st) {
+ _log.d('startOrganicPolling: onStopped 回调异常: $e');
+ _log.d('startOrganicPolling: stack $st');
+ }
+ }
+
+ Future tick() async {
+ if (!_organicPollRunning) return;
+
+ tickCount++;
+ final elapsed = DateTime.now().difference(startedAt);
+ if (elapsed >= _organicPollMaxDuration) {
+ await finish(false, reason: 'timeout');
+ return;
+ }
+
+ final adjustNetwork = await _readAdjustNetwork();
+ final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork);
+ final pirRef = await _readPlayInstallReferrerRaw();
+ final pirOrganic = _installReferrerIsOrganic(pirRef);
+ final stillOrganic = adjustOrganic && pirOrganic;
+ _log.d(
+ 'organic poll tick#$tickCount elapsed=${elapsed.inSeconds}s '
+ 'adjustNetwork="${adjustNetwork ?? ''}" adjustOrganic=$adjustOrganic '
+ 'pir="$pirRef" pirOrganic=$pirOrganic stillOrganic=$stillOrganic');
+
+ if (!_organicPollRunning) return;
+
+ if (!stillOrganic) {
+ await finish(true, reason: 'detectedNonOrganic');
+ return;
+ }
+
+ _organicPollTimer = Timer(_organicPollInterval, () {
+ if (!_organicPollRunning) return;
+ unawaited(tick());
+ });
+ }
+
+ _organicPollTimer = Timer(_organicPollInterval, () {
+ if (!_organicPollRunning) return;
+ unawaited(tick());
+ });
+ }
+
+ static void _stopOrganicPoll() {
+ _organicPollTimer?.cancel();
+ _organicPollTimer = null;
+ _organicPollRunning = false;
+ }
+
+ /// 当 Adjust 归因和 Play Install Referrer 都判定为自然量时返回 true
+ static Future _isCurrentlyOrganic() async {
+ final adjustNetwork = await _readAdjustNetwork();
+ final adjustOrganic = _attributionIsOrganicByNetwork(adjustNetwork);
+ final pirRef = await _readPlayInstallReferrerRaw();
+ final pirOrganic = _installReferrerIsOrganic(pirRef);
+ return adjustOrganic && pirOrganic;
+ }
+
+ /// 读取 Adjust 当前归因的 network 字段(异常返回 null,按未归因处理)
+ static Future _readAdjustNetwork() async {
+ try {
+ final attr = await Adjust.getAttribution();
+ return attr.network;
+ } catch (_) {
+ return null;
+ }
+ }
+
+ /// network 为空 / 'organic' 视为自然量(含未归因情况)
+ static bool _attributionIsOrganicByNetwork(String? network) {
+ final n = (network ?? '').trim().toLowerCase();
+ return n.isEmpty || n == 'organic';
+ }
+
+ /// 读取 Play Install Referrer 原始字符串(非 Android / 异常返回空串)
+ static Future _readPlayInstallReferrerRaw() async {
+ if (defaultTargetPlatform != TargetPlatform.android) return '';
+ try {
+ final details = await PlayInstallReferrer.installReferrer
+ .timeout(_playInstallReferrerTimeout);
+ return details.installReferrer ?? '';
+ } catch (_) {
+ return '';
+ }
+ }
+
+ /// Google Play 自然量典型 referrer:`utm_source=google-play&utm_medium=organic`;
+ /// 空串/缺少 `utm_medium=` 视为尚未归因,按自然量处理。
+ static bool _installReferrerIsOrganic(String ref) {
+ final s = ref.trim();
+ if (s.isEmpty) return true;
+ final lower = s.toLowerCase();
+ if (lower.contains('utm_medium=organic')) return true;
+ if (!lower.contains('utm_medium=')) return true;
+ return false;
+ }
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 886e263..bbfd743 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: pets_hero_ai
description: PetsHero AI Application.
publish_to: 'none'
-version: 1.2.0+120
+version: 1.2.1+121
environment:
sdk: '>=3.0.0 <4.0.0'