Commit d1e329b8e0e29971c2e8ae2b1985cb6093b1a156

Authored by 刘宏哲
1 parent 30f20ce4

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

import 'package:doublefeel_flutter/core/network/api/click_event_api.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:get/get.dart';
... ... @@ -56,7 +55,6 @@ void registerUserSessionDeps() {
Get.put(UserApi(dioClient), permanent: true);
Get.put(VipApi(dioClient), permanent: true);
Get.put(FriendApi(dioClient), permanent: true);
Get.put(ClickEventApi(dioClient), permanent: true);
Get.put<ImService>(ImServiceStub(), permanent: true);
Get.put(
ThinkingDataService(Get.find<AppEnvironmentConfig>()),
... ...
... ... @@ -38,7 +38,7 @@ class AppReviewPromptLogic {
final result = await showDialog<AppReviewPromptResult>(
context: context,
barrierColor: Colors.black.withValues(alpha: 0.7),
barrierDismissible: false,
barrierDismissible: true,
builder: (_) => const AppReviewPromptDialog(),
);
... ...
import 'package:doublefeel_flutter/core/constants/event_const.dart';
import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
import 'package:get/get.dart';
import '../../../../core/network/api/click_event_api.dart';
import '../../health_trend/controllers/health_trend_analytics.dart';
import '../../health_trend/controllers/health_trend_control.dart';
import '../../report_common/models/health_report_query.dart';
... ... @@ -70,12 +68,5 @@ class FriendTrendController extends GetxController with HealthTrendControl {
selectedTypeIndex.value,
userRole: '好友的',
);
_trackViewFriendSleepStatsIfNeeded();
}
void _trackViewFriendSleepStatsIfNeeded() {
if (selectedTypeIndex.value != 2) return;
if (!Get.isRegistered<ClickEventApi>()) return;
Get.find<ClickEventApi>().postClickEvent(EventConst.viewFriendSleepStats);
}
}
... ...
... ... @@ -3,10 +3,12 @@ import 'package:get/get.dart';
import 'package:lottie/lottie.dart';
import '../../../../core/constants/intent_keys.dart';
import '../../../../data/local/local_storage.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../routes/app_pages.dart';
import '../../activity_burn_report/controllers/activity_burn_report_logic.dart';
import '../../activity_burn_report/views/activity_burn_report_view.dart';
import '../../app_review_prompt/logic/app_review_prompt_logic.dart';
import '../../home/widgets/trend/trend_type_tab_bar.dart';
import '../../hrv_report/controllers/hrv_report_logic.dart';
import '../../hrv_report/views/hrv_report_view.dart';
... ... @@ -417,6 +419,7 @@ class _SleepTrendSection extends StatefulWidget {
class _SleepTrendSectionState extends State<_SleepTrendSection> {
late final SleepReportLogic _logic;
late final AppReviewPromptLogic _appReviewPromptLogic;
late final Worker _periodWorker;
late final Worker _dateWorker;
var _syncingExternalQuery = false;
... ... @@ -428,6 +431,9 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
_logic = SleepReportLogic(
initialTargetUserId: widget.query.targetUserId,
);
_appReviewPromptLogic = AppReviewPromptLogic(
storage: Get.find<LocalStorage>(),
);
_logic.initializeQuery(widget.query.period, widget.query.date);
_periodWorker = ever(_logic.selectedPeriod, (period) {
_scheduleQueryNotification();
... ... @@ -489,6 +495,9 @@ class _SleepTrendSectionState extends State<_SleepTrendSection> {
logic: _logic,
isVip: widget.isVip,
onSubscribe: widget.onSubscribe,
onReportViewed: widget.isSelected
? () => _appReviewPromptLogic.maybeShowPrompt(context)
: null,
loadingIndicator: const _HealthTrendLoadingIndicator(),
);
}
... ...
import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart';
import 'package:doublefeel_flutter/core/constants/event_const.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/network/api/click_event_api.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
... ... @@ -15,7 +13,6 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../../app_review_prompt/logic/app_review_prompt_logic.dart';
import '../../purchase/purchase_route_args.dart';
import '../../report_common/models/report_period.dart';
import 'today_controller.dart';
... ... @@ -31,9 +28,6 @@ class HomeController extends GetxController {
static const friendsTabIndex = 2;
static const _mainChannel = MethodChannel('doublefeel_flutter_main_channel');
final AppReviewPromptLogic _appReviewPromptLogic = AppReviewPromptLogic(
storage: Get.find<LocalStorage>(),
);
final LocalStorage _storage = Get.find<LocalStorage>();
final UserStateService userStateService = Get.find<UserStateService>();
final PlatformHostApi _platformHostApi = PlatformHostApi();
... ... @@ -90,30 +84,6 @@ class HomeController extends GetxController {
return url.isNotEmpty;
}
Future<void> maybeShowAppReviewPrompt(BuildContext context) async {
if (!_appReviewPromptLogic.canShowPrompt(context)) return;
final canShowPrompt = await _hasReviewPromptClickEvent();
if (!canShowPrompt) return;
await _appReviewPromptLogic.maybeShowPrompt(context);
}
Future<bool> _hasReviewPromptClickEvent() async {
if (!Get.isRegistered<ClickEventApi>()) return false;
final result = await Get.find<ClickEventApi>().getClickEvent();
return switch (result) {
AppSuccess(:final data) => data.list?.any(
(item) =>
EventConst.keys.contains(item.eventName) &&
(item.clickCnt ?? 0) > 0,
) ??
false,
AppFailure() => false,
};
}
Future<bool> maybeShowMembershipOffer() async {
if (isFromOnboard) {
isFromOnboard = false;
... ... @@ -193,7 +163,6 @@ class HomeController extends GetxController {
final params = uri.queryParameters; // {"tab": "today"} 等
_dispatchRoute(path, params);
_executePostClickEvent(path, params);
} catch (_) {}
}
... ... @@ -244,24 +213,6 @@ class HomeController extends GetxController {
}
}
void _executePostClickEvent(String path, Map<String, String> params) {
try {
var isHrvChange = params['is_hrv_change'] == "1";
if (isHrvChange) {
final clickEventApi = Get.find<ClickEventApi>();
if (!Get.isRegistered<ClickEventApi>()) return;
switch (path) {
case AppRoutes.home:
clickEventApi.postClickEvent(EventConst.hrvChangeClick);
break;
case Routes.FRIEND_HOME:
clickEventApi.postClickEvent(EventConst.friendHrvChangeClick);
break;
}
}
} catch (e) {}
}
/// 根据 tab 名称切换首页底部 tab
void _switchHomeTab(String? tab, Map<String, String> params) {
switch (tab) {
... ...
import 'package:doublefeel_flutter/core/constants/event_const.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../../core/network/api/click_event_api.dart';
import '../../../../../core/network/api/friend_api.dart';
import '../../../../../core/result/app_result.dart';
import '../../../../../data/models/friend/friend_models.dart';
... ... @@ -80,7 +78,6 @@ class TrendController extends GetxController with HealthTrendControl {
if (userId == targetUserId.value) return;
targetUserId.value = userId;
refreshToken.value++;
_trackViewSleepStatsIfNeeded();
}
void selectFriend(FriendItem friendInfo) {
... ... @@ -171,19 +168,5 @@ class TrendController extends GetxController with HealthTrendControl {
selectedTypeIndex.value,
userRole: targetFriendInfo.value == null ? '我的' : '好友的',
);
_trackViewSleepStatsIfNeeded();
}
void _trackViewSleepStatsIfNeeded() {
if (!_isPageVisible ||
selectedTypeIndex.value != TrendType.sleep.tabIndex) {
return;
}
final eventName = targetUserId.value == null
? EventConst.viewSleepStats
: EventConst.viewFriendSleepStats;
if (!Get.isRegistered<ClickEventApi>()) return;
Get.find<ClickEventApi>().postClickEvent(eventName);
}
}
... ...
... ... @@ -65,8 +65,6 @@ class _HomeReviewPromptGateState extends State<_HomeReviewPromptGate> {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return;
await widget.controller.maybeShowMembershipOffer();
if (!mounted) return;
widget.controller.maybeShowAppReviewPrompt(context);
});
}
... ...
... ... @@ -16,21 +16,77 @@ import '../widgets/sleep_quality_ring.dart';
import '../widgets/sleep_summary_card.dart';
import '../widgets/sleep_week_report_view.dart';
class SleepReportView extends StatelessWidget {
class SleepReportView extends StatefulWidget {
const SleepReportView({
super.key,
required this.logic,
required this.isVip,
required this.onSubscribe,
this.onReportViewed,
this.loadingIndicator = const ReportLoadingIndicator(),
});
final SleepReportLogic logic;
final bool isVip;
final ValueChanged<String> onSubscribe;
final Future<void> Function()? onReportViewed;
final Widget loadingIndicator;
@override
State<SleepReportView> createState() => _SleepReportViewState();
}
class _SleepReportViewState extends State<SleepReportView> {
String? _lastViewedReportKey;
bool _viewNotificationScheduled = false;
@override
void didUpdateWidget(covariant SleepReportView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.onReportViewed == null && widget.onReportViewed != null) {
_scheduleViewedReportNotification();
}
}
void _scheduleViewedReportNotification() {
if (_viewNotificationScheduled || widget.onReportViewed == null) return;
final reportKey = _loadedReportKey();
if (reportKey == null || reportKey == _lastViewedReportKey) return;
_viewNotificationScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
_viewNotificationScheduled = false;
if (!mounted || widget.onReportViewed == null) return;
if (_loadedReportKey() != reportKey ||
_lastViewedReportKey == reportKey) {
return;
}
_lastViewedReportKey = reportKey;
await widget.onReportViewed!();
});
}
String? _loadedReportKey() {
if (widget.logic.isLoading.value) return null;
final period = widget.logic.selectedPeriod.value;
final hasData = switch (period) {
ReportPeriod.day => widget.logic.report.value?.hasData ?? false,
ReportPeriod.week =>
widget.logic.weeklyReport.value?.daysWithData.isNotEmpty ?? false,
ReportPeriod.month =>
widget.logic.monthlyReport.value?.daysWithData.isNotEmpty ?? false,
ReportPeriod.year => false,
};
if (!hasData) return null;
return '${widget.logic.targetUserId.value ?? 'self'}:'
'${period.name}:${widget.logic.selectedDate.value.toIso8601String()}';
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.transparent,
... ... @@ -45,25 +101,27 @@ class SleepReportView extends StatelessWidget {
const SizedBox(height: 8),
Obx(
() => ReportPeriodTabBar(
selectedPeriod: logic.selectedPeriod.value,
onChanged: logic.selectPeriod,
lockedPeriods: isVip
selectedPeriod: widget.logic.selectedPeriod.value,
onChanged: widget.logic.selectPeriod,
lockedPeriods: widget.isVip
? const {}
: const {ReportPeriod.week, ReportPeriod.month},
onLockedPeriodTap: (_) => onSubscribe('非会员切换月周-睡眠报告'),
onLockedPeriodTap: (_) =>
widget.onSubscribe('非会员切换月周-睡眠报告'),
),
),
const SizedBox(height: 8),
_DateSwitcher(logic: logic),
_DateSwitcher(logic: widget.logic),
],
),
),
Expanded(
child: Obx(
() {
if (logic.isLoading.value) {
return loadingIndicator;
if (widget.logic.isLoading.value) {
return widget.loadingIndicator;
}
_scheduleViewedReportNotification();
return CustomScrollView(
physics: const ClampingScrollPhysics(),
slivers: [
... ... @@ -71,19 +129,21 @@ class SleepReportView extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
sliver: SliverList(
delegate: SliverChildListDelegate([
if (logic.selectedPeriod.value ==
if (widget.logic.selectedPeriod.value ==
ReportPeriod.week)
SleepWeekReportView(
report: logic.weeklyReport.value,
report: widget.logic.weeklyReport.value,
)
else if (logic.selectedPeriod.value ==
else if (widget.logic.selectedPeriod.value ==
ReportPeriod.month)
SleepMonthReportView(
monthStart: logic.monthStart,
report: logic.monthlyReport.value,
monthStart: widget.logic.monthStart,
report: widget.logic.monthlyReport.value,
)
else
_DailyReportContent(report: logic.report.value),
_DailyReportContent(
report: widget.logic.report.value,
),
const ReportBottomSlogan(),
]),
),
... ...
abstract final class EventConst {
static const String hrvChangeClick = "hrv_change_click";
static const String friendHrvChangeClick = "friend_hrv_change_click";
static const String viewSleepStats = "view_sleep_stats";
static const String viewFriendSleepStats = "view_friend_sleep_stats";
static const Set<String> keys = {
hrvChangeClick,
friendHrvChangeClick,
viewSleepStats,
viewFriendSleepStats,
};
}
import 'package:doublefeel_flutter/data/models/event/click_event_data.dart';
import '../../result/app_result.dart';
import '../../result/safe_call.dart';
import '../api_paths.dart';
import '../dio_client.dart';
class ClickEventApi {
ClickEventApi(this._dioClient);
final DioClient _dioClient;
Future<AppResult<void>> postClickEvent(String eventName) {
return safeCall(
call: () async {
await _dioClient.dio.post(
ApiPaths.clickEvent,
data: {'event_name': eventName},
);
},
);
}
Future<AppResult<ClickEventData>> getClickEvent() {
return safeCall(
call: () async {
final response = await _dioClient.dio.get(ApiPaths.clickEvent);
return ClickEventData.fromJson(response.data as Map<String, dynamic>);
},
errorHandlingPolicy: null,
);
}
}
... ... @@ -72,7 +72,4 @@ abstract final class ApiPaths {
// friends
static const friends = '/client/doublefeel/health/v2/friends/';
static const friendInfo = '/client/doublefeel/health/v2/friend_info/';
// click event
static const clickEvent = '/client/doublefeel/click_event/';
}
... ...
class ClickEventData {
ClickEventData({
this.list,
});
ClickEventData.fromJson(dynamic json) {
if (json['list'] != null) {
list = [];
json['list'].forEach((v) {
list?.add(ClickEventItem.fromJson(v));
});
}
}
List<ClickEventItem>? list;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (list != null) {
map['list'] = list?.map((v) => v.toJson()).toList();
}
return map;
}
}
class ClickEventItem {
ClickEventItem({
this.eventName,
this.clickCnt,
});
ClickEventItem.fromJson(dynamic json) {
eventName = json['event_name'];
clickCnt = json['click_cnt'];
}
String? eventName;
num? clickCnt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['event_name'] = eventName;
map['click_cnt'] = clickCnt;
return map;
}
}