Commit 98f4b6ab63291ee4cfb2f32d7c528f55a7feb26f

Authored by 刘宏哲
1 parent 086e13f1

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

... ... @@ -10,11 +10,14 @@ import '../widgets/app_review_prompt_dialog.dart';
class AppReviewPromptLogic {
AppReviewPromptLogic({
required LocalStorage storage,
required int Function() userIdProvider,
PlatformHostApi? platformHostApi,
}) : _storage = storage,
_userIdProvider = userIdProvider,
_platformHostApi = platformHostApi ?? PlatformHostApi();
final LocalStorage _storage;
final int Function() _userIdProvider;
final PlatformHostApi _platformHostApi;
bool _isShowing = false;
... ... @@ -25,9 +28,10 @@ class AppReviewPromptLogic {
bool canShowPrompt(BuildContext context) {
if (_isShowing || !context.mounted) return false;
final userId = _currentUserId;
if (userId == null) return false;
final now = DateTime.now();
final nextShowAt = _storage.appReviewPromptNextShowAt;
return nextShowAt == null || !now.isBefore(nextShowAt);
return _canShowAt(userId, now);
}
Future<void> maybeShowPrompt(BuildContext context) async {
... ... @@ -59,6 +63,42 @@ class AppReviewPromptLogic {
}
}
/// Records a qualified sleep-report view. The prompt is deliberately shown
/// only on the next app launch after the user reaches the home page.
Future<void> recordPromptEligibility() async {
final userId = _currentUserId;
if (userId == null) return;
final now = DateTime.now();
if (!_canShowAt(userId, now)) return;
if (_storage.hasAppReviewPromptPending(userId)) return;
await _storage.setAppReviewPromptPending(userId);
}
/// Consumes a pending qualified view when the app next enters its home page.
Future<void> maybeShowPendingPrompt(BuildContext context) async {
if (_isShowing || !context.mounted) return;
final userId = _currentUserId;
if (userId == null) return;
if (!_storage.hasAppReviewPromptPending(userId)) return;
final now = DateTime.now();
if (!_canShowAt(userId, now)) {
await _storage.clearAppReviewPromptPending(userId);
return;
}
// Clear before presenting so a recreated home page cannot show it twice.
await _storage.clearAppReviewPromptPending(userId);
if (context.mounted) {
await maybeShowPrompt(context);
}
}
Future<void> _requestAppReview() async {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.iOS) return;
... ... @@ -83,6 +123,21 @@ class AppReviewPromptLogic {
}
Future<void> _coolDown(Duration duration) {
return _storage.setAppReviewPromptNextShowAt(DateTime.now().add(duration));
final userId = _currentUserId;
if (userId == null) return Future.value();
return _storage.setAppReviewPromptNextShowAt(
userId,
DateTime.now().add(duration),
);
}
int? get _currentUserId {
final userId = _userIdProvider();
return userId > 0 ? userId : null;
}
bool _canShowAt(int userId, DateTime now) {
final nextShowAt = _storage.appReviewPromptNextShowAt(userId);
return nextShowAt == null || !now.isBefore(nextShowAt);
}
}
... ...
... ... @@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../../core/constants/intent_keys.dart';
import '../../../../core/services/user_state_service.dart';
import '../../../../data/local/local_storage.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../routes/app_pages.dart';
... ... @@ -33,6 +34,7 @@ class HealthTrendContent extends StatefulWidget {
final HealthReportQuery query;
final int selectedTypeIndex;
final int refreshToken;
/// Whether users can switch trend types by horizontally swiping the content.
/// Tapping a tab remains available when this is false.
final bool allowTabSwipe;
... ... @@ -441,6 +443,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
);
_appReviewPromptLogic = AppReviewPromptLogic(
storage: Get.find<LocalStorage>(),
userIdProvider: () => Get.find<UserStateService>().userId,
);
_logic.initializeQuery(widget.query.period, widget.query.date);
_periodWorker = ever(_logic.selectedPeriod, (period) {
... ... @@ -504,7 +507,7 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
isVip: widget.isVip,
onSubscribe: widget.onSubscribe,
onReportViewed: widget.isSelected
? () => _appReviewPromptLogic.maybeShowPrompt(context)
? _appReviewPromptLogic.recordPromptEligibility
: null,
loadingIndicator: const _HealthTrendLoadingIndicator(),
);
... ...
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../app_review_prompt/logic/app_review_prompt_logic.dart';
import '../../../../data/local/local_storage.dart';
import '../../../../core/services/user_state_service.dart';
import '../controllers/home_controller.dart';
import '../widgets/df_tab_bar.dart';
import '../../friends/views/friends_tab.dart';
... ... @@ -58,13 +61,21 @@ class _HomeReviewPromptGate extends StatefulWidget {
}
class _HomeReviewPromptGateState extends State<_HomeReviewPromptGate> {
late final AppReviewPromptLogic _appReviewPromptLogic;
@override
void initState() {
super.initState();
_appReviewPromptLogic = AppReviewPromptLogic(
storage: Get.find<LocalStorage>(),
userIdProvider: () => Get.find<UserStateService>().userId,
);
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
await widget.controller.maybeShowMembershipOffer();
if (!mounted) return;
await _appReviewPromptLogic.maybeShowPendingPrompt(context);
});
}
... ...
... ... @@ -9,6 +9,7 @@ abstract final class StorageConst {
static const String lastLoginMethodKey = 'last_login_method';
static const String appReviewPromptNextShowAtKey =
'app_review_prompt_next_show_at';
static const String appReviewPromptPendingKey = 'app_review_prompt_pending';
static const String homeMembershipOfferPromptDateKey =
'home_membership_offer_prompt_date';
static const String homeMembershipOfferPromptUserIdKey =
... ...
... ... @@ -49,21 +49,40 @@ class LocalStorage {
// ─── App Review Prompt Storage ──────────────────────────────────────────────
DateTime? get appReviewPromptNextShowAt {
DateTime? appReviewPromptNextShowAt(int userId) {
final value = sharedPreferences.getInt(
StorageConst.appReviewPromptNextShowAtKey,
_appReviewPromptNextShowAtKey(userId),
);
if (value == null || value <= 0) return null;
return DateTime.fromMillisecondsSinceEpoch(value);
}
Future<void> setAppReviewPromptNextShowAt(DateTime value) async {
Future<void> setAppReviewPromptNextShowAt(int userId, DateTime value) async {
await sharedPreferences.setInt(
StorageConst.appReviewPromptNextShowAtKey,
_appReviewPromptNextShowAtKey(userId),
value.millisecondsSinceEpoch,
);
}
bool hasAppReviewPromptPending(int userId) {
return sharedPreferences.getBool(_appReviewPromptPendingKey(userId)) ??
false;
}
Future<void> setAppReviewPromptPending(int userId) async {
await sharedPreferences.setBool(_appReviewPromptPendingKey(userId), true);
}
Future<void> clearAppReviewPromptPending(int userId) async {
await sharedPreferences.remove(_appReviewPromptPendingKey(userId));
}
String _appReviewPromptNextShowAtKey(int userId) =>
'${StorageConst.appReviewPromptNextShowAtKey}_$userId';
String _appReviewPromptPendingKey(int userId) =>
'${StorageConst.appReviewPromptPendingKey}_$userId';
// ─── Home Membership Offer Prompt Storage ─────────────────────────────────
int? get homeMembershipOfferPromptDate =>
... ...