Commit 98f4b6ab63291ee4cfb2f32d7c528f55a7feb26f

Authored by 刘宏哲
1 parent 086e13f1

feat(app): 修改好评弹窗逻辑

@@ -10,11 +10,14 @@ import '../widgets/app_review_prompt_dialog.dart'; @@ -10,11 +10,14 @@ import '../widgets/app_review_prompt_dialog.dart';
10 class AppReviewPromptLogic { 10 class AppReviewPromptLogic {
11 AppReviewPromptLogic({ 11 AppReviewPromptLogic({
12 required LocalStorage storage, 12 required LocalStorage storage,
  13 + required int Function() userIdProvider,
13 PlatformHostApi? platformHostApi, 14 PlatformHostApi? platformHostApi,
14 }) : _storage = storage, 15 }) : _storage = storage,
  16 + _userIdProvider = userIdProvider,
15 _platformHostApi = platformHostApi ?? PlatformHostApi(); 17 _platformHostApi = platformHostApi ?? PlatformHostApi();
16 18
17 final LocalStorage _storage; 19 final LocalStorage _storage;
  20 + final int Function() _userIdProvider;
18 final PlatformHostApi _platformHostApi; 21 final PlatformHostApi _platformHostApi;
19 22
20 bool _isShowing = false; 23 bool _isShowing = false;
@@ -25,9 +28,10 @@ class AppReviewPromptLogic { @@ -25,9 +28,10 @@ class AppReviewPromptLogic {
25 bool canShowPrompt(BuildContext context) { 28 bool canShowPrompt(BuildContext context) {
26 if (_isShowing || !context.mounted) return false; 29 if (_isShowing || !context.mounted) return false;
27 30
  31 + final userId = _currentUserId;
  32 + if (userId == null) return false;
28 final now = DateTime.now(); 33 final now = DateTime.now();
29 - final nextShowAt = _storage.appReviewPromptNextShowAt;  
30 - return nextShowAt == null || !now.isBefore(nextShowAt); 34 + return _canShowAt(userId, now);
31 } 35 }
32 36
33 Future<void> maybeShowPrompt(BuildContext context) async { 37 Future<void> maybeShowPrompt(BuildContext context) async {
@@ -59,6 +63,42 @@ class AppReviewPromptLogic { @@ -59,6 +63,42 @@ class AppReviewPromptLogic {
59 } 63 }
60 } 64 }
61 65
  66 + /// Records a qualified sleep-report view. The prompt is deliberately shown
  67 + /// only on the next app launch after the user reaches the home page.
  68 + Future<void> recordPromptEligibility() async {
  69 + final userId = _currentUserId;
  70 + if (userId == null) return;
  71 +
  72 + final now = DateTime.now();
  73 + if (!_canShowAt(userId, now)) return;
  74 +
  75 + if (_storage.hasAppReviewPromptPending(userId)) return;
  76 +
  77 + await _storage.setAppReviewPromptPending(userId);
  78 + }
  79 +
  80 + /// Consumes a pending qualified view when the app next enters its home page.
  81 + Future<void> maybeShowPendingPrompt(BuildContext context) async {
  82 + if (_isShowing || !context.mounted) return;
  83 +
  84 + final userId = _currentUserId;
  85 + if (userId == null) return;
  86 +
  87 + if (!_storage.hasAppReviewPromptPending(userId)) return;
  88 +
  89 + final now = DateTime.now();
  90 + if (!_canShowAt(userId, now)) {
  91 + await _storage.clearAppReviewPromptPending(userId);
  92 + return;
  93 + }
  94 +
  95 + // Clear before presenting so a recreated home page cannot show it twice.
  96 + await _storage.clearAppReviewPromptPending(userId);
  97 + if (context.mounted) {
  98 + await maybeShowPrompt(context);
  99 + }
  100 + }
  101 +
62 Future<void> _requestAppReview() async { 102 Future<void> _requestAppReview() async {
63 if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) return; 103 if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) return;
64 104
@@ -83,6 +123,21 @@ class AppReviewPromptLogic { @@ -83,6 +123,21 @@ class AppReviewPromptLogic {
83 } 123 }
84 124
85 Future<void> _coolDown(Duration duration) { 125 Future<void> _coolDown(Duration duration) {
86 - return _storage.setAppReviewPromptNextShowAt(DateTime.now().add(duration)); 126 + final userId = _currentUserId;
  127 + if (userId == null) return Future.value();
  128 + return _storage.setAppReviewPromptNextShowAt(
  129 + userId,
  130 + DateTime.now().add(duration),
  131 + );
  132 + }
  133 +
  134 + int? get _currentUserId {
  135 + final userId = _userIdProvider();
  136 + return userId > 0 ? userId : null;
  137 + }
  138 +
  139 + bool _canShowAt(int userId, DateTime now) {
  140 + final nextShowAt = _storage.appReviewPromptNextShowAt(userId);
  141 + return nextShowAt == null || !now.isBefore(nextShowAt);
87 } 142 }
88 } 143 }
@@ -3,6 +3,7 @@ import 'package:get/get.dart'; @@ -3,6 +3,7 @@ import 'package:get/get.dart';
3 import 'package:lottie/lottie.dart'; 3 import 'package:lottie/lottie.dart';
4 4
5 import '../../../../core/constants/intent_keys.dart'; 5 import '../../../../core/constants/intent_keys.dart';
  6 +import '../../../../core/services/user_state_service.dart';
6 import '../../../../data/local/local_storage.dart'; 7 import '../../../../data/local/local_storage.dart';
7 import '../../../../data/local/user_preferences_storage.dart'; 8 import '../../../../data/local/user_preferences_storage.dart';
8 import '../../../routes/app_pages.dart'; 9 import '../../../routes/app_pages.dart';
@@ -33,6 +34,7 @@ class HealthTrendContent extends StatefulWidget { @@ -33,6 +34,7 @@ class HealthTrendContent extends StatefulWidget {
33 final HealthReportQuery query; 34 final HealthReportQuery query;
34 final int selectedTypeIndex; 35 final int selectedTypeIndex;
35 final int refreshToken; 36 final int refreshToken;
  37 +
36 /// Whether users can switch trend types by horizontally swiping the content. 38 /// Whether users can switch trend types by horizontally swiping the content.
37 /// Tapping a tab remains available when this is false. 39 /// Tapping a tab remains available when this is false.
38 final bool allowTabSwipe; 40 final bool allowTabSwipe;
@@ -441,6 +443,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> { @@ -441,6 +443,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
441 ); 443 );
442 _appReviewPromptLogic = AppReviewPromptLogic( 444 _appReviewPromptLogic = AppReviewPromptLogic(
443 storage: Get.find<LocalStorage>(), 445 storage: Get.find<LocalStorage>(),
  446 + userIdProvider: () => Get.find<UserStateService>().userId,
444 ); 447 );
445 _logic.initializeQuery(widget.query.period, widget.query.date); 448 _logic.initializeQuery(widget.query.period, widget.query.date);
446 _periodWorker = ever(_logic.selectedPeriod, (period) { 449 _periodWorker = ever(_logic.selectedPeriod, (period) {
@@ -504,7 +507,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> { @@ -504,7 +507,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
504 isVip: widget.isVip, 507 isVip: widget.isVip,
505 onSubscribe: widget.onSubscribe, 508 onSubscribe: widget.onSubscribe,
506 onReportViewed: widget.isSelected 509 onReportViewed: widget.isSelected
507 - ? () => _appReviewPromptLogic.maybeShowPrompt(context) 510 + ? _appReviewPromptLogic.recordPromptEligibility
508 : null, 511 : null,
509 loadingIndicator: const _HealthTrendLoadingIndicator(), 512 loadingIndicator: const _HealthTrendLoadingIndicator(),
510 ); 513 );
1 import 'package:flutter/material.dart'; 1 import 'package:flutter/material.dart';
2 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
3 3
  4 +import '../../app_review_prompt/logic/app_review_prompt_logic.dart';
  5 +import '../../../../data/local/local_storage.dart';
  6 +import '../../../../core/services/user_state_service.dart';
4 import '../controllers/home_controller.dart'; 7 import '../controllers/home_controller.dart';
5 import '../widgets/df_tab_bar.dart'; 8 import '../widgets/df_tab_bar.dart';
6 import '../../friends/views/friends_tab.dart'; 9 import '../../friends/views/friends_tab.dart';
@@ -58,13 +61,21 @@ class _HomeReviewPromptGate extends StatefulWidget { @@ -58,13 +61,21 @@ class _HomeReviewPromptGate extends StatefulWidget {
58 } 61 }
59 62
60 class _HomeReviewPromptGateState extends State<_HomeReviewPromptGate> { 63 class _HomeReviewPromptGateState extends State<_HomeReviewPromptGate> {
  64 + late final AppReviewPromptLogic _appReviewPromptLogic;
  65 +
61 @override 66 @override
62 void initState() { 67 void initState() {
63 super.initState(); 68 super.initState();
  69 + _appReviewPromptLogic = AppReviewPromptLogic(
  70 + storage: Get.find<LocalStorage>(),
  71 + userIdProvider: () => Get.find<UserStateService>().userId,
  72 + );
64 73
65 WidgetsBinding.instance.addPostFrameCallback((_) async { 74 WidgetsBinding.instance.addPostFrameCallback((_) async {
66 if (!mounted) return; 75 if (!mounted) return;
67 await widget.controller.maybeShowMembershipOffer(); 76 await widget.controller.maybeShowMembershipOffer();
  77 + if (!mounted) return;
  78 + await _appReviewPromptLogic.maybeShowPendingPrompt(context);
68 }); 79 });
69 } 80 }
70 81
@@ -9,6 +9,7 @@ abstract final class StorageConst { @@ -9,6 +9,7 @@ abstract final class StorageConst {
9 static const String lastLoginMethodKey = 'last_login_method'; 9 static const String lastLoginMethodKey = 'last_login_method';
10 static const String appReviewPromptNextShowAtKey = 10 static const String appReviewPromptNextShowAtKey =
11 'app_review_prompt_next_show_at'; 11 'app_review_prompt_next_show_at';
  12 + static const String appReviewPromptPendingKey = 'app_review_prompt_pending';
12 static const String homeMembershipOfferPromptDateKey = 13 static const String homeMembershipOfferPromptDateKey =
13 'home_membership_offer_prompt_date'; 14 'home_membership_offer_prompt_date';
14 static const String homeMembershipOfferPromptUserIdKey = 15 static const String homeMembershipOfferPromptUserIdKey =
@@ -49,21 +49,40 @@ class LocalStorage { @@ -49,21 +49,40 @@ class LocalStorage {
49 49
50 // ─── App Review Prompt Storage ────────────────────────────────────────────── 50 // ─── App Review Prompt Storage ──────────────────────────────────────────────
51 51
52 - DateTime? get appReviewPromptNextShowAt { 52 + DateTime? appReviewPromptNextShowAt(int userId) {
53 final value = sharedPreferences.getInt( 53 final value = sharedPreferences.getInt(
54 - StorageConst.appReviewPromptNextShowAtKey, 54 + _appReviewPromptNextShowAtKey(userId),
55 ); 55 );
56 if (value == null || value <= 0) return null; 56 if (value == null || value <= 0) return null;
57 return DateTime.fromMillisecondsSinceEpoch(value); 57 return DateTime.fromMillisecondsSinceEpoch(value);
58 } 58 }
59 59
60 - Future<void> setAppReviewPromptNextShowAt(DateTime value) async { 60 + Future<void> setAppReviewPromptNextShowAt(int userId, DateTime value) async {
61 await sharedPreferences.setInt( 61 await sharedPreferences.setInt(
62 - StorageConst.appReviewPromptNextShowAtKey, 62 + _appReviewPromptNextShowAtKey(userId),
63 value.millisecondsSinceEpoch, 63 value.millisecondsSinceEpoch,
64 ); 64 );
65 } 65 }
66 66
  67 + bool hasAppReviewPromptPending(int userId) {
  68 + return sharedPreferences.getBool(_appReviewPromptPendingKey(userId)) ??
  69 + false;
  70 + }
  71 +
  72 + Future<void> setAppReviewPromptPending(int userId) async {
  73 + await sharedPreferences.setBool(_appReviewPromptPendingKey(userId), true);
  74 + }
  75 +
  76 + Future<void> clearAppReviewPromptPending(int userId) async {
  77 + await sharedPreferences.remove(_appReviewPromptPendingKey(userId));
  78 + }
  79 +
  80 + String _appReviewPromptNextShowAtKey(int userId) =>
  81 + '${StorageConst.appReviewPromptNextShowAtKey}_$userId';
  82 +
  83 + String _appReviewPromptPendingKey(int userId) =>
  84 + '${StorageConst.appReviewPromptPendingKey}_$userId';
  85 +
67 // ─── Home Membership Offer Prompt Storage ───────────────────────────────── 86 // ─── Home Membership Offer Prompt Storage ─────────────────────────────────
68 87
69 int? get homeMembershipOfferPromptDate => 88 int? get homeMembershipOfferPromptDate =>