Commit 9a6eb289e699acb68d4870cc64441c9546c953b4

Authored by 刘宏哲
1 parent d9a755aa

feat(app): add burial points

@@ -25,7 +25,7 @@ class ActivityBurnReportView extends StatelessWidget { @@ -25,7 +25,7 @@ class ActivityBurnReportView extends StatelessWidget {
25 25
26 final ActivityBurnReportLogic logic; 26 final ActivityBurnReportLogic logic;
27 final bool isVip; 27 final bool isVip;
28 - final VoidCallback onSubscribe; 28 + final ValueChanged<String> onSubscribe;
29 29
30 @override 30 @override
31 Widget build(BuildContext context) { 31 Widget build(BuildContext context) {
@@ -47,7 +47,7 @@ class ActivityBurnReportView extends StatelessWidget { @@ -47,7 +47,7 @@ class ActivityBurnReportView extends StatelessWidget {
47 lockedPeriods: isVip 47 lockedPeriods: isVip
48 ? const {} 48 ? const {}
49 : const {ReportPeriod.week, ReportPeriod.month}, 49 : const {ReportPeriod.week, ReportPeriod.month},
50 - onLockedPeriodTap: (_) => onSubscribe(), 50 + onLockedPeriodTap: (_) => onSubscribe('非会员切换月周-活动消耗'),
51 ), 51 ),
52 ), 52 ),
53 const SizedBox(height: 8), 53 const SizedBox(height: 8),
1 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; 1 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
2 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
3 3
  4 +import '../../health_trend/controllers/health_trend_analytics.dart';
4 import '../../health_trend/controllers/health_trend_control.dart'; 5 import '../../health_trend/controllers/health_trend_control.dart';
5 import '../../report_common/models/health_report_query.dart'; 6 import '../../report_common/models/health_report_query.dart';
6 import '../../report_common/models/report_period.dart'; 7 import '../../report_common/models/report_period.dart';
@@ -32,6 +33,7 @@ class FriendTrendController extends GetxController with HealthTrendControl { @@ -32,6 +33,7 @@ class FriendTrendController extends GetxController with HealthTrendControl {
32 } 33 }
33 34
34 final FriendItem? friendItem; 35 final FriendItem? friendItem;
  36 + bool _isPageReady = false;
35 37
36 HealthReportQuery get query => HealthReportQuery( 38 HealthReportQuery get query => HealthReportQuery(
37 targetUserId: friendItem?.friendUserId, 39 targetUserId: friendItem?.friendUserId,
@@ -44,4 +46,27 @@ class FriendTrendController extends GetxController with HealthTrendControl { @@ -44,4 +46,27 @@ class FriendTrendController extends GetxController with HealthTrendControl {
44 period: periodForType(typeIndex), 46 period: periodForType(typeIndex),
45 date: dateForType(typeIndex), 47 date: dateForType(typeIndex),
46 ); 48 );
  49 +
  50 + @override
  51 + void onReady() {
  52 + super.onReady();
  53 + _isPageReady = true;
  54 + _trackEnterPage();
  55 + }
  56 +
  57 + @override
  58 + void changeType(int index) {
  59 + final previousIndex = selectedTypeIndex.value;
  60 + super.changeType(index);
  61 + if (_isPageReady && selectedTypeIndex.value != previousIndex) {
  62 + _trackEnterPage();
  63 + }
  64 + }
  65 +
  66 + void _trackEnterPage() {
  67 + HealthTrendAnalytics.trackEnterPage(
  68 + selectedTypeIndex.value,
  69 + userRole: '好友的',
  70 + );
  71 + }
47 } 72 }
@@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/core/error/app_error.dart'; @@ -8,6 +8,7 @@ import 'package:doublefeel_flutter/core/error/app_error.dart';
8 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 8 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
9 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 9 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
10 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 10 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
  11 +import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
11 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 12 import 'package:doublefeel_flutter/core/util/app_toast.dart';
12 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; 13 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
13 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 14 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
@@ -43,6 +44,7 @@ class FriendsController extends GetxController { @@ -43,6 +44,7 @@ class FriendsController extends GetxController {
43 final isSelfHealthLoading = false.obs; 44 final isSelfHealthLoading = false.obs;
44 Future<void>? _friendsRequest; 45 Future<void>? _friendsRequest;
45 Future<void>? _selfHealthRequest; 46 Future<void>? _selfHealthRequest;
  47 + bool _isPageVisible = false;
46 48
47 bool get isFull => friends.length >= maxFriends; 49 bool get isFull => friends.length >= maxFriends;
48 50
@@ -52,6 +54,16 @@ class FriendsController extends GetxController { @@ -52,6 +54,16 @@ class FriendsController extends GetxController {
52 unawaited(refreshData()); 54 unawaited(refreshData());
53 } 55 }
54 56
  57 + void markPageVisible() {
  58 + if (_isPageVisible) return;
  59 + _isPageVisible = true;
  60 + ta.track('enter_doublefeel_friend_page');
  61 + }
  62 +
  63 + void markPageHidden() {
  64 + _isPageVisible = false;
  65 + }
  66 +
55 Future<void> refreshData() { 67 Future<void> refreshData() {
56 return Future.wait([loadFriends(), loadSelfHealth()]); 68 return Future.wait([loadFriends(), loadSelfHealth()]);
57 } 69 }
@@ -246,7 +246,7 @@ class FriendsTab extends GetView<FriendsController> { @@ -246,7 +246,7 @@ class FriendsTab extends GetView<FriendsController> {
246 final String name = type.toString(); 246 final String name = type.toString();
247 await Get.toNamed( 247 await Get.toNamed(
248 AppRoutes.bindPartner, 248 AppRoutes.bindPartner,
249 - arguments: {IntentKeys.keyFrom: name}, 249 + arguments: {IntentKeys.keyFrom: name, IntentKeys.channelType: '好友页'},
250 ); 250 );
251 await controller.loadFriends(); 251 await controller.loadFriends();
252 } 252 }
  1 +import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
  2 +
  3 +class HealthTrendAnalytics {
  4 + const HealthTrendAnalytics._();
  5 +
  6 + static void trackEnterPage(int typeIndex, {required String userRole}) {
  7 + final eventName = switch (typeIndex) {
  8 + 1 => 'enter_doublefeel_activity_cost_page',
  9 + 2 => 'enter_doublefeel_sleep_report_page',
  10 + _ => 'enter_doublefeel_hrv_page',
  11 + };
  12 + ta.track(
  13 + eventName,
  14 + properties: {'user_role': userRole},
  15 + );
  16 + }
  17 +}
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 '../../../../core/constants/intent_keys.dart';
4 import '../../../../data/local/user_preferences_storage.dart'; 5 import '../../../../data/local/user_preferences_storage.dart';
5 import '../../../routes/app_pages.dart'; 6 import '../../../routes/app_pages.dart';
6 import '../../activity_burn_report/controllers/activity_burn_report_logic.dart'; 7 import '../../activity_burn_report/controllers/activity_burn_report_logic.dart';
@@ -99,7 +100,10 @@ class _HealthTrendContentState extends State<HealthTrendContent> @@ -99,7 +100,10 @@ class _HealthTrendContentState extends State<HealthTrendContent>
99 final preferences = userPrefs.preferences.value; 100 final preferences = userPrefs.preferences.value;
100 final vipInfo = preferences.vipInfo; 101 final vipInfo = preferences.vipInfo;
101 final isVip = vipInfo?.isVip ?? false; 102 final isVip = vipInfo?.isVip ?? false;
102 - void openPurchase() => Get.toNamed(Routes.PURCHASE); 103 + void openPurchase(String channelType) => Get.toNamed(
  104 + Routes.PURCHASE,
  105 + arguments: {IntentKeys.channelType: channelType},
  106 + );
103 107
104 return HealthReportSubjectScope( 108 return HealthReportSubjectScope(
105 isSelf: widget.query.isSelf, 109 isSelf: widget.query.isSelf,
@@ -158,7 +162,7 @@ class _HrvTrendSection extends StatefulWidget { @@ -158,7 +162,7 @@ class _HrvTrendSection extends StatefulWidget {
158 162
159 final HealthReportQuery query; 163 final HealthReportQuery query;
160 final bool isVip; 164 final bool isVip;
161 - final VoidCallback onSubscribe; 165 + final ValueChanged<String> onSubscribe;
162 final ValueChanged<ReportPeriod> onPeriodChanged; 166 final ValueChanged<ReportPeriod> onPeriodChanged;
163 final ValueChanged<DateTime> onDateChanged; 167 final ValueChanged<DateTime> onDateChanged;
164 168
@@ -231,7 +235,7 @@ class _ActivityBurnTrendSection extends StatefulWidget { @@ -231,7 +235,7 @@ class _ActivityBurnTrendSection extends StatefulWidget {
231 235
232 final HealthReportQuery query; 236 final HealthReportQuery query;
233 final bool isVip; 237 final bool isVip;
234 - final VoidCallback onSubscribe; 238 + final ValueChanged<String> onSubscribe;
235 final ValueChanged<ReportPeriod> onPeriodChanged; 239 final ValueChanged<ReportPeriod> onPeriodChanged;
236 final ValueChanged<DateTime> onDateChanged; 240 final ValueChanged<DateTime> onDateChanged;
237 241
@@ -307,7 +311,7 @@ class _SleepTrendSection extends StatefulWidget { @@ -307,7 +311,7 @@ class _SleepTrendSection extends StatefulWidget {
307 311
308 final HealthReportQuery query; 312 final HealthReportQuery query;
309 final bool isVip; 313 final bool isVip;
310 - final VoidCallback onSubscribe; 314 + final ValueChanged<String> onSubscribe;
311 final ValueChanged<ReportPeriod> onPeriodChanged; 315 final ValueChanged<ReportPeriod> onPeriodChanged;
312 final ValueChanged<DateTime> onDateChanged; 316 final ValueChanged<DateTime> onDateChanged;
313 317
1 import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart'; 1 import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
  2 +import 'package:doublefeel_flutter/app/modules/friends/controllers/friends_controller.dart';
2 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 3 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
3 -import 'package:doublefeel_flutter/core/network/api/user_api.dart';  
4 import 'package:doublefeel_flutter/core/result/app_result.dart'; 4 import 'package:doublefeel_flutter/core/result/app_result.dart';
5 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 5 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
6 import 'package:doublefeel_flutter/data/local/local_storage.dart'; 6 import 'package:doublefeel_flutter/data/local/local_storage.dart';
@@ -20,6 +20,7 @@ import 'trend/trend_controller.dart'; @@ -20,6 +20,7 @@ import 'trend/trend_controller.dart';
20 20
21 class HomeController extends GetxController { 21 class HomeController extends GetxController {
22 static const trendTabIndex = 1; 22 static const trendTabIndex = 1;
  23 + static const friendsTabIndex = 2;
23 static const _mainChannel = MethodChannel('doublefeel_flutter_main_channel'); 24 static const _mainChannel = MethodChannel('doublefeel_flutter_main_channel');
24 25
25 final AppReviewPromptLogic _appReviewPromptLogic = AppReviewPromptLogic( 26 final AppReviewPromptLogic _appReviewPromptLogic = AppReviewPromptLogic(
@@ -106,7 +107,18 @@ class HomeController extends GetxController { @@ -106,7 +107,18 @@ class HomeController extends GetxController {
106 } 107 }
107 108
108 void changeTab(int index) { 109 void changeTab(int index) {
  110 + final previousIndex = selectedIndex.value;
109 selectedIndex.value = index; 111 selectedIndex.value = index;
  112 + if (index == trendTabIndex && previousIndex != trendTabIndex) {
  113 + Get.find<TrendController>().markPageVisible();
  114 + } else if (previousIndex == trendTabIndex && index != trendTabIndex) {
  115 + Get.find<TrendController>().markPageHidden();
  116 + }
  117 + if (index == friendsTabIndex && previousIndex != friendsTabIndex) {
  118 + Get.find<FriendsController>().markPageVisible();
  119 + } else if (previousIndex == friendsTabIndex && index != friendsTabIndex) {
  120 + Get.find<FriendsController>().markPageHidden();
  121 + }
110 } 122 }
111 123
112 void openTrend(TrendType type, {DateTime? date, ReportPeriod? period}) { 124 void openTrend(TrendType type, {DateTime? date, ReportPeriod? period}) {
@@ -199,21 +211,16 @@ class HomeController extends GetxController { @@ -199,21 +211,16 @@ class HomeController extends GetxController {
199 } 211 }
200 break; 212 break;
201 case 'trend': 213 case 'trend':
202 - changeTab(trendTabIndex);  
203 final type = params['type']; 214 final type = params['type'];
204 - if (type != null) {  
205 - SchedulerBinding.instance.addPostFrameCallback((_) {  
206 - TrendType? trendType = getTrendType(type);  
207 - if (trendType != null) {  
208 - openTrend(  
209 - trendType,  
210 - );  
211 - }  
212 - }); 215 + final trendType = type == null ? null : getTrendType(type);
  216 + if (trendType != null) {
  217 + openTrend(trendType);
  218 + } else {
  219 + changeTab(trendTabIndex);
213 } 220 }
214 break; 221 break;
215 case 'friends': 222 case 'friends':
216 - changeTab(2); 223 + changeTab(friendsTabIndex);
217 break; 224 break;
218 case 'my': 225 case 'my':
219 changeTab(3); 226 changeTab(3);
1 import 'package:get/get.dart'; 1 import 'package:get/get.dart';
2 2
  3 +import '../../../health_trend/controllers/health_trend_analytics.dart';
3 import '../../../health_trend/controllers/health_trend_control.dart'; 4 import '../../../health_trend/controllers/health_trend_control.dart';
4 import '../../../report_common/models/health_report_query.dart'; 5 import '../../../report_common/models/health_report_query.dart';
5 import '../../../report_common/models/report_period.dart'; 6 import '../../../report_common/models/report_period.dart';
@@ -15,6 +16,8 @@ enum TrendType { @@ -15,6 +16,8 @@ enum TrendType {
15 /// 趋势页顶层 Controller,仅负责: 16 /// 趋势页顶层 Controller,仅负责:
16 /// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex) 17 /// 顶层 HRV / 活动 / 睡眠 类型切换 (selectedTypeIndex)
17 class TrendController extends GetxController with HealthTrendControl { 18 class TrendController extends GetxController with HealthTrendControl {
  19 + bool _isPageVisible = false;
  20 +
18 // 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。 21 // 当前查看的用户。null 表示查看自己;非 null 表示查看指定用户。
19 final targetUserId = RxnInt(); 22 final targetUserId = RxnInt();
20 23
@@ -30,6 +33,15 @@ class TrendController extends GetxController with HealthTrendControl { @@ -30,6 +33,15 @@ class TrendController extends GetxController with HealthTrendControl {
30 date: dateForType(typeIndex), 33 date: dateForType(typeIndex),
31 ); 34 );
32 35
  36 + @override
  37 + void changeType(int index) {
  38 + final previousIndex = selectedTypeIndex.value;
  39 + super.changeType(index);
  40 + if (_isPageVisible && selectedTypeIndex.value != previousIndex) {
  41 + _trackEnterPage();
  42 + }
  43 + }
  44 +
33 void selectType(TrendType type, {DateTime? date, ReportPeriod? period}) { 45 void selectType(TrendType type, {DateTime? date, ReportPeriod? period}) {
34 changeType(type.tabIndex); 46 changeType(type.tabIndex);
35 if (period != null) { 47 if (period != null) {
@@ -44,4 +56,21 @@ class TrendController extends GetxController with HealthTrendControl { @@ -44,4 +56,21 @@ class TrendController extends GetxController with HealthTrendControl {
44 if (userId == targetUserId.value) return; 56 if (userId == targetUserId.value) return;
45 targetUserId.value = userId; 57 targetUserId.value = userId;
46 } 58 }
  59 +
  60 + void markPageVisible() {
  61 + if (_isPageVisible) return;
  62 + _isPageVisible = true;
  63 + _trackEnterPage();
  64 + }
  65 +
  66 + void markPageHidden() {
  67 + _isPageVisible = false;
  68 + }
  69 +
  70 + void _trackEnterPage() {
  71 + HealthTrendAnalytics.trackEnterPage(
  72 + selectedTypeIndex.value,
  73 + userRole: '我的',
  74 + );
  75 + }
47 } 76 }
@@ -22,7 +22,7 @@ class HrvReportView extends StatelessWidget { @@ -22,7 +22,7 @@ class HrvReportView extends StatelessWidget {
22 22
23 final HrvReportLogic logic; 23 final HrvReportLogic logic;
24 final bool isVip; 24 final bool isVip;
25 - final VoidCallback onSubscribe; 25 + final ValueChanged<String> onSubscribe;
26 26
27 @override 27 @override
28 Widget build(BuildContext context) { 28 Widget build(BuildContext context) {
@@ -44,20 +44,24 @@ class HrvReportView extends StatelessWidget { @@ -44,20 +44,24 @@ class HrvReportView extends StatelessWidget {
44 lockedPeriods: isVip 44 lockedPeriods: isVip
45 ? const {} 45 ? const {}
46 : const {ReportPeriod.month, ReportPeriod.year}, 46 : const {ReportPeriod.month, ReportPeriod.year},
47 - onLockedPeriodTap: (_) => onSubscribe(), 47 + onLockedPeriodTap: (_) => onSubscribe('非会员切换年月-HRV心率'),
48 ), 48 ),
49 ), 49 ),
50 const SizedBox(height: 8), 50 const SizedBox(height: 8),
51 Obx( 51 Obx(
52 () => ReportDateSwitcher( 52 () => ReportDateSwitcher(
53 label: logic.dateLabel, 53 label: logic.dateLabel,
54 - onPrevious: isVip ? logic.previousDay : onSubscribe,  
55 - onNext: isVip ? logic.nextDay : onSubscribe, 54 + onPrevious: isVip
  55 + ? logic.previousDay
  56 + : () => onSubscribe('非会员切换日期-HRV心率'),
  57 + onNext: isVip
  58 + ? logic.nextDay
  59 + : () => onSubscribe('非会员切换日期-HRV心率'),
56 canPrevious: logic.canGoPrevious, 60 canPrevious: logic.canGoPrevious,
57 canNext: logic.canGoNext, 61 canNext: logic.canGoNext,
58 onTapLabel: isVip 62 onTapLabel: isVip
59 ? () => _showDatePicker(context) 63 ? () => _showDatePicker(context)
60 - : onSubscribe, 64 + : () => onSubscribe('非会员切换日期-HRV心率'),
61 ), 65 ),
62 ), 66 ),
63 ], 67 ],
@@ -82,7 +86,7 @@ class HrvReportView extends StatelessWidget { @@ -82,7 +86,7 @@ class HrvReportView extends StatelessWidget {
82 HrvWeekReportView( 86 HrvWeekReportView(
83 report: logic.weeklyReport.value, 87 report: logic.weeklyReport.value,
84 showExample: !isVip, 88 showExample: !isVip,
85 - onSubscribe: onSubscribe, 89 + onSubscribe: () => onSubscribe('解锁每日压力-HRV心率'),
86 ), 90 ),
87 ReportBottomSlogan(bottomSpacing: isVip ? 16 : 88), 91 ReportBottomSlogan(bottomSpacing: isVip ? 16 : 88),
88 ], 92 ],
@@ -100,7 +104,9 @@ class HrvReportView extends StatelessWidget { @@ -100,7 +104,9 @@ class HrvReportView extends StatelessWidget {
100 right: 0, 104 right: 0,
101 bottom: MediaQuery.paddingOf(context).bottom + 24, 105 bottom: MediaQuery.paddingOf(context).bottom + 24,
102 child: Center( 106 child: Center(
103 - child: _UnlockButton(onPressed: onSubscribe), 107 + child: _UnlockButton(
  108 + onPressed: () => onSubscribe('解锁每日压力-HRV心率'),
  109 + ),
104 ), 110 ),
105 ) 111 )
106 : const SizedBox.shrink(), 112 : const SizedBox.shrink(),
@@ -2,9 +2,11 @@ import 'dart:async'; @@ -2,9 +2,11 @@ import 'dart:async';
2 2
3 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 3 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
4 import 'package:doublefeel_flutter/core/constants/app_const.dart'; 4 import 'package:doublefeel_flutter/core/constants/app_const.dart';
  5 +import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
5 import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; 6 import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
6 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 7 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
7 import 'package:doublefeel_flutter/core/result/app_result.dart'; 8 import 'package:doublefeel_flutter/core/result/app_result.dart';
  9 +import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
8 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 10 import 'package:doublefeel_flutter/core/util/app_toast.dart';
9 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 11 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
10 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 12 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
@@ -15,6 +17,8 @@ import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; @@ -15,6 +17,8 @@ import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
15 import 'package:flutter/material.dart'; 17 import 'package:flutter/material.dart';
16 import 'package:get/get.dart'; 18 import 'package:get/get.dart';
17 19
  20 +import '../widgets/refund_explanation_bottom_sheet.dart';
  21 +
18 class PurchasePlan { 22 class PurchasePlan {
19 const PurchasePlan({ 23 const PurchasePlan({
20 required this.title, 24 required this.title,
@@ -54,19 +58,29 @@ class PurchaseController extends GetxController { @@ -54,19 +58,29 @@ class PurchaseController extends GetxController {
54 final isRestoring = false.obs; 58 final isRestoring = false.obs;
55 59
56 bool isFromOnboard = false; 60 bool isFromOnboard = false;
  61 + String channelType = '';
57 62
58 @override 63 @override
59 void onInit() { 64 void onInit() {
60 super.onInit(); 65 super.onInit();
61 final args = Get.arguments; 66 final args = Get.arguments;
62 if (args is Map) { 67 if (args is Map) {
63 - isFromOnboard = args['from'] == 'onboarding'; 68 + isFromOnboard = args[IntentKeys.keyFrom] == 'onboarding';
  69 + channelType = _nonEmpty(_stringArgument(args, IntentKeys.channelType)) ??
  70 + _nonEmpty(_stringArgument(args, 'channelType')) ??
  71 + (isFromOnboard ? '新手引导' : '');
64 } 72 }
65 scrollController.addListener(_handleScroll); 73 scrollController.addListener(_handleScroll);
66 unawaited(loadProductList()); 74 unawaited(loadProductList());
67 } 75 }
68 76
69 @override 77 @override
  78 + void onReady() {
  79 + super.onReady();
  80 + _trackEnterVipCenter();
  81 + }
  82 +
  83 + @override
70 void onClose() { 84 void onClose() {
71 planScrollController.dispose(); 85 planScrollController.dispose();
72 scrollController 86 scrollController
@@ -108,6 +122,7 @@ class PurchaseController extends GetxController { @@ -108,6 +122,7 @@ class PurchaseController extends GetxController {
108 Future<void> restorePurchase() async { 122 Future<void> restorePurchase() async {
109 if (isRestoring.value || isUnlocking.value) return; 123 if (isRestoring.value || isUnlocking.value) return;
110 124
  125 + _trackClickVipCenter('恢复购买');
111 isRestoring.value = true; 126 isRestoring.value = true;
112 try { 127 try {
113 final success = await _performRestore(); 128 final success = await _performRestore();
@@ -122,6 +137,7 @@ class PurchaseController extends GetxController { @@ -122,6 +137,7 @@ class PurchaseController extends GetxController {
122 Future<void> unlock() async { 137 Future<void> unlock() async {
123 if (isUnlocking.value || isRestoring.value) return; 138 if (isUnlocking.value || isRestoring.value) return;
124 139
  140 + _trackClickVipCenter('立即购买');
125 final plan = _selectedPlan; 141 final plan = _selectedPlan;
126 final productId = plan?.product?.id; 142 final productId = plan?.product?.id;
127 final appleProductId = plan?.appleProductId; 143 final appleProductId = plan?.appleProductId;
@@ -228,6 +244,7 @@ class PurchaseController extends GetxController { @@ -228,6 +244,7 @@ class PurchaseController extends GetxController {
228 AppleProductPaymentResult? result, 244 AppleProductPaymentResult? result,
229 ) async { 245 ) async {
230 if (result?.success == true) { 246 if (result?.success == true) {
  247 + _trackSuccessPayOrder();
231 await _completeSuccessfulPurchase(); 248 await _completeSuccessfulPurchase();
232 return; 249 return;
233 } 250 }
@@ -349,7 +366,18 @@ class PurchaseController extends GetxController { @@ -349,7 +366,18 @@ class PurchaseController extends GetxController {
349 return trimmed; 366 return trimmed;
350 } 367 }
351 368
  369 + String? _stringArgument(Map args, String key) {
  370 + final value = args[key];
  371 + return value is String ? value : null;
  372 + }
  373 +
  374 + void openRefundExplanation() {
  375 + _trackClickVipCenter('了解更多');
  376 + showRefundExplanationBottomSheet();
  377 + }
  378 +
352 void openContactUs() { 379 void openContactUs() {
  380 + _trackClickVipCenter('联系我们');
353 Get.toNamed(Routes.SUBMIT_FEEDBACK); 381 Get.toNamed(Routes.SUBMIT_FEEDBACK);
354 } 382 }
355 383
@@ -366,4 +394,36 @@ class PurchaseController extends GetxController { @@ -366,4 +394,36 @@ class PurchaseController extends GetxController {
366 parameters: {'url': AppConst.privacyPolicy}, 394 parameters: {'url': AppConst.privacyPolicy},
367 ); 395 );
368 } 396 }
  397 +
  398 + Map<String, dynamic> get _baseAnalyticsProperties =>
  399 + {'channel_type': channelType};
  400 +
  401 + void _trackEnterVipCenter() {
  402 + ta.track(
  403 + 'enter_doublefeel_vip_center',
  404 + properties: _baseAnalyticsProperties,
  405 + );
  406 + }
  407 +
  408 + void _trackClickVipCenter(String clickContent) {
  409 + ta.track(
  410 + 'click_doublefeel_vip_center',
  411 + properties: {
  412 + ..._baseAnalyticsProperties,
  413 + 'click_content': clickContent,
  414 + },
  415 + );
  416 + }
  417 +
  418 + void _trackSuccessPayOrder() {
  419 + final plan = _selectedPlan;
  420 + ta.track(
  421 + 'success_doublefeel_pay_order',
  422 + properties: {
  423 + ..._baseAnalyticsProperties,
  424 + 'product_type': plan?.product?.appleId,
  425 + 'price': plan?.product?.price,
  426 + },
  427 + );
  428 + }
369 } 429 }
@@ -10,7 +10,6 @@ import '../purchase_route_args.dart'; @@ -10,7 +10,6 @@ import '../purchase_route_args.dart';
10 import '../widgets/purchase_bottom_bar.dart'; 10 import '../widgets/purchase_bottom_bar.dart';
11 import '../widgets/purchase_colors.dart'; 11 import '../widgets/purchase_colors.dart';
12 import '../widgets/purchase_plan_card.dart'; 12 import '../widgets/purchase_plan_card.dart';
13 -import '../widgets/refund_explanation_bottom_sheet.dart';  
14 13
15 class PurchaseView extends GetView<PurchaseController> { 14 class PurchaseView extends GetView<PurchaseController> {
16 const PurchaseView({super.key}); 15 const PurchaseView({super.key});
@@ -18,79 +17,79 @@ class PurchaseView extends GetView<PurchaseController> { @@ -18,79 +17,79 @@ class PurchaseView extends GetView<PurchaseController> {
18 @override 17 @override
19 Widget build(BuildContext context) { 18 Widget build(BuildContext context) {
20 return PopScope( 19 return PopScope(
21 - canPop: !controller.isFromOnboard,  
22 - onPopInvokedWithResult: (didPop, _) {  
23 - if (!didPop) controller.onBackPressed();  
24 - },  
25 - child: AnnotatedRegion<SystemUiOverlayStyle>(  
26 - value: const SystemUiOverlayStyle(  
27 - statusBarColor: Colors.transparent,  
28 - statusBarIconBrightness: Brightness.dark,  
29 - statusBarBrightness: Brightness.light,  
30 - systemNavigationBarColor: PurchaseColors.background,  
31 - systemNavigationBarIconBrightness: Brightness.dark,  
32 - ),  
33 - child: Scaffold(  
34 - backgroundColor: PurchaseColors.background,  
35 - body: Stack(  
36 - children: [  
37 - Positioned.fill(  
38 - child: SingleChildScrollView(  
39 - controller: controller.scrollController,  
40 - physics: const ClampingScrollPhysics(),  
41 - padding: const EdgeInsets.only(bottom: 170),  
42 - child: Column(  
43 - crossAxisAlignment: CrossAxisAlignment.start,  
44 - children: [  
45 - const _HeroHeader(),  
46 - Padding(  
47 - padding: const EdgeInsets.only(left: 18, top: 9),  
48 - child: Text(  
49 - context.l10n.purchaseHeroTitle,  
50 - style: const TextStyle(  
51 - color: Colors.black,  
52 - fontSize: 24,  
53 - fontWeight: FontWeight.w600,  
54 - height: 1.2, 20 + canPop: !controller.isFromOnboard,
  21 + onPopInvokedWithResult: (didPop, _) {
  22 + if (!didPop) controller.onBackPressed();
  23 + },
  24 + child: AnnotatedRegion<SystemUiOverlayStyle>(
  25 + value: const SystemUiOverlayStyle(
  26 + statusBarColor: Colors.transparent,
  27 + statusBarIconBrightness: Brightness.dark,
  28 + statusBarBrightness: Brightness.light,
  29 + systemNavigationBarColor: PurchaseColors.background,
  30 + systemNavigationBarIconBrightness: Brightness.dark,
  31 + ),
  32 + child: Scaffold(
  33 + backgroundColor: PurchaseColors.background,
  34 + body: Stack(
  35 + children: [
  36 + Positioned.fill(
  37 + child: SingleChildScrollView(
  38 + controller: controller.scrollController,
  39 + physics: const ClampingScrollPhysics(),
  40 + padding: const EdgeInsets.only(bottom: 170),
  41 + child: Column(
  42 + crossAxisAlignment: CrossAxisAlignment.start,
  43 + children: [
  44 + const _HeroHeader(),
  45 + Padding(
  46 + padding: const EdgeInsets.only(left: 18, top: 9),
  47 + child: Text(
  48 + context.l10n.purchaseHeroTitle,
  49 + style: const TextStyle(
  50 + color: Colors.black,
  51 + fontSize: 24,
  52 + fontWeight: FontWeight.w600,
  53 + height: 1.2,
  54 + ),
  55 + ),
55 ), 56 ),
56 - ), 57 + const SizedBox(height: 26),
  58 + _PlanScroller(controller: controller),
  59 + const SizedBox(height: 22),
  60 + _SectionTitle(context.l10n.purchaseBenefitsTitle),
  61 + const SizedBox(height: 14),
  62 + const PremiumBenefitListView(),
  63 + const SizedBox(height: 24),
  64 + _LegalNotes(controller: controller),
  65 + ],
57 ), 66 ),
58 - const SizedBox(height: 26),  
59 - _PlanScroller(controller: controller),  
60 - const SizedBox(height: 22),  
61 - _SectionTitle(context.l10n.purchaseBenefitsTitle),  
62 - const SizedBox(height: 14),  
63 - const PremiumBenefitListView(),  
64 - const SizedBox(height: 24),  
65 - _LegalNotes(controller: controller),  
66 - ], 67 + ),
67 ), 68 ),
68 - ),  
69 - ),  
70 - Positioned(  
71 - left: 0,  
72 - right: 0,  
73 - top: 0,  
74 - child: _PurchaseNavigationBar(controller: controller),  
75 - ),  
76 - Positioned(  
77 - left: 0,  
78 - right: 0,  
79 - bottom: 0,  
80 - child: Obx(  
81 - () => PurchaseBottomBar(  
82 - label: context.l10n.purchaseUnlockNow,  
83 - loading: controller.isUnlocking.value,  
84 - onPressed: controller.unlock,  
85 - onTermsTap: controller.openUserTerms,  
86 - onPrivacyTap: controller.openPrivacyPolicy, 69 + Positioned(
  70 + left: 0,
  71 + right: 0,
  72 + top: 0,
  73 + child: _PurchaseNavigationBar(controller: controller),
87 ), 74 ),
88 - ), 75 + Positioned(
  76 + left: 0,
  77 + right: 0,
  78 + bottom: 0,
  79 + child: Obx(
  80 + () => PurchaseBottomBar(
  81 + label: context.l10n.purchaseUnlockNow,
  82 + loading: controller.isUnlocking.value,
  83 + onPressed: controller.unlock,
  84 + onTermsTap: controller.openUserTerms,
  85 + onPrivacyTap: controller.openPrivacyPolicy,
  86 + ),
  87 + ),
  88 + ),
  89 + ],
89 ), 90 ),
90 - ],  
91 - ),  
92 - ),  
93 - )); 91 + ),
  92 + ));
94 } 93 }
95 } 94 }
96 95
@@ -256,7 +255,7 @@ class _LegalNotes extends StatelessWidget { @@ -256,7 +255,7 @@ class _LegalNotes extends StatelessWidget {
256 text: context.l10n.purchaseNoteSubscription, 255 text: context.l10n.purchaseNoteSubscription,
257 linkText: '了解更多', 256 linkText: '了解更多',
258 linkSuffix: '。', 257 linkSuffix: '。',
259 - onLinkTap: showRefundExplanationBottomSheet, 258 + onLinkTap: controller.openRefundExplanation,
260 ), 259 ),
261 const SizedBox(height: 12), 260 const SizedBox(height: 12),
262 _LegalText( 261 _LegalText(
@@ -26,7 +26,7 @@ class SleepReportView extends StatelessWidget { @@ -26,7 +26,7 @@ class SleepReportView extends StatelessWidget {
26 26
27 final SleepReportLogic logic; 27 final SleepReportLogic logic;
28 final bool isVip; 28 final bool isVip;
29 - final VoidCallback onSubscribe; 29 + final ValueChanged<String> onSubscribe;
30 30
31 @override 31 @override
32 Widget build(BuildContext context) { 32 Widget build(BuildContext context) {
@@ -48,7 +48,7 @@ class SleepReportView extends StatelessWidget { @@ -48,7 +48,7 @@ class SleepReportView extends StatelessWidget {
48 lockedPeriods: isVip 48 lockedPeriods: isVip
49 ? const {} 49 ? const {}
50 : const {ReportPeriod.week, ReportPeriod.month}, 50 : const {ReportPeriod.week, ReportPeriod.month},
51 - onLockedPeriodTap: (_) => onSubscribe(), 51 + onLockedPeriodTap: (_) => onSubscribe('非会员切换月周-睡眠报告'),
52 ), 52 ),
53 ), 53 ),
54 const SizedBox(height: 8), 54 const SizedBox(height: 8),
1 abstract final class IntentKeys { 1 abstract final class IntentKeys {
2 static const String keyFrom = "from"; 2 static const String keyFrom = "from";
  3 + static const String channelType = "channel_type";
3 } 4 }