Commit 5894b5955a6d5dd487271f8b3445d1fbcf46bcca

Authored by 刘宏哲
1 parent 5541eb49

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';
... ... @@ -55,6 +56,7 @@ 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>()),
... ...
... ... @@ -33,6 +33,16 @@ ActivityBurnDurationText activityBurnDurationText(
);
}
ActivityBurnDurationText activityBurnMinutesText(
BuildContext context,
int? minutes,
) {
return ActivityBurnDurationText(
value: minutes?.toString() ?? '-',
unit: context.l10n.reportUnitMinute,
);
}
ActivityBurnDurationText activityBurnHoursText(
BuildContext context,
int? hours,
... ...
... ... @@ -19,7 +19,7 @@ class ActivityBurnSummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final exercise = activityBurnDurationText(
final exercise = activityBurnMinutesText(
context,
report?.exerciseMinutes?.value,
);
... ...
... ... @@ -22,12 +22,16 @@ class AppReviewPromptLogic {
static const _likedCooldown = Duration(days: 90);
static const _feedbackCooldown = Duration(days: 30);
Future<void> maybeShowPrompt(BuildContext context) async {
if (_isShowing || !context.mounted) return;
bool canShowPrompt(BuildContext context) {
if (_isShowing || !context.mounted) return false;
final now = DateTime.now();
final nextShowAt = _storage.appReviewPromptNextShowAt;
if (nextShowAt != null && now.isBefore(nextShowAt)) return;
return nextShowAt == null || !now.isBefore(nextShowAt);
}
Future<void> maybeShowPrompt(BuildContext context) async {
if (!canShowPrompt(context)) return;
_isShowing = true;
try {
... ...
... ... @@ -18,6 +18,7 @@ class AppReviewPromptDialog extends StatelessWidget {
illustrationAsset: R.assetsImagesAppReviewGoodBg,
title: context.l10n.appReviewPromptTitle,
message: context.l10n.appReviewPromptMessage,
primaryEmoji: context.l10n.appReviewPromptLikeActionEmoji,
primaryText: context.l10n.appReviewPromptLikeAction,
secondaryText: context.l10n.appReviewPromptFeedbackAction,
onPrimaryTap: () =>
... ... @@ -57,12 +58,14 @@ class _ReviewPromptCard extends StatelessWidget {
required this.secondaryText,
required this.onPrimaryTap,
required this.onSecondaryTap,
this.primaryEmoji,
});
final double height;
final String illustrationAsset;
final String title;
final String message;
final String? primaryEmoji;
final String primaryText;
final String secondaryText;
final VoidCallback onPrimaryTap;
... ... @@ -149,6 +152,7 @@ class _ReviewPromptCard extends StatelessWidget {
left: 40,
right: 40,
child: _PromptButton(
emoji: primaryEmoji,
text: primaryText,
textColor: Colors.white,
fontWeight: FontWeight.w600,
... ... @@ -182,15 +186,31 @@ class _PromptButton extends StatelessWidget {
required this.textColor,
required this.fontWeight,
required this.onTap,
this.emoji,
this.backgroundColor,
});
final String? emoji;
final String text;
final Color textColor;
final FontWeight fontWeight;
final VoidCallback onTap;
final Color? backgroundColor;
TextStyle get _emojiStyle => TextStyle(
color: textColor,
fontSize: 18,
height: 1.2,
fontWeight: fontWeight,
);
TextStyle get _textStyle => TextStyle(
color: textColor,
fontSize: 16,
height: 1.2,
fontWeight: fontWeight,
);
@override
Widget build(BuildContext context) {
return GestureDetector(
... ... @@ -203,18 +223,42 @@ class _PromptButton extends StatelessWidget {
color: backgroundColor,
borderRadius: BorderRadius.circular(24),
),
child: Text(
text,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: _buildContent(),
),
);
}
Widget _buildContent() {
final emojiText = emoji;
if (emojiText == null) {
return Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _textStyle,
);
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
emojiText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: textColor,
fontSize: 16,
height: 1.2,
fontWeight: fontWeight,
style: _emojiStyle,
),
const SizedBox(width: 2),
Flexible(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: _textStyle,
),
),
),
],
);
}
}
... ...
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';
... ... @@ -68,5 +70,12 @@ 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);
}
}
... ...
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';
... ... @@ -7,14 +10,13 @@ import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/local_storage.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../../purchase/purchase_route_args.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';
import 'trend/trend_controller.dart';
... ... @@ -81,8 +83,29 @@ class HomeController extends GetxController {
return url.isNotEmpty;
}
Future<void> maybeShowAppReviewPrompt(BuildContext context) {
return _appReviewPromptLogic.maybeShowPrompt(context);
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 {
... ... @@ -102,7 +125,10 @@ class HomeController extends GetxController {
);
await Get.toNamed(
Routes.PURCHASE,
arguments: {PurchaseRouteArgs.showCloseButton: true},
arguments: {
PurchaseRouteArgs.showCloseButton: true,
IntentKeys.channelType: "登录全屏推送",
},
);
return true;
}
... ... @@ -120,7 +146,7 @@ class HomeController extends GetxController {
} else if (previousIndex == friendsTabIndex && index != friendsTabIndex) {
Get.find<FriendsController>().markPageHidden();
}
switch (index) {
switch (index) {
case 0:
Get.find<TodayController>().refreshTab();
break;
... ... @@ -149,6 +175,7 @@ class HomeController extends GetxController {
final params = uri.queryParameters; // {"tab": "today"} 等
_dispatchRoute(path, params);
_executePostClickEvent(path, params);
} catch (_) {}
}
... ... @@ -199,6 +226,24 @@ 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';
... ... @@ -78,6 +80,7 @@ class TrendController extends GetxController with HealthTrendControl {
if (userId == targetUserId.value) return;
targetUserId.value = userId;
refreshToken.value++;
_trackViewSleepStatsIfNeeded();
}
void selectFriend(FriendItem friendInfo) {
... ... @@ -154,5 +157,19 @@ 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);
}
}
... ...
... ... @@ -96,7 +96,11 @@ class PurchaseController extends GetxController {
arguments: {'from': 'onboarding'},
);
} else {
Get.back();
if (Navigator.canPop(Get.context!)) {
Get.back();
} else {
Get.offAllNamed(AppRoutes.home);
}
}
}
... ...
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,4 +72,7 @@ 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;
}
}
... ...
... ... @@ -498,7 +498,8 @@
"refundFaqTitle": "DoubleFeel FAQs",
"appReviewPromptTitle": "Enjoying DoubleFeel?",
"appReviewPromptMessage": "Hi! Is DoubleFeel helping you better understand\nyour stress and sleep? 💜",
"appReviewPromptLikeAction": "😍 Love it",
"appReviewPromptLikeActionEmoji": "😍",
"appReviewPromptLikeAction": "Love it",
"appReviewPromptFeedbackAction": "I have feedback",
"appReviewFeedbackTitle": "We're sorry DoubleFeel didn't give you\na good experience",
"appReviewFeedbackMessage": "Would you tell us what went wrong?\nYour feedback helps us improve the stress and health experience. 💜",
... ...
... ... @@ -786,7 +786,8 @@
"refundFaqTitle": "DoubleFeel 常见问题",
"appReviewPromptTitle": "喜欢 DoubleFeel 吗?",
"appReviewPromptMessage": "嗨~想知道 DoubleFeel 是否正在帮助你\n更了解自己的压力与睡眠状态 💜",
"appReviewPromptLikeAction": "😍 很喜欢",
"appReviewPromptLikeActionEmoji": "😍",
"appReviewPromptLikeAction": "很喜欢",
"appReviewPromptFeedbackAction": "我有意见",
"appReviewFeedbackTitle": "很抱歉 DoubleFeel 没有带\n给你好的体验",
"appReviewFeedbackMessage": "愿意告诉我们遇到了什么问题吗?\n你的反馈可以帮助我们持续改进压力与健康体验 💜",
... ...
... ... @@ -3087,10 +3087,16 @@ abstract class AppLocalizations {
/// **'嗨~想知道 DoubleFeel 是否正在帮助你\n更了解自己的压力与睡眠状态 💜'**
String get appReviewPromptMessage;
/// No description provided for @appReviewPromptLikeActionEmoji.
///
/// In zh, this message translates to:
/// **'😍'**
String get appReviewPromptLikeActionEmoji;
/// No description provided for @appReviewPromptLikeAction.
///
/// In zh, this message translates to:
/// **'😍 很喜欢'**
/// **'很喜欢'**
String get appReviewPromptLikeAction;
/// No description provided for @appReviewPromptFeedbackAction.
... ...
... ... @@ -1722,7 +1722,10 @@ class AppLocalizationsEn extends AppLocalizations {
'Hi! Is DoubleFeel helping you better understand\nyour stress and sleep? 💜';
@override
String get appReviewPromptLikeAction => '😍 Love it';
String get appReviewPromptLikeActionEmoji => '😍';
@override
String get appReviewPromptLikeAction => 'Love it';
@override
String get appReviewPromptFeedbackAction => 'I have feedback';
... ...
... ... @@ -1628,7 +1628,10 @@ class AppLocalizationsZh extends AppLocalizations {
'嗨~想知道 DoubleFeel 是否正在帮助你\n更了解自己的压力与睡眠状态 💜';
@override
String get appReviewPromptLikeAction => '😍 很喜欢';
String get appReviewPromptLikeActionEmoji => '😍';
@override
String get appReviewPromptLikeAction => '很喜欢';
@override
String get appReviewPromptFeedbackAction => '我有意见';
... ...