Commit 2bf8d2c5795d801125e01f1d96f4ffd67b716efe

Authored by 常守达
1 parent 490cf3cd

feat(today): 资源图替换

Showing 75 changed files with 2346 additions and 924 deletions

Too many changes to show.

To preserve performance only 75 of 75+ files are displayed.

7.59 KB | W: | H:

5.1 KB | W: | H:

  • 2-up
  • Swipe
  • Onion skin
This diff could not be displayed because it is too large.
@@ -83,7 +83,7 @@ EXTERNAL SOURCES: @@ -83,7 +83,7 @@ EXTERNAL SOURCES:
83 :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" 83 :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin"
84 84
85 SPEC CHECKSUMS: 85 SPEC CHECKSUMS:
86 - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 86 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
87 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 87 fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
88 image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537 88 image_cropper: c4326ea50132b1e1564499e5d32a84f01fb03537
89 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a 89 image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
  1 +import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
1 import 'package:doublefeel_flutter/app/modules/user_onboarding/widget/guide_common_scaffold.dart'; 2 import 'package:doublefeel_flutter/app/modules/user_onboarding/widget/guide_common_scaffold.dart';
2 import 'package:doublefeel_flutter/app/modules/user_onboarding/widget/onboarding_common_widgets.dart'; 3 import 'package:doublefeel_flutter/app/modules/user_onboarding/widget/onboarding_common_widgets.dart';
3 import 'package:doublefeel_flutter/app/routes/app_pages.dart'; 4 import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  5 +import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
  6 +import 'package:doublefeel_flutter/core/error/app_error.dart';
  7 +import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
4 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 8 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
5 import 'package:doublefeel_flutter/core/result/app_result.dart'; 9 import 'package:doublefeel_flutter/core/result/app_result.dart';
6 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 10 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
7 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 11 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  12 +import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
8 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 13 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
9 import 'package:flutter/material.dart'; 14 import 'package:flutter/material.dart';
10 import 'package:get/get.dart'; 15 import 'package:get/get.dart';
  16 +import 'package:lottie/lottie.dart';
11 import 'package:share_plus/share_plus.dart'; 17 import 'package:share_plus/share_plus.dart';
12 18
13 class BindPartnerController extends GetxController { 19 class BindPartnerController extends GetxController {
@@ -23,9 +29,21 @@ class BindPartnerController extends GetxController { @@ -23,9 +29,21 @@ class BindPartnerController extends GetxController {
23 29
24 final RxString myInviteCode = ''.obs; 30 final RxString myInviteCode = ''.obs;
25 31
  32 + late bool isShowAppBarBack;
  33 + late bool isShowSkipButton;
  34 +
26 @override 35 @override
27 void onInit() { 36 void onInit() {
28 super.onInit(); 37 super.onInit();
  38 + final from = Get.arguments?['from'] ?? '';
  39 + if (from == 'onboarding') {
  40 + isShowAppBarBack = false;
  41 + isShowSkipButton = true;
  42 + } else {
  43 + isShowAppBarBack = true;
  44 + isShowSkipButton = false;
  45 + }
  46 +
29 partnerIdController.addListener(() { 47 partnerIdController.addListener(() {
30 partnerIdInput.value = partnerIdController.text.trim(); 48 partnerIdInput.value = partnerIdController.text.trim();
31 }); 49 });
@@ -54,11 +72,84 @@ class BindPartnerController extends GetxController { @@ -54,11 +72,84 @@ class BindPartnerController extends GetxController {
54 if (!canSubmit || isSubmitting.value) return; 72 if (!canSubmit || isSubmitting.value) return;
55 isSubmitting.value = true; 73 isSubmitting.value = true;
56 try { 74 try {
57 - final result = await _friendApi.addFriend(partnerIdInput.value);  
58 - if (result is AppFailure) {  
59 - return; 75 + final result = await _friendApi.addFriendHandleError(
  76 + partnerIdInput.value,
  77 + HttpErrorHandlingPolicy(
  78 + excludeErrorCodeList: {-1011201, -1011202, -1011203}));
  79 +
  80 + switch (result) {
  81 + case AppFailure(error: final err):
  82 + if (err is AppHttpError) {
  83 + switch (err.businessCode) {
  84 + case -1011201:
  85 + DialogUtils.showCommonDialog(DialogMetaData(
  86 + title: '该ID不存在',
  87 + message: '这个ID不存在哦,请检查后重新输入',
  88 + confirmText: '我知道了',
  89 + iconAsset: null,
  90 + ));
  91 + break;
  92 + case -1011202:
  93 + DialogUtils.showCommonDialog(DialogMetaData(
  94 + title: '添加失败',
  95 + message: '该用户不允许被添加好友,无法添加Ta哦',
  96 + confirmText: '我知道了',
  97 + iconAsset: null,
  98 + ));
  99 + break;
  100 + case -1011203:
  101 + DialogUtils.showCommonDialog(DialogMetaData(
  102 + title: '对方已经是你的好友啦',
  103 + message: '请不要重复添加哦',
  104 + confirmText: '我知道了',
  105 + iconAsset: null,
  106 + ));
  107 + break;
  108 + }
  109 + }
  110 + break;
  111 + case AppSuccess(data: final friendItem):
  112 + if (isShowAppBarBack) {
  113 + Get.back();
  114 + } else {
  115 + Get.to(_BindSuccessPage(friendItem));
  116 + }
  117 + break;
60 } 118 }
61 - Get.to(_BindSuccessPage()); 119 + // if (result is AppFailure) {
  120 + // if (result.error is AppHttpError) {
  121 + // var error = result.error as AppHttpError;
  122 +
  123 + // switch (error.businessCode) {
  124 + // case -1011201:
  125 + // DialogUtils.showCommonDialog(DialogMetaData(
  126 + // title: '该ID不存在',
  127 + // message: '这个ID不存在哦,请检查后重新输入',
  128 + // confirmText: '我知道了',
  129 + // iconAsset: null,
  130 + // ));
  131 + // break;
  132 + // case -1011202:
  133 + // DialogUtils.showCommonDialog(DialogMetaData(
  134 + // title: '添加失败',
  135 + // message: '该用户不允许被添加好友,无法添加Ta哦',
  136 + // confirmText: '我知道了',
  137 + // iconAsset: null,
  138 + // ));
  139 + // break;
  140 + // case -1011203:
  141 + // DialogUtils.showCommonDialog(DialogMetaData(
  142 + // title: '对方已经是你的好友啦',
  143 + // message: '请不要重复添加哦',
  144 + // confirmText: '我知道了',
  145 + // iconAsset: null,
  146 + // ));
  147 + // break;
  148 + // }
  149 + // }
  150 + // return;
  151 + // }
  152 + // if (result is AppSuccess) {}
62 } catch (e) { 153 } catch (e) {
63 } finally { 154 } finally {
64 isSubmitting.value = false; 155 isSubmitting.value = false;
@@ -68,18 +159,27 @@ class BindPartnerController extends GetxController { @@ -68,18 +159,27 @@ class BindPartnerController extends GetxController {
68 void onSkip() { 159 void onSkip() {
69 final userStateService = Get.find<UserStateService>(); 160 final userStateService = Get.find<UserStateService>();
70 if (!userStateService.isVip) { 161 if (!userStateService.isVip) {
71 - Get.toNamed(Routes.MEMBERSHIP_OFFER); 162 + Get.toNamed(
  163 + Routes.MEMBERSHIP_OFFER,
  164 + arguments: {'from': 'onboarding'},
  165 + );
72 } else { 166 } else {
73 Get.offAllNamed(AppRoutes.home); 167 Get.offAllNamed(AppRoutes.home);
74 } 168 }
  169 + // Get.to(_BindSuccessPage(FriendItem(
  170 + // avatar: 'https://cdn.couple360.net/couple360/themes/im_theme7.png')));
75 } 171 }
76 } 172 }
77 173
78 class _BindSuccessPage extends StatelessWidget { 174 class _BindSuccessPage extends StatelessWidget {
79 - const _BindSuccessPage(); 175 + const _BindSuccessPage(this.friendItem);
  176 + final FriendItem friendItem;
80 @override 177 @override
81 Widget build(BuildContext context) { 178 Widget build(BuildContext context) {
82 final l10n = context.l10n; 179 final l10n = context.l10n;
  180 + final userPrefs = Get.find<UserPreferencesStorage>();
  181 + final preferences = userPrefs.preferences.value;
  182 + final me = preferences.meUserInfo;
83 return GuideCommonScaffold( 183 return GuideCommonScaffold(
84 onBackPressed: () => Get.back(), 184 onBackPressed: () => Get.back(),
85 bottom: OnboardingBottomButton( 185 bottom: OnboardingBottomButton(
@@ -88,7 +188,10 @@ class _BindSuccessPage extends StatelessWidget { @@ -88,7 +188,10 @@ class _BindSuccessPage extends StatelessWidget {
88 onPressed: () { 188 onPressed: () {
89 final userStateService = Get.find<UserStateService>(); 189 final userStateService = Get.find<UserStateService>();
90 if (!userStateService.isVip) { 190 if (!userStateService.isVip) {
91 - Get.toNamed(Routes.MEMBERSHIP_OFFER); 191 + Get.toNamed(
  192 + Routes.MEMBERSHIP_OFFER,
  193 + arguments: {'from': 'onboarding'},
  194 + );
92 } else { 195 } else {
93 Get.offAllNamed(AppRoutes.home); 196 Get.offAllNamed(AppRoutes.home);
94 } 197 }
@@ -97,33 +200,82 @@ class _BindSuccessPage extends StatelessWidget { @@ -97,33 +200,82 @@ class _BindSuccessPage extends StatelessWidget {
97 child: OnboardingPageScrollBody( 200 child: OnboardingPageScrollBody(
98 horizontalPadding: 0, 201 horizontalPadding: 0,
99 topPadding: 0, 202 topPadding: 0,
100 - child: Column(  
101 - mainAxisAlignment: MainAxisAlignment.start,  
102 - mainAxisSize: MainAxisSize.min, 203 + child: Stack(
103 children: [ 204 children: [
104 - Container(  
105 - height: 509,  
106 - decoration:  
107 - BoxDecoration(color: const Color(0xFFFF0000).withAlpha(120)),  
108 - ),  
109 - const SizedBox(height: 16),  
110 - OnboardingTitleText(  
111 - context.l10n.healthCompanionIsNowAvailable,  
112 - maxWidth: 310,  
113 - ),  
114 - const SizedBox(height: 8),  
115 - Padding(  
116 - padding: const EdgeInsets.symmetric(horizontal: 16),  
117 - child: Text(  
118 - l10n.youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired,  
119 - textAlign: TextAlign.center,  
120 - style: TextStyle(  
121 - color: Colors.black,  
122 - fontSize: 12,  
123 - fontWeight: FontWeight.w400, 205 + Lottie.asset('assets/lottie/scatter_flowers.json'),
  206 + Column(
  207 + mainAxisAlignment: MainAxisAlignment.start,
  208 + mainAxisSize: MainAxisSize.min,
  209 + children: [
  210 + SizedBox(
  211 + height: 509,
  212 + child: TweenAnimationBuilder<double>(
  213 + tween: Tween(begin: 160.0, end: 46.0),
  214 + duration: const Duration(milliseconds: 800),
  215 + curve: Curves.easeOut,
  216 + builder: (context, offset, _) {
  217 + final avatar = Container(
  218 + width: 100,
  219 + height: 100,
  220 + decoration: ShapeDecoration(
  221 + image: DecorationImage(
  222 + image: NetworkImage(me?.avatar ?? ''),
  223 + fit: BoxFit.cover,
  224 + ),
  225 + shape: OvalBorder(
  226 + side: BorderSide(width: 1.79, color: Colors.white),
  227 + ),
  228 + ),
  229 + );
  230 + final friendAvatar = Container(
  231 + width: 100,
  232 + height: 100,
  233 + decoration: ShapeDecoration(
  234 + image: DecorationImage(
  235 + image: NetworkImage(friendItem.avatar ?? ''),
  236 + fit: BoxFit.cover,
  237 + ),
  238 + shape: OvalBorder(
  239 + side: BorderSide(width: 1.79, color: Colors.white),
  240 + ),
  241 + ),
  242 + );
  243 + return Stack(
  244 + alignment: Alignment(0.5, 0.5),
  245 + children: [
  246 + Transform.translate(
  247 + offset: Offset(offset, 0),
  248 + child: friendAvatar,
  249 + ),
  250 + Transform.translate(
  251 + offset: Offset(-offset, 0),
  252 + child: avatar,
  253 + ),
  254 + ],
  255 + );
  256 + },
  257 + ),
  258 + ),
  259 + const SizedBox(height: 16),
  260 + OnboardingTitleText(
  261 + context.l10n.healthCompanionIsNowAvailable,
  262 + maxWidth: 310,
124 ), 263 ),
125 - ),  
126 - ) 264 + const SizedBox(height: 8),
  265 + Padding(
  266 + padding: const EdgeInsets.symmetric(horizontal: 16),
  267 + child: Text(
  268 + l10n.youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired,
  269 + textAlign: TextAlign.center,
  270 + style: TextStyle(
  271 + color: Colors.black,
  272 + fontSize: 12,
  273 + fontWeight: FontWeight.w400,
  274 + ),
  275 + ),
  276 + )
  277 + ],
  278 + ),
127 ], 279 ],
128 ), 280 ),
129 ), 281 ),
1 -import 'package:doublefeel_flutter/app/modules/user_onboarding/widget/onboarding_common_widgets.dart';  
2 import 'package:doublefeel_flutter/app/widget/dash_divider.dart'; 1 import 'package:doublefeel_flutter/app/widget/dash_divider.dart';
3 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 2 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
4 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 3 import 'package:doublefeel_flutter/core/util/app_toast.dart';
@@ -18,6 +17,21 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -18,6 +17,21 @@ class BindPartnerView extends GetView<BindPartnerController> {
18 return Scaffold( 17 return Scaffold(
19 backgroundColor: Colors.white, 18 backgroundColor: Colors.white,
20 resizeToAvoidBottomInset: true, 19 resizeToAvoidBottomInset: true,
  20 + extendBodyBehindAppBar: true,
  21 + appBar: AppBar(
  22 + automaticallyImplyLeading: false,
  23 + leading: controller.isShowAppBarBack
  24 + ? IconButton(
  25 + highlightColor: Colors.transparent,
  26 + padding: EdgeInsets.zero,
  27 + onPressed: Get.back,
  28 + icon: Image.asset(
  29 + 'assets/images/common/ic_nav_back.webp',
  30 + width: 24,
  31 + height: 24,
  32 + ),
  33 + )
  34 + : SizedBox()),
21 body: PopScope( 35 body: PopScope(
22 canPop: false, 36 canPop: false,
23 child: Stack( 37 child: Stack(
@@ -33,32 +47,28 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -33,32 +47,28 @@ class BindPartnerView extends GetView<BindPartnerController> {
33 SizedBox(height: 28), 47 SizedBox(height: 28),
34 _buildCardArea(context), 48 _buildCardArea(context),
35 SizedBox(height: 58), 49 SizedBox(height: 58),
36 - _buildSkipButton(context), 50 + if (controller.isShowSkipButton) _buildSkipButton(context),
37 ], 51 ],
38 ), 52 ),
39 ), 53 ),
40 Positioned( 54 Positioned(
41 top: MediaQuery.paddingOf(context).top + 92, 55 top: MediaQuery.paddingOf(context).top + 92,
42 left: 0, 56 left: 0,
43 - child: Opacity(  
44 - opacity: 0.40,  
45 - child: Container(  
46 - width: 133,  
47 - height: 124,  
48 - decoration: BoxDecoration(color: const Color(0xFFFF0000)),  
49 - ), 57 + child: Image.asset(
  58 + 'assets/images/user_onboarding/ic_bind_decor_top.png',
  59 + width: 133,
  60 + height: 124,
  61 + fit: BoxFit.fitHeight,
50 ), 62 ),
51 ), 63 ),
52 Positioned( 64 Positioned(
53 top: MediaQuery.paddingOf(context).top + 532, 65 top: MediaQuery.paddingOf(context).top + 532,
54 right: 0, 66 right: 0,
55 - child: Opacity(  
56 - opacity: 0.40,  
57 - child: Container(  
58 - width: 133,  
59 - height: 124,  
60 - decoration: BoxDecoration(color: const Color(0xFFFF0000)),  
61 - ), 67 + child: Image.asset(
  68 + 'assets/images/user_onboarding/ic_bind_decor_bottom.png',
  69 + width: 133,
  70 + height: 124,
  71 + fit: BoxFit.fitHeight,
62 ), 72 ),
63 ), 73 ),
64 ], 74 ],
@@ -109,8 +119,12 @@ class BindPartnerView extends GetView<BindPartnerController> { @@ -109,8 +119,12 @@ class BindPartnerView extends GetView<BindPartnerController> {
109 return Container( 119 return Container(
110 margin: EdgeInsets.symmetric(horizontal: 24), 120 margin: EdgeInsets.symmetric(horizontal: 24),
111 decoration: BoxDecoration( 121 decoration: BoxDecoration(
112 - color: Colors.white,  
113 borderRadius: BorderRadius.circular(20), 122 borderRadius: BorderRadius.circular(20),
  123 + image: DecorationImage(
  124 + image:
  125 + AssetImage('assets/images/friends/bg_friend_add_subtract.webp'),
  126 + fit: BoxFit.cover,
  127 + ),
114 boxShadow: [ 128 boxShadow: [
115 BoxShadow( 129 BoxShadow(
116 color: context.colors.primary.withValues(alpha: 0.08), 130 color: context.colors.primary.withValues(alpha: 0.08),
@@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; @@ -3,6 +3,7 @@ import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
3 import 'package:doublefeel_flutter/core/network/api/user_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/util/app_toast.dart'; 5 import 'package:doublefeel_flutter/core/util/app_toast.dart';
  6 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
6 import 'package:flutter/material.dart'; 7 import 'package:flutter/material.dart';
7 import 'package:get/get.dart'; 8 import 'package:get/get.dart';
8 import 'package:image_picker/image_picker.dart'; 9 import 'package:image_picker/image_picker.dart';
@@ -21,6 +22,8 @@ class SubmitFeedbackController extends GetxController { @@ -21,6 +22,8 @@ class SubmitFeedbackController extends GetxController {
21 22
22 final ImagePicker _picker = ImagePicker(); 23 final ImagePicker _picker = ImagePicker();
23 24
  25 + final PlatformHostApi _platformHostApi = PlatformHostApi();
  26 +
24 @override 27 @override
25 void onInit() { 28 void onInit() {
26 super.onInit(); 29 super.onInit();
@@ -70,27 +73,48 @@ class SubmitFeedbackController extends GetxController { @@ -70,27 +73,48 @@ class SubmitFeedbackController extends GetxController {
70 AppToast.show('邮箱格式错误,请重新输入'); 73 AppToast.show('邮箱格式错误,请重新输入');
71 return; 74 return;
72 } 75 }
73 -  
74 - final result = await _userApi.submitFeedback(  
75 - content: feedbackTextController.text.trim(),  
76 - email: contactTextController.text.trim(),  
77 - // images: selectedImages.map((e) => e.path).toList(),  
78 - images: selectedImages  
79 - .map(  
80 - (e) => 'https://cdn.couple360.net/couple360/themes/im_theme7.png')  
81 - .toList(), 76 + Get.dialog(
  77 + const PopScope(
  78 + canPop: false,
  79 + child: Center(child: CircularProgressIndicator()),
  80 + ),
  81 + barrierDismissible: false,
82 ); 82 );
83 - switch (result) {  
84 - case AppSuccess():  
85 - await DialogUtils.showCommonDialog(DialogMetaData(  
86 - iconAsset: null,  
87 - title: '反馈提交成功',  
88 - message: '谢谢您的反馈。如需进一步沟通,我们会尽快通过您留下的邮箱地址与您联系,请留意查收邮件。',  
89 - confirmText: '好的',  
90 - ));  
91 - Get.back();  
92 - case AppFailure(:final error):  
93 - AppToast.show(error.displayMessage); 83 + try {
  84 + final selectedPaths = <String>[];
  85 + for (var i = 0; i < selectedImages.length; i++) {
  86 + final image = selectedImages[i];
  87 + image.mimeType;
  88 + final ossPath = await _platformHostApi.uploadFile(
  89 + image.path,
  90 + _isVideo(image) ? HResourceType.video : HResourceType.image,
  91 + );
  92 + if (ossPath != null && ossPath.isNotEmpty) {
  93 + selectedPaths.add(ossPath);
  94 + }
  95 + }
  96 +
  97 + final result = await _userApi.submitFeedback(
  98 + content: feedbackTextController.text.trim(),
  99 + email: contactTextController.text.trim(),
  100 + // images: selectedImages.map((e) => e.path).toList(),
  101 + images: selectedPaths,
  102 + );
  103 + Get.back();
  104 + switch (result) {
  105 + case AppSuccess():
  106 + await DialogUtils.showCommonDialog(DialogMetaData(
  107 + iconAsset: null,
  108 + title: '反馈提交成功',
  109 + message: '谢谢您的反馈。如需进一步沟通,我们会尽快通过您留下的邮箱地址与您联系,请留意查收邮件。',
  110 + confirmText: '好的',
  111 + ));
  112 + Get.back();
  113 + case AppFailure(:final error):
  114 + AppToast.show(error.displayMessage);
  115 + }
  116 + } catch (_) {
  117 + Get.back();
94 } 118 }
95 } 119 }
96 120
@@ -100,4 +124,20 @@ class SubmitFeedbackController extends GetxController { @@ -100,4 +124,20 @@ class SubmitFeedbackController extends GetxController {
100 contactTextController.dispose(); 124 contactTextController.dispose();
101 super.onClose(); 125 super.onClose();
102 } 126 }
  127 +
  128 + bool _isVideo(XFile file) {
  129 + const videoExtensions = {
  130 + 'mp4',
  131 + 'mov',
  132 + 'avi',
  133 + 'mkv',
  134 + 'wmv',
  135 + 'flv',
  136 + 'webm',
  137 + 'm4v',
  138 + '3gp'
  139 + };
  140 + final ext = file.path.split('.').last.toLowerCase();
  141 + return videoExtensions.contains(ext);
  142 + }
103 } 143 }
1 import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart'; 1 import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
2 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 2 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
3 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 3 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
  4 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
4 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 5 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
5 import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; 6 import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart';
6 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 7 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
@@ -14,6 +15,7 @@ class FriendHomeBinding extends Bindings { @@ -14,6 +15,7 @@ class FriendHomeBinding extends Bindings {
14 final args = Get.arguments as FriendItem; 15 final args = Get.arguments as FriendItem;
15 Get.lazyPut<TodayController>( 16 Get.lazyPut<TodayController>(
16 () => TodayController( 17 () => TodayController(
  18 + Get.find<PayApi>(),
17 Get.find<VipApi>(), 19 Get.find<VipApi>(),
18 Get.find<HealthApi>(), 20 Get.find<HealthApi>(),
19 Get.find<FriendApi>(), 21 Get.find<FriendApi>(),
@@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_t @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/app/apple_health_upload/apple_health_upload_t
2 import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; 2 import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
3 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 3 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
4 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 4 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
  5 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
5 import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; 6 import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
6 import 'package:doublefeel_flutter/core/network/api/user_api.dart'; 7 import 'package:doublefeel_flutter/core/network/api/user_api.dart';
7 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 8 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
@@ -20,6 +21,7 @@ class HomeBinding extends Bindings { @@ -20,6 +21,7 @@ class HomeBinding extends Bindings {
20 Get.lazyPut<HomeController>(() => HomeController(), fenix: true); 21 Get.lazyPut<HomeController>(() => HomeController(), fenix: true);
21 Get.lazyPut<TodayController>( 22 Get.lazyPut<TodayController>(
22 () => TodayController( 23 () => TodayController(
  24 + Get.find<PayApi>(),
23 Get.find<VipApi>(), 25 Get.find<VipApi>(),
24 Get.find<HealthApi>(), 26 Get.find<HealthApi>(),
25 Get.find<FriendApi>(), 27 Get.find<FriendApi>(),
@@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart'; @@ -9,6 +9,7 @@ import 'package:doublefeel_flutter/core/result/app_result.dart';
9 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 9 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
10 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 10 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
11 import 'package:doublefeel_flutter/data/models/user/user_models.dart'; 11 import 'package:doublefeel_flutter/data/models/user/user_models.dart';
  12 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
12 import 'package:get/get.dart'; 13 import 'package:get/get.dart';
13 import 'package:image_cropper/image_cropper.dart'; 14 import 'package:image_cropper/image_cropper.dart';
14 import 'package:image_picker/image_picker.dart'; 15 import 'package:image_picker/image_picker.dart';
@@ -22,6 +23,7 @@ class MyController extends GetxController { @@ -22,6 +23,7 @@ class MyController extends GetxController {
22 final ImagePicker _picker = ImagePicker(); 23 final ImagePicker _picker = ImagePicker();
23 final watchThemeItems = <WatchThemeItem>[].obs; 24 final watchThemeItems = <WatchThemeItem>[].obs;
24 final isLoadingWatchThemes = false.obs; 25 final isLoadingWatchThemes = false.obs;
  26 + final PlatformHostApi _platformHostApi = PlatformHostApi();
25 27
26 List<WatchThemeItem> get officialWatchThemes => 28 List<WatchThemeItem> get officialWatchThemes =>
27 watchThemeItems.where((theme) => theme.isOfficialTheme).toList(); 29 watchThemeItems.where((theme) => theme.isOfficialTheme).toList();
@@ -77,9 +79,11 @@ class MyController extends GetxController { @@ -77,9 +79,11 @@ class MyController extends GetxController {
77 ); 79 );
78 if (cropped == null) return; // 用户取消裁剪 80 if (cropped == null) return; // 用户取消裁剪
79 81
80 - // 目前用占位 URL,替换为真实上传逻辑  
81 - const obsUrl = 'https://cdn.couple360.net/couple360/themes/im_theme7.png';  
82 - final updateResult = await _userApi.updateOwnerUserInfo(avatar: obsUrl); 82 + final ossPath = await _platformHostApi.uploadFile(
  83 + cropped.path,
  84 + HResourceType.image,
  85 + );
  86 + final updateResult = await _userApi.updateOwnerUserInfo(avatar: ossPath);
83 if (updateResult is AppSuccess<UserInfoResponse>) { 87 if (updateResult is AppSuccess<UserInfoResponse>) {
84 await Get.find<UserPreferencesStorage>() 88 await Get.find<UserPreferencesStorage>()
85 .updateMeUserInfo(updateResult.data); 89 .updateMeUserInfo(updateResult.data);
@@ -11,16 +11,21 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart'; @@ -11,16 +11,21 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart';
11 import 'package:doublefeel_flutter/core/logging/app_logger.dart'; 11 import 'package:doublefeel_flutter/core/logging/app_logger.dart';
12 import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; 12 import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
13 import 'package:doublefeel_flutter/core/network/api/health_api.dart'; 13 import 'package:doublefeel_flutter/core/network/api/health_api.dart';
  14 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
14 import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; 15 import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
15 import 'package:doublefeel_flutter/core/result/app_result.dart'; 16 import 'package:doublefeel_flutter/core/result/app_result.dart';
16 import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart'; 17 import 'package:doublefeel_flutter/core/services/health_kit_upload_service.dart';
17 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 18 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
18 import 'package:doublefeel_flutter/core/util/app_toast.dart'; 19 import 'package:doublefeel_flutter/core/util/app_toast.dart';
  20 +import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
19 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 21 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
20 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart'; 22 import 'package:doublefeel_flutter/data/models/friend/friend_models.dart';
21 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; 23 import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
22 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; 24 import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
  25 +import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
23 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; 26 import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
  27 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
  28 +import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
24 import 'package:flutter/material.dart'; 29 import 'package:flutter/material.dart';
25 import 'package:get/get.dart'; 30 import 'package:get/get.dart';
26 import 'package:intl/intl.dart'; 31 import 'package:intl/intl.dart';
@@ -48,8 +53,9 @@ class HrvAnnotation { @@ -48,8 +53,9 @@ class HrvAnnotation {
48 }); 53 });
49 } 54 }
50 55
51 -class TodayController extends GetxController { 56 +class TodayController extends GetMaterialController {
52 TodayController( 57 TodayController(
  58 + this._payApi,
53 this._vipApi, 59 this._vipApi,
54 this._healthApi, 60 this._healthApi,
55 this._friendApi, 61 this._friendApi,
@@ -59,6 +65,7 @@ class TodayController extends GetxController { @@ -59,6 +65,7 @@ class TodayController extends GetxController {
59 FriendItem? friendInfo, // null = 自己,非 null = 好友 65 FriendItem? friendInfo, // null = 自己,非 null = 好友
60 }) : _initialFriendInfo = friendInfo; 66 }) : _initialFriendInfo = friendInfo;
61 67
  68 + final PayApi _payApi;
62 final VipApi _vipApi; 69 final VipApi _vipApi;
63 final HealthApi _healthApi; 70 final HealthApi _healthApi;
64 final FriendApi _friendApi; 71 final FriendApi _friendApi;
@@ -84,7 +91,7 @@ class TodayController extends GetxController { @@ -84,7 +91,7 @@ class TodayController extends GetxController {
84 final scrollOffset = 0.0.obs; 91 final scrollOffset = 0.0.obs;
85 92
86 final isLoadingToday = false.obs; 93 final isLoadingToday = false.obs;
87 - final showHealthDataAuthCardStatus = (-1).obs; 94 + final showHealthDataAuthCardStatus = 1.obs;
88 final stressSubtitle = 'Hi, 你今日的综合压力状态'.obs; 95 final stressSubtitle = 'Hi, 你今日的综合压力状态'.obs;
89 96
90 // ── HRV 表盘引导 Banner ─────────────────── 97 // ── HRV 表盘引导 Banner ───────────────────
@@ -93,7 +100,14 @@ class TodayController extends GetxController { @@ -93,7 +100,14 @@ class TodayController extends GetxController {
93 100
94 void dismissHrvAdBanner() => showHrvAdBanner.value = false; 101 void dismissHrvAdBanner() => showHrvAdBanner.value = false;
95 102
96 - void dismissPartnerAdBanner() => showPartnerAdBanner.value = false; 103 + Future<void> dismissPartnerAdBanner() async {
  104 + final userId =
  105 + Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
  106 + 0;
  107 + await Get.find<UserAccountStorage>()
  108 + .saveLastAddFriendBannerShowTime(userId);
  109 + showPartnerAdBanner.value = false;
  110 + }
97 111
98 // ── HRV 数字 + 心率 ─────────────────────── 112 // ── HRV 数字 + 心率 ───────────────────────
99 final avgHrv = '--'.obs; 113 final avgHrv = '--'.obs;
@@ -106,8 +120,6 @@ class TodayController extends GetxController { @@ -106,8 +120,6 @@ class TodayController extends GetxController {
106 final activityMoveProgress = 0.0.obs; 120 final activityMoveProgress = 0.0.obs;
107 final activityExerciseProgress = 0.0.obs; 121 final activityExerciseProgress = 0.0.obs;
108 final activityStandProgress = 0.0.obs; 122 final activityStandProgress = 0.0.obs;
109 - int? _activityMoveTarget;  
110 - int? _activityStandTarget;  
111 123
112 // ── HRV 趋势图 ──────────────────────────── 124 // ── HRV 趋势图 ────────────────────────────
113 final hrvChartData = <V2HrvTrendItem>[].obs; 125 final hrvChartData = <V2HrvTrendItem>[].obs;
@@ -138,32 +150,50 @@ class TodayController extends GetxController { @@ -138,32 +150,50 @@ class TodayController extends GetxController {
138 }); 150 });
139 151
140 unawaited(loadDataForDate(today)); 152 unawaited(loadDataForDate(today));
  153 +
141 checkAddFriendVisible(); 154 checkAddFriendVisible();
142 checkHrvAdBannerVisible(); 155 checkHrvAdBannerVisible();
143 checkHealthDataAuthCardVisible(); 156 checkHealthDataAuthCardVisible();
  157 + if (!Get.find<UserStateService>().isVip) {
  158 + getProductList();
  159 + }
144 } 160 }
145 161
146 - void checkHrvAdBannerVisible() { 162 + Future<void> checkHrvAdBannerVisible() async {
147 if (isFriend) { 163 if (isFriend) {
148 showHrvAdBanner.value = false; 164 showHrvAdBanner.value = false;
149 } else { 165 } else {
150 - showHrvAdBanner.value = true; 166 + try {
  167 + bool hasInstalledWatchSurface =
  168 + await WearEngineHostApi().hasInstalledWatchSurface();
  169 + showHrvAdBanner.value = !hasInstalledWatchSurface;
  170 + } on Exception catch (e) {}
151 } 171 }
152 } 172 }
153 173
154 void checkAddFriendVisible() { 174 void checkAddFriendVisible() {
155 - if (isFriend) {  
156 - showHrvAdBanner.value = false;  
157 - showPartnerAdBanner.value = false;  
158 - } else {  
159 - _refreshFriendList(onFriendListUpdated: () {  
160 - if (friendsList.isNotEmpty || friendsList.value.length >= 10) { 175 + _refreshFriendList(onFriendListUpdated: () {
  176 + if (friendsList.isNotEmpty ||
  177 + friendsList.value.length >= friendsListLimit) {
  178 + showPartnerAdBanner.value = false;
  179 + return;
  180 + }
  181 + // 关闭后 3 天内不再显示
  182 + final userId =
  183 + Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
  184 + 0;
  185 + final lastShowTime =
  186 + Get.find<UserAccountStorage>().getLastAddFriendBannerShowTime(userId);
  187 + if (lastShowTime != null) {
  188 + final diff = DateTime.now()
  189 + .difference(DateTime.fromMillisecondsSinceEpoch(lastShowTime));
  190 + if (diff.inDays < 3) {
161 showPartnerAdBanner.value = false; 191 showPartnerAdBanner.value = false;
162 - } else {  
163 - showPartnerAdBanner.value = true; 192 + return;
164 } 193 }
165 - });  
166 - } 194 + }
  195 + showPartnerAdBanner.value = true;
  196 + });
167 } 197 }
168 198
169 void changeDate(DateTime date, {DateTime? focused}) { 199 void changeDate(DateTime date, {DateTime? focused}) {
@@ -251,8 +281,17 @@ class TodayController extends GetxController { @@ -251,8 +281,17 @@ class TodayController extends GetxController {
251 281
252 Future<void> checkHealthDataAuthCardVisible() async { 282 Future<void> checkHealthDataAuthCardVisible() async {
253 try { 283 try {
  284 + final hasUploaded =
  285 + switch (await _healthApi.getHealthDataEverUploaded()) {
  286 + AppSuccess(:final data) => data,
  287 + AppFailure() => false,
  288 + };
254 final result = await _hostApi.checkHealthAppAuthorization(); 289 final result = await _hostApi.checkHealthAppAuthorization();
255 290
  291 + if (!hasUploaded) {
  292 + showHealthDataAuthCardStatus.value = 0;
  293 + return;
  294 + }
256 showHealthDataAuthCardStatus.value = result.status; 295 showHealthDataAuthCardStatus.value = result.status;
257 // TODO:若用户从始至终没有任何数据,也显示该引导 296 // TODO:若用户从始至终没有任何数据,也显示该引导
258 if (result.status == 1) { 297 if (result.status == 1) {
@@ -276,13 +315,13 @@ class TodayController extends GetxController { @@ -276,13 +315,13 @@ class TodayController extends GetxController {
276 315
277 switch (await _healthApi.getV2StressScore(friendUserId, intDate)) { 316 switch (await _healthApi.getV2StressScore(friendUserId, intDate)) {
278 case AppSuccess(:final data): 317 case AppSuccess(:final data):
279 - // v2StressScore.value = data; 318 + v2StressScore.value = data;
280 319
281 - var nextInt = Random().nextInt(4);  
282 - v2StressScore.value = V2StressScore(  
283 - state: nextInt,  
284 - comprehensiveScore: 100 - (nextInt * 25),  
285 - ); 320 + // var nextInt = Random().nextInt(4);
  321 + // v2StressScore.value = V2StressScore(
  322 + // state: nextInt,
  323 + // comprehensiveScore: 100 - (nextInt * 25),
  324 + // );
286 case AppFailure(): 325 case AppFailure():
287 v2StressScore.value = null; 326 v2StressScore.value = null;
288 } 327 }
@@ -296,66 +335,67 @@ class TodayController extends GetxController { @@ -296,66 +335,67 @@ class TodayController extends GetxController {
296 335
297 switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) { 336 switch (await _healthApi.getV2HrvTrend(friendUserId, intDate)) {
298 case AppSuccess(:final data): 337 case AppSuccess(:final data):
299 - // hrvChartData.assignAll(data.list ?? []);  
300 - hrvChartData.assignAll([  
301 - // 每小时一条,00:00 ~ 15:00(CST 午夜 = 1782057600)  
302 - V2HrvTrendItem(time: 1782057600, trendHrv: 45, state: 3), // 00:00  
303 - V2HrvTrendItem(time: 1782061200, trendHrv: 42, state: 3), // 01:00  
304 - V2HrvTrendItem(time: 1782064800, trendHrv: 38, state: 3), // 02:00  
305 - V2HrvTrendItem(time: 1782068400, trendHrv: 35, state: 2), // 03:00  
306 - V2HrvTrendItem(time: 1782072000, trendHrv: 40, state: 3), // 04:00  
307 - V2HrvTrendItem(time: 1782075600, trendHrv: 55, state: 4), // 05:00  
308 - V2HrvTrendItem(time: 1782079200, trendHrv: 65, state: 4), // 06:00  
309 - V2HrvTrendItem(time: 1782082800, trendHrv: 72, state: 4), // 07:00  
310 - V2HrvTrendItem(time: 1782086400, trendHrv: 68, state: 4), // 08:00  
311 - V2HrvTrendItem(time: 1782090000, trendHrv: 58, state: 4), // 09:00  
312 - V2HrvTrendItem(time: 1782093600, trendHrv: 52, state: 3), // 10:00  
313 - V2HrvTrendItem(time: 1782097200, trendHrv: 48, state: 3), // 11:00  
314 - V2HrvTrendItem(time: 1782100800, trendHrv: 44, state: 3), // 12:00  
315 - V2HrvTrendItem(time: 1782104400, trendHrv: 38, state: 2), // 13:00  
316 - V2HrvTrendItem(time: 1782108000, trendHrv: 42, state: 3), // 14:00  
317 - V2HrvTrendItem(time: 1782111600, trendHrv: 45, state: 3), // 15:00  
318 - ]); 338 + hrvChartData.assignAll(data.list ?? []);
  339 + // hrvChartData.assignAll([
  340 + // // 每小时一条,00:00 ~ 15:00(CST 午夜 = 1782057600)
  341 + // V2HrvTrendItem(time: 1782057600, trendHrv: 45, state: 3), // 00:00
  342 + // V2HrvTrendItem(time: 1782061200, trendHrv: 42, state: 3), // 01:00
  343 + // V2HrvTrendItem(time: 1782064800, trendHrv: 38, state: 3), // 02:00
  344 + // V2HrvTrendItem(time: 1782068400, trendHrv: 35, state: 2), // 03:00
  345 + // V2HrvTrendItem(time: 1782072000, trendHrv: 40, state: 3), // 04:00
  346 + // V2HrvTrendItem(time: 1782075600, trendHrv: 55, state: 4), // 05:00
  347 + // V2HrvTrendItem(time: 1782079200, trendHrv: 65, state: 4), // 06:00
  348 + // V2HrvTrendItem(time: 1782082800, trendHrv: 72, state: 4), // 07:00
  349 + // V2HrvTrendItem(time: 1782086400, trendHrv: 68, state: 4), // 08:00
  350 + // V2HrvTrendItem(time: 1782090000, trendHrv: 58, state: 4), // 09:00
  351 + // V2HrvTrendItem(time: 1782093600, trendHrv: 52, state: 3), // 10:00
  352 + // V2HrvTrendItem(time: 1782097200, trendHrv: 48, state: 3), // 11:00
  353 + // V2HrvTrendItem(time: 1782100800, trendHrv: 44, state: 3), // 12:00
  354 + // V2HrvTrendItem(time: 1782104400, trendHrv: 38, state: 2), // 13:00
  355 + // V2HrvTrendItem(time: 1782108000, trendHrv: 42, state: 3), // 14:00
  356 + // V2HrvTrendItem(time: 1782111600, trendHrv: 45, state: 3), // 15:00
  357 + // ]);
319 case AppFailure(): 358 case AppFailure():
320 hrvChartData.clear(); 359 hrvChartData.clear();
321 } 360 }
322 361
323 switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) { 362 switch (await _healthApi.getV2RealtimeStress(friendUserId, intDate)) {
324 case AppSuccess(:final data): 363 case AppSuccess(:final data):
325 - // stressChartData.assignAll(data.list ?? []);  
326 - stressChartData.assignAll(  
327 - // 每 6 分钟一条,00:00 ~ 15:00(共 150 条)  
328 - // midnight CST = 1782057600,步长 360s  
329 - List.generate(150, (i) {  
330 - const midnight = 1782057600;  
331 - final ts = midnight + i * 360;  
332 - final minutesFromMidnight = i * 6;  
333 - final hour = minutesFromMidnight ~/ 60;  
334 - final minInHour = minutesFromMidnight % 60;  
335 - // 用正弦波模拟真实波动,早高峰(7-9点)压力最高  
336 - final base = hour < 6  
337 - ? 25.0 // 深夜低压力  
338 - : hour < 9  
339 - ? 55.0 + (hour - 6) * 10.0 // 早高峰爬升  
340 - : hour < 12  
341 - ? 75.0 - (hour - 9) * 8.0 // 上午下降  
342 - : 50.0 - (hour - 12) * 3.0; // 下午缓降  
343 - // 叠加微小波动(用 i 模拟随机)  
344 - final jitter = (i % 7 - 3) * 2.0 + (minInHour % 3) * 1.5;  
345 - final value = (base + jitter).clamp(10.0, 100.0).round();  
346 - final int state;  
347 - if (value >= 75) {  
348 - state = 1;  
349 - } else if (value >= 55) {  
350 - state = 2;  
351 - } else if (value >= 35) {  
352 - state = 3;  
353 - } else {  
354 - state = 4;  
355 - }  
356 - return V2RealtimeStressItem(time: ts, value: value, state: state);  
357 - }),  
358 - ); 364 + stressChartData.assignAll(data.list ?? []);
  365 + // stressChartData.assignAll(
  366 + // // 每 6 分钟一条,00:00 ~ 15:00(共 150 条)
  367 + // // midnight CST = 1782057600,步长 360s
  368 + // List.generate(150, (i) {
  369 + // const midnight = 1782057600;
  370 + // final ts = midnight + i * 360;
  371 + // final minutesFromMidnight = i * 6;
  372 + // final hour = minutesFromMidnight ~/ 60;
  373 + // final minInHour = minutesFromMidnight % 60;
  374 + // // 用正弦波模拟真实波动,早高峰(7-9点)压力最高
  375 + // final base = hour < 6
  376 + // ? 25.0 // 深夜低压力
  377 + // : hour < 9
  378 + // ? 55.0 + (hour - 6) * 10.0 // 早高峰爬升
  379 + // : hour < 12
  380 + // ? 75.0 - (hour - 9) * 8.0 // 上午下降
  381 + // : 50.0 - (hour - 12) * 3.0; // 下午缓降
  382 + // // 叠加微小波动(用 i 模拟随机)
  383 + // final jitter = (i % 7 - 3) * 2.0 + (minInHour % 3) * 1.5;
  384 + // final value = (base + jitter).clamp(10.0, 100.0).round();
  385 + // final int state;
  386 + // if (value >= 75) {
  387 + // state = 1;
  388 + // } else if (value >= 55) {
  389 + // state = 2;
  390 + // } else if (value >= 35) {
  391 + // state = 3;
  392 + // } else {
  393 + // state = 4;
  394 + // }
  395 + // return V2RealtimeStressItem(time: ts, value: value, state: state);
  396 + // }),
  397 + // );
  398 +
359 case AppFailure(): 399 case AppFailure():
360 stressChartData.clear(); 400 stressChartData.clear();
361 } 401 }
@@ -459,6 +499,7 @@ class TodayController extends GetxController { @@ -459,6 +499,7 @@ class TodayController extends GetxController {
459 switch (res) { 499 switch (res) {
460 case AppSuccess(:final data): 500 case AppSuccess(:final data):
461 friendsList.assignAll(data.list); 501 friendsList.assignAll(data.list);
  502 + friendsListLimit = data.limit ?? 10;
462 onFriendListUpdated?.call(); 503 onFriendListUpdated?.call();
463 case AppFailure(): 504 case AppFailure():
464 } 505 }
@@ -467,6 +508,7 @@ class TodayController extends GetxController { @@ -467,6 +508,7 @@ class TodayController extends GetxController {
467 } 508 }
468 509
469 RxList<FriendItem> friendsList = RxList.empty(); 510 RxList<FriendItem> friendsList = RxList.empty();
  511 + int friendsListLimit = 10;
470 512
471 Future<void> toPremiumPage() async { 513 Future<void> toPremiumPage() async {
472 await Get.toNamed(Routes.PURCHASE); 514 await Get.toNamed(Routes.PURCHASE);
@@ -478,4 +520,88 @@ class TodayController extends GetxController { @@ -478,4 +520,88 @@ class TodayController extends GetxController {
478 } on Exception catch (e) {} 520 } on Exception catch (e) {}
479 } 521 }
480 } 522 }
  523 +
  524 + Future<void> toPremiumDiscoutPage() async {
  525 + await Get.toNamed(Routes.MEMBERSHIP_OFFER);
  526 + final vipResult = await _vipApi.getVipInfo();
  527 + if (vipResult case AppSuccess(data: final vip)) {
  528 + try {
  529 + final vipPrefs = UserPreferencesVipInfo.fromVipInfo(vip);
  530 + await Get.find<UserPreferencesStorage>().updateVipInfo(vipPrefs);
  531 + } on Exception catch (e) {}
  532 + }
  533 + }
  534 +
  535 + final yearlyProduct = Rxn<PayProduct>();
  536 + final yearlyProductAppleInfo = Rxn<AppleProductInfo>();
  537 + final PlatformHostApi _platformHostApi = PlatformHostApi();
  538 +
  539 + Future<void> getProductList() async {
  540 + final result = await _payApi.getProductList(10);
  541 + if (result is! AppSuccess<PayProductListResponse>) return;
  542 +
  543 + final products = (result.data.productList ?? const <PayProduct>[])
  544 + .where((product) => _nonEmpty(product.appleId) != null)
  545 + .toList();
  546 + yearlyProduct.value = products
  547 + // .firstWhereOrNull((p) => p.appleId == 'com.doublefeel.yearly_v1');
  548 + .firstWhereOrNull((p) => p.appleId == 'com.doublefeel.yearly');
  549 + AppLogger.e(yearlyProduct);
  550 + if (yearlyProduct.value != null) {
  551 + final appleProductId = _nonEmpty(yearlyProduct.value!.appleId);
  552 + if (appleProductId == null) return;
  553 +
  554 + yearlyProductAppleInfo.value = await _requestAppleProductInfo(
  555 + productId: appleProductId,
  556 + baseUnit: yearlyProduct.value!.content?.baseUnit() ?? 1);
  557 + }
  558 + }
  559 +
  560 + Future<AppleProductInfo?> _requestAppleProductInfo({
  561 + required String productId,
  562 + required int baseUnit,
  563 + }) async {
  564 + try {
  565 + return await _platformHostApi.requestAppleProductInfo(
  566 + productId,
  567 + baseUnit,
  568 + );
  569 + } catch (_) {
  570 + return null;
  571 + }
  572 + }
  573 +
  574 + String? _nonEmpty(String? value) {
  575 + final trimmed = value?.trim();
  576 + if (trimmed == null || trimmed.isEmpty) return null;
  577 + return trimmed;
  578 + }
  579 +
  580 + String originDisplayPrice() {
  581 + final appleInfo = yearlyProductAppleInfo.value;
  582 + if (appleInfo == null) return '';
  583 + final actual =
  584 + appleInfo.price > 0 ? appleInfo.price : appleInfo.originPrice;
  585 + final inflated = actual / 100.0 * 1.2;
  586 + // 保留两位小数,单位与 currencyCode 一致
  587 + return '${appleInfo.currencyCode}${inflated.toStringAsFixed(2)}';
  588 + }
  589 +
  590 + String displayPrice() {
  591 + final appleInfo = yearlyProductAppleInfo.value;
  592 + if (appleInfo == null) return '';
  593 + final price = appleInfo.price;
  594 + if (price > 0) {
  595 + final priceDescription = appleInfo.priceDescription.trim();
  596 + if (priceDescription.isNotEmpty) {
  597 + return priceDescription;
  598 + }
  599 + }
  600 + return appleInfo.originPriceDescription;
  601 + }
  602 +
  603 + @override
  604 + void didChangeAppLifecycleState(AppLifecycleState state) {
  605 + super.didChangeAppLifecycleState(state);
  606 + }
481 } 607 }
@@ -51,7 +51,7 @@ class MyTab extends GetView<MyController> { @@ -51,7 +51,7 @@ class MyTab extends GetView<MyController> {
51 const SizedBox(height: 28), 51 const SizedBox(height: 28),
52 vipInfo?.isVip == true 52 vipInfo?.isVip == true
53 ? _ProCard(vipInfo: vipInfo) 53 ? _ProCard(vipInfo: vipInfo)
54 - : const _UnlockPremiumCard(), 54 + : _UnlockPremiumCard(controller),
55 const SizedBox(height: 12), 55 const SizedBox(height: 12),
56 _WatchThemeCard( 56 _WatchThemeCard(
57 themes: controller.officialWatchThemes, 57 themes: controller.officialWatchThemes,
@@ -356,16 +356,18 @@ class _Avatar extends StatelessWidget { @@ -356,16 +356,18 @@ class _Avatar extends StatelessWidget {
356 } 356 }
357 357
358 class _UnlockPremiumCard extends StatelessWidget { 358 class _UnlockPremiumCard extends StatelessWidget {
359 - const _UnlockPremiumCard();  
360 - 359 + const _UnlockPremiumCard(this.controller);
  360 + final MyController controller;
361 @override 361 @override
362 Widget build(BuildContext context) { 362 Widget build(BuildContext context) {
363 return GestureDetector( 363 return GestureDetector(
364 behavior: HitTestBehavior.opaque, 364 behavior: HitTestBehavior.opaque,
365 - onTap: () => Get.toNamed(Routes.PURCHASE), 365 + onTap: () {
  366 + controller.toPremiumPage();
  367 + },
366 child: Container( 368 child: Container(
367 height: 128, 369 height: 128,
368 - padding: const EdgeInsets.fromLTRB(20, 14, 20, 14), 370 + padding: const EdgeInsets.fromLTRB(20, 14, 0, 0),
369 decoration: BoxDecoration( 371 decoration: BoxDecoration(
370 gradient: const LinearGradient( 372 gradient: const LinearGradient(
371 begin: Alignment.centerLeft, 373 begin: Alignment.centerLeft,
@@ -377,48 +379,88 @@ class _UnlockPremiumCard extends StatelessWidget { @@ -377,48 +379,88 @@ class _UnlockPremiumCard extends StatelessWidget {
377 ), 379 ),
378 borderRadius: BorderRadius.circular(16), 380 borderRadius: BorderRadius.circular(16),
379 ), 381 ),
380 - child: Column(  
381 - mainAxisAlignment: MainAxisAlignment.end,  
382 - crossAxisAlignment: CrossAxisAlignment.start, 382 + child: Row(
383 children: [ 383 children: [
384 - Container(  
385 - height: 32,  
386 - padding:  
387 - const EdgeInsets.symmetric(horizontal: 22, vertical: 8),  
388 - decoration: ShapeDecoration(  
389 - color: const Color(0xFFFFDF50),  
390 - shape: RoundedRectangleBorder(  
391 - borderRadius: BorderRadius.circular(24),  
392 - ),  
393 - ),  
394 - child: Row(  
395 - mainAxisSize: MainAxisSize.min,  
396 - mainAxisAlignment: MainAxisAlignment.center,  
397 - crossAxisAlignment: CrossAxisAlignment.center, 384 + Expanded(
  385 + child: Column(
  386 + mainAxisAlignment: MainAxisAlignment.end,
  387 + crossAxisAlignment: CrossAxisAlignment.start,
398 children: [ 388 children: [
399 - Row(  
400 - mainAxisSize: MainAxisSize.min,  
401 - mainAxisAlignment: MainAxisAlignment.start,  
402 - crossAxisAlignment: CrossAxisAlignment.center,  
403 - spacing: 4,  
404 - children: [  
405 - Image.asset('assets/images/common/ic_pro.png',  
406 - width: 18,  
407 - height: 18,  
408 - color: context.colors.textPrimary),  
409 - Text(  
410 - '立即解锁',  
411 - style: TextStyle(  
412 - color: context.colors.textPrimary,  
413 - fontSize: 14,  
414 - fontWeight: FontWeight.w500,  
415 - ), 389 + ShaderMask(
  390 + shaderCallback: (bounds) => const LinearGradient(
  391 + begin: Alignment.topCenter,
  392 + end: Alignment.bottomCenter,
  393 + colors: [
  394 + Color(0xFFFFE8AE),
  395 + Color(0xFFFFFAED),
  396 + Color(0xFFFFFFFF)
  397 + ],
  398 + ).createShader(bounds),
  399 + child: const Text(
  400 + '解锁专业版',
  401 + style: TextStyle(
  402 + color: Colors.white,
  403 + fontSize: 16,
  404 + fontWeight: FontWeight.w600,
416 ), 405 ),
417 - ], 406 + ),
  407 + ),
  408 + SizedBox(
  409 + height: 4,
  410 + ),
  411 + Text(
  412 + '开启压力预警与健康陪伴之旅',
  413 + textAlign: TextAlign.center,
  414 + style: TextStyle(
  415 + color: const Color(0xFFC6B2FF),
  416 + fontSize: 12,
  417 + fontFamily: 'PingFang SC',
  418 + fontWeight: FontWeight.w500,
  419 + ),
  420 + ),
  421 + Container(
  422 + height: 32,
  423 + padding: const EdgeInsets.symmetric(
  424 + horizontal: 22, vertical: 8),
  425 + margin: EdgeInsets.only(bottom: 20, top: 12),
  426 + decoration: ShapeDecoration(
  427 + color: const Color(0xFFFFDF50),
  428 + shape: RoundedRectangleBorder(
  429 + borderRadius: BorderRadius.circular(24),
  430 + ),
  431 + ),
  432 + child: Row(
  433 + mainAxisSize: MainAxisSize.min,
  434 + mainAxisAlignment: MainAxisAlignment.center,
  435 + crossAxisAlignment: CrossAxisAlignment.center,
  436 + children: [
  437 + Row(
  438 + mainAxisSize: MainAxisSize.min,
  439 + mainAxisAlignment: MainAxisAlignment.start,
  440 + crossAxisAlignment: CrossAxisAlignment.center,
  441 + spacing: 4,
  442 + children: [
  443 + Image.asset('assets/images/common/ic_pro.png',
  444 + width: 18,
  445 + height: 18,
  446 + color: context.colors.textPrimary),
  447 + Text(
  448 + '立即解锁',
  449 + style: TextStyle(
  450 + color: context.colors.textPrimary,
  451 + fontSize: 14,
  452 + fontWeight: FontWeight.w500,
  453 + ),
  454 + ),
  455 + ],
  456 + ),
  457 + ],
  458 + ),
418 ), 459 ),
419 ], 460 ],
420 ), 461 ),
421 ), 462 ),
  463 + Image.asset('assets/images/my/bg_my_unlock_vip.png')
422 ], 464 ],
423 ), 465 ),
424 )); 466 ));
@@ -434,7 +476,7 @@ class _ProCard extends StatelessWidget { @@ -434,7 +476,7 @@ class _ProCard extends StatelessWidget {
434 Widget build(BuildContext context) { 476 Widget build(BuildContext context) {
435 return GestureDetector( 477 return GestureDetector(
436 behavior: HitTestBehavior.opaque, 478 behavior: HitTestBehavior.opaque,
437 - onTap: () => Get.toNamed(Routes.PURCHASE), 479 + onTap: () => Get.toNamed(Routes.MEMBERSHIP_DETAIL),
438 child: Container( 480 child: Container(
439 height: 80, 481 height: 80,
440 padding: const EdgeInsets.fromLTRB(20, 14, 20, 14), 482 padding: const EdgeInsets.fromLTRB(20, 14, 20, 14),
@@ -458,7 +500,17 @@ class _ProCard extends StatelessWidget { @@ -458,7 +500,17 @@ class _ProCard extends StatelessWidget {
458 children: [ 500 children: [
459 Row( 501 Row(
460 children: [ 502 children: [
461 - const Flexible( 503 + Flexible(
  504 + child: ShaderMask(
  505 + shaderCallback: (bounds) => const LinearGradient(
  506 + begin: Alignment.topCenter,
  507 + end: Alignment.bottomCenter,
  508 + colors: [
  509 + Color(0xFFFFE8AE),
  510 + Color(0xFFFFFAED),
  511 + Color(0xFFFFFFFF),
  512 + ],
  513 + ).createShader(bounds),
462 child: Text( 514 child: Text(
463 'DoubleFeel Pro', 515 'DoubleFeel Pro',
464 maxLines: 1, 516 maxLines: 1,
@@ -470,7 +522,7 @@ class _ProCard extends StatelessWidget { @@ -470,7 +522,7 @@ class _ProCard extends StatelessWidget {
470 height: 1.4, 522 height: 1.4,
471 ), 523 ),
472 ), 524 ),
473 - ), 525 + )),
474 const SizedBox(width: 6), 526 const SizedBox(width: 6),
475 Image.asset( 527 Image.asset(
476 'assets/images/common/ic_pro_badge.webp', 528 'assets/images/common/ic_pro_badge.webp',
@@ -98,7 +98,7 @@ class TodayTabBody extends StatelessWidget { @@ -98,7 +98,7 @@ class TodayTabBody extends StatelessWidget {
98 ), 98 ),
99 onBackToToday: _backToToday, 99 onBackToToday: _backToToday,
100 onTapPremiumCard: () { 100 onTapPremiumCard: () {
101 - controller.toPremiumPage(); 101 + controller.toPremiumDiscoutPage();
102 }, 102 },
103 ), 103 ),
104 _buildWeekCalendar(context), 104 _buildWeekCalendar(context),
@@ -151,7 +151,7 @@ class TodayTabBody extends StatelessWidget { @@ -151,7 +151,7 @@ class TodayTabBody extends StatelessWidget {
151 child: Obx(() => Image.asset( 151 child: Obx(() => Image.asset(
152 controller.v2StressScore.value?.stateBg() ?? 152 controller.v2StressScore.value?.stateBg() ??
153 'assets/images/today/bg_stress_state_0.png', 153 'assets/images/today/bg_stress_state_0.png',
154 - fit: BoxFit.fitHeight, 154 + fit: BoxFit.fill,
155 )), 155 )),
156 ), 156 ),
157 Obx( 157 Obx(
@@ -224,8 +224,10 @@ class TodayTabBody extends StatelessWidget { @@ -224,8 +224,10 @@ class TodayTabBody extends StatelessWidget {
224 return Get.find<UserStateService>().isVip 224 return Get.find<UserStateService>().isVip
225 ? const SizedBox.shrink() 225 ? const SizedBox.shrink()
226 : GestureDetector( 226 : GestureDetector(
227 - onTap: () => controller.toPremiumPage(),  
228 - child: const PremiumCard()); 227 + onTap: () => controller.toPremiumDiscoutPage(),
  228 + child: PremiumCard(
  229 + controller,
  230 + ));
229 }), 231 }),
230 Obx(() => controller.showHrvAdBanner.value 232 Obx(() => controller.showHrvAdBanner.value
231 ? TodayHrvAdBanner(controller: controller) 233 ? TodayHrvAdBanner(controller: controller)
  1 +import 'package:doublefeel_flutter/app/modules/home/controllers/today_controller.dart';
1 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 2 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 3 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
3 import 'package:flutter/material.dart'; 4 import 'package:flutter/material.dart';
  5 +import 'package:get/get.dart';
4 6
5 class PremiumCard extends StatelessWidget { 7 class PremiumCard extends StatelessWidget {
6 - const PremiumCard({ 8 + const PremiumCard(
  9 + this.controller, {
7 super.key, 10 super.key,
8 - this.onClaimTap,  
9 - this.onAllPlansTap,  
10 }); 11 });
11 12
12 - final VoidCallback? onClaimTap;  
13 - final VoidCallback? onAllPlansTap; 13 + final TodayController controller;
14 14
15 static const _accent = Color(0xFFFF773C); 15 static const _accent = Color(0xFFFF773C);
16 static const _proYellow = Color(0xFFFFDF51); 16 static const _proYellow = Color(0xFFFFDF51);
@@ -44,7 +44,7 @@ class PremiumCard extends StatelessWidget { @@ -44,7 +44,7 @@ class PremiumCard extends StatelessWidget {
44 top: 7, 44 top: 7,
45 right: 12, 45 right: 12,
46 child: Image.asset( 46 child: Image.asset(
47 - 'assets/images/today/premium_gift_illustration.png', 47 + 'assets/images/today/ic_gift_box.png',
48 width: 90, 48 width: 90,
49 height: 98, 49 height: 98,
50 fit: BoxFit.contain, 50 fit: BoxFit.contain,
@@ -96,37 +96,42 @@ class PremiumCard extends StatelessWidget { @@ -96,37 +96,42 @@ class PremiumCard extends StatelessWidget {
96 ], 96 ],
97 ), 97 ),
98 const Spacer(), 98 const Spacer(),
99 - Row(  
100 - children: [  
101 - Text(  
102 - '现价',  
103 - style: TextStyle(  
104 - fontSize: 12,  
105 - fontWeight: FontWeight.w500,  
106 - color: context.colors.textPrimary,  
107 - ),  
108 - ),  
109 - const SizedBox(width: 6),  
110 - Text(  
111 - '¥78.00/年',  
112 - style: TextStyle(  
113 - fontSize: 12,  
114 - fontWeight: FontWeight.w500,  
115 - color: context.colors.textPrimary,  
116 - ),  
117 - ),  
118 - const SizedBox(width: 8),  
119 - Text(  
120 - '原价 ¥198/年',  
121 - style: TextStyle(  
122 - fontSize: 12,  
123 - color: context.colors.textTertiary,  
124 - decoration: TextDecoration.lineThrough,  
125 - decorationColor: context.colors.textTertiary,  
126 - ),  
127 - ),  
128 - ],  
129 - ), 99 + Obx(() => controller.yearlyProductAppleInfo.value !=
  100 + null &&
  101 + controller.yearlyProduct.value != null
  102 + ? Row(
  103 + children: [
  104 + Text(
  105 + '现价',
  106 + style: TextStyle(
  107 + fontSize: 12,
  108 + fontWeight: FontWeight.w500,
  109 + color: context.colors.textPrimary,
  110 + ),
  111 + ),
  112 + const SizedBox(width: 6),
  113 + Text(
  114 + controller.displayPrice(),
  115 + style: TextStyle(
  116 + fontSize: 12,
  117 + fontWeight: FontWeight.w500,
  118 + color: context.colors.textPrimary,
  119 + ),
  120 + ),
  121 + const SizedBox(width: 8),
  122 + Text(
  123 + '原价${controller.originDisplayPrice()}',
  124 + style: TextStyle(
  125 + fontSize: 12,
  126 + color: context.colors.textTertiary,
  127 + decoration: TextDecoration.lineThrough,
  128 + decorationColor:
  129 + context.colors.textTertiary,
  130 + ),
  131 + ),
  132 + ],
  133 + )
  134 + : SizedBox()),
130 const SizedBox(height: 12), 135 const SizedBox(height: 12),
131 ], 136 ],
132 ), 137 ),
@@ -145,7 +150,7 @@ class PremiumCard extends StatelessWidget { @@ -145,7 +150,7 @@ class PremiumCard extends StatelessWidget {
145 mainAxisAlignment: MainAxisAlignment.spaceBetween, 150 mainAxisAlignment: MainAxisAlignment.spaceBetween,
146 children: [ 151 children: [
147 GestureDetector( 152 GestureDetector(
148 - onTap: onAllPlansTap, 153 + onTap: () => controller.toPremiumPage(),
149 behavior: HitTestBehavior.opaque, 154 behavior: HitTestBehavior.opaque,
150 child: Text( 155 child: Text(
151 context.l10n.allPlans, 156 context.l10n.allPlans,
@@ -157,7 +162,7 @@ class PremiumCard extends StatelessWidget { @@ -157,7 +162,7 @@ class PremiumCard extends StatelessWidget {
157 ), 162 ),
158 ), 163 ),
159 GestureDetector( 164 GestureDetector(
160 - onTap: onClaimTap, 165 + onTap: () => controller.toPremiumDiscoutPage(),
161 child: Container( 166 child: Container(
162 height: 28, 167 height: 28,
163 padding: const EdgeInsets.only(left: 12, right: 8), 168 padding: const EdgeInsets.only(left: 12, right: 8),
@@ -61,7 +61,7 @@ class TodayPartnerAdBanner extends StatelessWidget { @@ -61,7 +61,7 @@ class TodayPartnerAdBanner extends StatelessWidget {
61 61
62 return GestureDetector( 62 return GestureDetector(
63 onTap: () async { 63 onTap: () async {
64 - await Get.toNamed(Routes.ADD_FRIEND); 64 + await Get.toNamed(AppRoutes.bindPartner);
65 controller.checkAddFriendVisible(); 65 controller.checkAddFriendVisible();
66 }, 66 },
67 child: _TodayGuideBanner( 67 child: _TodayGuideBanner(
@@ -288,7 +288,8 @@ class LoginController extends GetxController { @@ -288,7 +288,8 @@ class LoginController extends GetxController {
288 288
289 // 根据引导完成状态决定跳转目标 289 // 根据引导完成状态决定跳转目标
290 final userId = me.id ?? 0; 290 final userId = me.id ?? 0;
291 - if (_userAccount.hasCompletedOnboarding(userId)) { 291 + if (_userAccount.hasCompletedOnboarding(userId) ||
  292 + me.isPassNoviceGuide == 1) {
292 // 该账号已完成引导,直接进主页 293 // 该账号已完成引导,直接进主页
293 Get.offAllNamed(AppRoutes.home); 294 Get.offAllNamed(AppRoutes.home);
294 } else { 295 } else {
@@ -22,312 +22,292 @@ class PhoneLoginView extends GetView<LoginController> { @@ -22,312 +22,292 @@ class PhoneLoginView extends GetView<LoginController> {
22 ), 22 ),
23 child: Scaffold( 23 child: Scaffold(
24 resizeToAvoidBottomInset: false, 24 resizeToAvoidBottomInset: false,
25 - body: DecoratedBox(  
26 - decoration: const BoxDecoration(  
27 - gradient: LinearGradient(  
28 - begin: Alignment.topCenter,  
29 - end: Alignment.bottomCenter,  
30 - stops: [0.0, 0.25, 0.75, 1.0],  
31 - colors: [  
32 - Color(0xFFE6DBFF),  
33 - Color(0xFFEDE4FF),  
34 - Color(0xFFEEE6FF),  
35 - Color(0xFFF9F6FF),  
36 - ],  
37 - ),  
38 - ),  
39 - child: Stack(  
40 - fit: StackFit.expand,  
41 - children: [  
42 - // 主体内容  
43 - SafeArea(  
44 - child: SingleChildScrollView(  
45 - physics: const ClampingScrollPhysics(),  
46 - padding: EdgeInsets.only(  
47 - bottom: MediaQuery.viewInsetsOf(context).bottom + 24,  
48 - ),  
49 - child: Column(  
50 - crossAxisAlignment: CrossAxisAlignment.stretch, 25 + backgroundColor: Colors.white,
  26 + // body: DecoratedBox(
  27 + // // decoration: const BoxDecoration(
  28 + // // gradient: LinearGradient(
  29 + // // begin: Alignment.topCenter,
  30 + // // end: Alignment.bottomCenter,
  31 + // // // stops: [0.0, 0.25, 0.75, 1.0],
  32 + // // colors: [
  33 + // // Color(0xFFE6DBFF),
  34 + // // Color(0xFFEDE4FF),
  35 + // // Color(0xFFEEE6FF),
  36 + // // Color(0xFFF9F6FF),
  37 + // // ],
  38 + // // ),
  39 + // ),
  40 + body: Stack(
  41 + fit: StackFit.expand,
  42 + children: [
  43 + SingleChildScrollView(
  44 + physics: const ClampingScrollPhysics(),
  45 + padding: EdgeInsets.only(
  46 + bottom: MediaQuery.viewInsetsOf(context).bottom + 24,
  47 + ),
  48 + child: Column(
  49 + crossAxisAlignment: CrossAxisAlignment.stretch,
  50 + children: [
  51 + Stack(
51 children: [ 52 children: [
52 - // 顶部栏:返回按钮 + 角色插图  
53 - Stack(  
54 - clipBehavior: Clip.none,  
55 - children: [  
56 - // 返回按钮  
57 - Align(  
58 - alignment: Alignment.centerLeft,  
59 - child: IconButton(  
60 - icon: Icon(  
61 - Icons.arrow_back_ios_new_rounded,  
62 - color: context.colors.textPrimary,  
63 - size: 20,  
64 - ),  
65 - onPressed: Get.back,  
66 - ),  
67 - ),  
68 - // // 右上角角色插图  
69 - // Positioned(  
70 - // right: 16,  
71 - // top: -8,  
72 - // child: Image.asset(  
73 - // '',  
74 - // width: 113,  
75 - // height: 140,  
76 - // fit: BoxFit.contain,  
77 - // ),  
78 - // ),  
79 - ], 53 + Image.asset(
  54 + 'assets/images/user_onboarding/bg_phone_login_top.png',
  55 + fit: BoxFit.fitWidth,
80 ), 56 ),
81 -  
82 - const SizedBox(height: 44),  
83 -  
84 - // 标题区  
85 - Padding(  
86 - padding: const EdgeInsets.symmetric(horizontal: 40), 57 + Positioned(
  58 + bottom: 67,
  59 + left: 32,
87 child: Column( 60 child: Column(
88 crossAxisAlignment: CrossAxisAlignment.start, 61 crossAxisAlignment: CrossAxisAlignment.start,
89 children: [ 62 children: [
90 Text( 63 Text(
91 l10n.phoneLoginHello, 64 l10n.phoneLoginHello,
92 style: TextStyle( 65 style: TextStyle(
  66 + color: Colors.white,
93 fontSize: 28, 67 fontSize: 28,
94 - fontWeight: FontWeight.w600,  
95 - color: context.colors.textPrimary,  
96 - height: 1.4, 68 + fontWeight: FontWeight.w900,
97 ), 69 ),
98 ), 70 ),
99 const SizedBox(height: 4), 71 const SizedBox(height: 4),
100 Text( 72 Text(
101 l10n.phoneLoginWelcome, 73 l10n.phoneLoginWelcome,
102 style: TextStyle( 74 style: TextStyle(
  75 + color: Colors.white,
103 fontSize: 16, 76 fontSize: 16,
104 fontWeight: FontWeight.w600, 77 fontWeight: FontWeight.w600,
105 - color: context.colors.textPrimary,  
106 - height: 1.4,  
107 ), 78 ),
108 ), 79 ),
109 ], 80 ],
110 ), 81 ),
111 - ), 82 + )
  83 + ],
  84 + ),
112 85
113 - const SizedBox(height: 28), 86 + const SizedBox(height: 11),
114 87
115 - // 输入区  
116 - Padding(  
117 - padding: const EdgeInsets.symmetric(horizontal: 28),  
118 - child: Column(  
119 - crossAxisAlignment: CrossAxisAlignment.stretch,  
120 - children: [  
121 - // 手机号输入框  
122 - Obx(() {  
123 - final phoneNotEmpty =  
124 - controller.phoneInput.value.isNotEmpty;  
125 - return TextField(  
126 - controller: controller.phoneController,  
127 - keyboardType: TextInputType.phone,  
128 - maxLength: 13,  
129 - inputFormatters: [  
130 - FilteringTextInputFormatter.allow(  
131 - RegExp(r'[0-9\s]')),  
132 - _PhoneTextInputFormatter(),  
133 - ],  
134 - style: TextStyle(  
135 - color: context.colors.textPrimary,  
136 - fontSize: 16,  
137 - fontWeight: FontWeight.w400,  
138 - ),  
139 - decoration: InputDecoration(  
140 - hintText: l10n.phoneLoginPhoneHint,  
141 - hintStyle: TextStyle(  
142 - color: context.colors.textTertiary,  
143 - fontSize: 16,  
144 - ),  
145 - counterText: '',  
146 - filled: true,  
147 - fillColor: Colors.white,  
148 - contentPadding: const EdgeInsets.symmetric(  
149 - horizontal: 20,  
150 - vertical: 16,  
151 - ),  
152 - border: OutlineInputBorder(  
153 - borderRadius: BorderRadius.circular(27),  
154 - borderSide: BorderSide.none,  
155 - ),  
156 - enabledBorder: OutlineInputBorder(  
157 - borderRadius: BorderRadius.circular(27),  
158 - borderSide: BorderSide.none,  
159 - ),  
160 - focusedBorder: OutlineInputBorder(  
161 - borderRadius: BorderRadius.circular(27),  
162 - borderSide: BorderSide(  
163 - color: context.colors.primary,  
164 - width: 1,  
165 - ),  
166 - ),  
167 - suffixIcon: phoneNotEmpty  
168 - ? GestureDetector(  
169 - onTap:  
170 - controller.phoneController.clear,  
171 - child: Icon(  
172 - Icons.cancel,  
173 - color: context.colors.textTertiary,  
174 - size: 18,  
175 - ),  
176 - )  
177 - : null, 88 + // 输入区
  89 + Padding(
  90 + padding: const EdgeInsets.symmetric(horizontal: 28),
  91 + child: Column(
  92 + crossAxisAlignment: CrossAxisAlignment.stretch,
  93 + children: [
  94 + // 手机号输入框
  95 + Obx(() {
  96 + final phoneNotEmpty =
  97 + controller.phoneInput.value.isNotEmpty;
  98 + return TextField(
  99 + controller: controller.phoneController,
  100 + keyboardType: TextInputType.phone,
  101 + maxLength: 13,
  102 + inputFormatters: [
  103 + FilteringTextInputFormatter.allow(
  104 + RegExp(r'[0-9\s]')),
  105 + _PhoneTextInputFormatter(),
  106 + ],
  107 + style: TextStyle(
  108 + color: context.colors.textPrimary,
  109 + fontSize: 16,
  110 + fontWeight: FontWeight.w400,
  111 + ),
  112 + decoration: InputDecoration(
  113 + hintText: l10n.phoneLoginPhoneHint,
  114 + hintStyle: TextStyle(
  115 + color: context.colors.textTertiary,
  116 + fontSize: 16,
  117 + ),
  118 + counterText: '',
  119 + filled: true,
  120 + fillColor: Color(0xFFF3F3F3),
  121 + contentPadding: const EdgeInsets.symmetric(
  122 + horizontal: 20,
  123 + vertical: 16,
  124 + ),
  125 + border: OutlineInputBorder(
  126 + borderRadius: BorderRadius.circular(27),
  127 + borderSide: BorderSide.none,
  128 + ),
  129 + enabledBorder: OutlineInputBorder(
  130 + borderRadius: BorderRadius.circular(27),
  131 + borderSide: BorderSide.none,
  132 + ),
  133 + focusedBorder: OutlineInputBorder(
  134 + borderRadius: BorderRadius.circular(27),
  135 + borderSide: BorderSide(
  136 + color: context.colors.primary,
  137 + width: 1,
178 ), 138 ),
179 - );  
180 - }), 139 + ),
  140 + suffixIcon: phoneNotEmpty
  141 + ? GestureDetector(
  142 + onTap: controller.phoneController.clear,
  143 + child: Icon(
  144 + Icons.cancel,
  145 + color: context.colors.textTertiary,
  146 + size: 18,
  147 + ),
  148 + )
  149 + : null,
  150 + ),
  151 + );
  152 + }),
181 153
182 - const SizedBox(height: 16), 154 + const SizedBox(height: 16),
183 155
184 - // 验证码输入框(内嵌发送按钮)  
185 - Obx(() {  
186 - final countdown =  
187 - controller.countdownSeconds.value;  
188 - final isSending = controller.isSendingCode.value;  
189 - final isCounting = countdown > 0;  
190 - final hasSent = controller.hasSentCode.value; 156 + // 验证码输入框(内嵌发送按钮)
  157 + Obx(() {
  158 + final countdown = controller.countdownSeconds.value;
  159 + final isSending = controller.isSendingCode.value;
  160 + final isCounting = countdown > 0;
  161 + final hasSent = controller.hasSentCode.value;
191 162
192 - String btnText = hasSent  
193 - ? l10n.phoneLoginResend  
194 - : l10n.phoneLoginSendCode;  
195 - if (isSending) {  
196 - btnText = l10n.phoneLoginSending;  
197 - } else if (isCounting) {  
198 - btnText =  
199 - l10n.phoneLoginSentCountdown(countdown);  
200 - } 163 + String btnText = hasSent
  164 + ? l10n.phoneLoginResend
  165 + : l10n.phoneLoginSendCode;
  166 + if (isSending) {
  167 + btnText = l10n.phoneLoginSending;
  168 + } else if (isCounting) {
  169 + btnText = l10n.phoneLoginSentCountdown(countdown);
  170 + }
201 171
202 - return TextField(  
203 - controller: controller.codeController,  
204 - keyboardType: TextInputType.text,  
205 - maxLength: 8,  
206 - style: TextStyle(  
207 - color: context.colors.textPrimary,  
208 - fontSize: 16,  
209 - fontWeight: FontWeight.w400, 172 + return TextField(
  173 + controller: controller.codeController,
  174 + keyboardType: TextInputType.text,
  175 + maxLength: 8,
  176 + style: TextStyle(
  177 + color: context.colors.textPrimary,
  178 + fontSize: 16,
  179 + fontWeight: FontWeight.w400,
  180 + ),
  181 + decoration: InputDecoration(
  182 + hintText: l10n.phoneLoginCodeHint,
  183 + hintStyle: TextStyle(
  184 + color: context.colors.textTertiary,
  185 + fontSize: 16,
  186 + ),
  187 + counterText: '',
  188 + filled: true,
  189 + fillColor: Color(0xFFF3F3F3),
  190 + contentPadding: const EdgeInsets.symmetric(
  191 + horizontal: 20,
  192 + vertical: 16,
  193 + ),
  194 + border: OutlineInputBorder(
  195 + borderRadius: BorderRadius.circular(27),
  196 + borderSide: BorderSide.none,
  197 + ),
  198 + enabledBorder: OutlineInputBorder(
  199 + borderRadius: BorderRadius.circular(27),
  200 + borderSide: BorderSide.none,
  201 + ),
  202 + focusedBorder: OutlineInputBorder(
  203 + borderRadius: BorderRadius.circular(27),
  204 + borderSide: BorderSide(
  205 + color: context.colors.primary,
  206 + width: 1,
210 ), 207 ),
211 - decoration: InputDecoration(  
212 - hintText: l10n.phoneLoginCodeHint,  
213 - hintStyle: TextStyle(  
214 - color: context.colors.textTertiary,  
215 - fontSize: 16,  
216 - ),  
217 - counterText: '',  
218 - filled: true,  
219 - fillColor: Colors.white,  
220 - contentPadding: const EdgeInsets.symmetric(  
221 - horizontal: 20,  
222 - vertical: 16,  
223 - ),  
224 - border: OutlineInputBorder(  
225 - borderRadius: BorderRadius.circular(27),  
226 - borderSide: BorderSide.none,  
227 - ),  
228 - enabledBorder: OutlineInputBorder(  
229 - borderRadius: BorderRadius.circular(27),  
230 - borderSide: BorderSide.none,  
231 - ),  
232 - focusedBorder: OutlineInputBorder(  
233 - borderRadius: BorderRadius.circular(27),  
234 - borderSide: BorderSide(  
235 - color: context.colors.primary,  
236 - width: 1,  
237 - ),  
238 - ),  
239 - suffixIcon: Padding(  
240 - padding: const EdgeInsets.only(right: 12),  
241 - child: TextButton(  
242 - onPressed: controller.canRequestCode  
243 - ? controller.requestVerifyCode  
244 - : null,  
245 - style: TextButton.styleFrom(  
246 - minimumSize: Size.zero,  
247 - padding: const EdgeInsets.symmetric(  
248 - horizontal: 8, vertical: 4),  
249 - tapTargetSize:  
250 - MaterialTapTargetSize.shrinkWrap,  
251 - foregroundColor: context.colors.primary,  
252 - disabledForegroundColor:  
253 - context.colors.textTertiary,  
254 - textStyle: const TextStyle(  
255 - fontSize: 14,  
256 - fontWeight: FontWeight.w500,  
257 - ),  
258 - ),  
259 - child: Text(btnText), 208 + ),
  209 + suffixIcon: Padding(
  210 + padding: const EdgeInsets.only(right: 12),
  211 + child: TextButton(
  212 + onPressed: controller.canRequestCode
  213 + ? controller.requestVerifyCode
  214 + : null,
  215 + style: TextButton.styleFrom(
  216 + minimumSize: Size.zero,
  217 + padding: const EdgeInsets.symmetric(
  218 + horizontal: 8, vertical: 4),
  219 + tapTargetSize:
  220 + MaterialTapTargetSize.shrinkWrap,
  221 + foregroundColor: context.colors.primary,
  222 + disabledForegroundColor:
  223 + context.colors.textTertiary,
  224 + textStyle: const TextStyle(
  225 + fontSize: 14,
  226 + fontWeight: FontWeight.w500,
260 ), 227 ),
261 ), 228 ),
262 - suffixIconConstraints:  
263 - const BoxConstraints(minWidth: 0), 229 + child: Text(btnText),
264 ), 230 ),
265 - );  
266 - }),  
267 -  
268 - const SizedBox(height: 56),  
269 -  
270 - // 提示文字  
271 - Text(  
272 - l10n.phoneLoginAutoRegisterHint,  
273 - style: TextStyle(  
274 - color: context.colors.textTertiary,  
275 - fontSize: 12,  
276 - fontWeight: FontWeight.w400,  
277 ), 231 ),
278 - textAlign: TextAlign.center, 232 + suffixIconConstraints:
  233 + const BoxConstraints(minWidth: 0),
279 ), 234 ),
  235 + );
  236 + }),
280 237
281 - const SizedBox(height: 16), 238 + const SizedBox(height: 56),
282 239
283 - // 立即登录按钮  
284 - Obx(() {  
285 - final isLoggingIn = controller.isLoggingIn.value;  
286 - return SizedBox(  
287 - height: 48,  
288 - child: ElevatedButton(  
289 - onPressed: controller.canLogin  
290 - ? controller.login  
291 - : null,  
292 - style: ElevatedButton.styleFrom(  
293 - backgroundColor: context.colors.primary,  
294 - foregroundColor: Colors.white,  
295 - disabledBackgroundColor: context  
296 - .colors.primary  
297 - .withValues(alpha: 0.4),  
298 - disabledForegroundColor: Colors.white,  
299 - elevation: 0,  
300 - shape: RoundedRectangleBorder(  
301 - borderRadius: BorderRadius.circular(24),  
302 - ),  
303 - textStyle: const TextStyle(  
304 - fontSize: 16,  
305 - fontWeight: FontWeight.w600,  
306 - ),  
307 - ),  
308 - child: isLoggingIn  
309 - ? Row(  
310 - mainAxisAlignment:  
311 - MainAxisAlignment.center,  
312 - children: [  
313 - const _SpinningLoader(),  
314 - const SizedBox(width: 8),  
315 - Text(l10n.phoneLoginLoggingIn),  
316 - ],  
317 - )  
318 - : Text(l10n.loginBtn),  
319 - ),  
320 - );  
321 - }),  
322 - ], 240 + // 提示文字
  241 + Text(
  242 + l10n.phoneLoginAutoRegisterHint,
  243 + style: TextStyle(
  244 + color: context.colors.textTertiary,
  245 + fontSize: 12,
  246 + fontWeight: FontWeight.w400,
  247 + ),
  248 + textAlign: TextAlign.center,
323 ), 249 ),
324 - ),  
325 - ], 250 +
  251 + const SizedBox(height: 16),
  252 +
  253 + // 立即登录按钮
  254 + Obx(() {
  255 + final isLoggingIn = controller.isLoggingIn.value;
  256 + return SizedBox(
  257 + height: 48,
  258 + child: ElevatedButton(
  259 + onPressed:
  260 + controller.canLogin ? controller.login : null,
  261 + style: ElevatedButton.styleFrom(
  262 + backgroundColor: context.colors.primary,
  263 + foregroundColor: Colors.white,
  264 + disabledBackgroundColor: context.colors.primary
  265 + .withValues(alpha: 0.4),
  266 + disabledForegroundColor: Colors.white,
  267 + elevation: 0,
  268 + shape: RoundedRectangleBorder(
  269 + borderRadius: BorderRadius.circular(24),
  270 + ),
  271 + textStyle: const TextStyle(
  272 + fontSize: 16,
  273 + fontWeight: FontWeight.w600,
  274 + ),
  275 + ),
  276 + child: isLoggingIn
  277 + ? Row(
  278 + mainAxisAlignment:
  279 + MainAxisAlignment.center,
  280 + children: [
  281 + const _SpinningLoader(),
  282 + const SizedBox(width: 8),
  283 + Text(l10n.phoneLoginLoggingIn),
  284 + ],
  285 + )
  286 + : Text(l10n.loginBtn),
  287 + ),
  288 + );
  289 + }),
  290 + ],
  291 + ),
326 ), 292 ),
327 - ), 293 + ],
328 ), 294 ),
329 - ],  
330 - ), 295 + ),
  296 + Positioned(
  297 + top: MediaQuery.paddingOf(context).top,
  298 + left: 0,
  299 + child: IconButton(
  300 + highlightColor: Colors.transparent,
  301 + splashColor: Colors.transparent,
  302 + padding: EdgeInsets.zero,
  303 + onPressed: Get.back,
  304 + icon: Image.asset(
  305 + 'assets/images/common/ic_nav_back.webp',
  306 + width: 24,
  307 + height: 24,
  308 + )),
  309 + ),
  310 + ],
331 ), 311 ),
332 ), 312 ),
333 ); 313 );
  1 +import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_detail_controller.dart';
  2 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
  3 +import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
  4 +import 'package:get/get.dart';
  5 +
  6 +import '../controllers/membership_offer_controller.dart';
  7 +
  8 +class MembershipDetailBinding extends Bindings {
  9 + @override
  10 + void dependencies() {
  11 + Get.put(
  12 + MembershipDetailController(Get.find<VipApi>(), Get.find<PayApi>()),
  13 + );
  14 + }
  15 +}
  1 +import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_offer_controller.dart';
  2 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
  3 +import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
1 import 'package:get/get.dart'; 4 import 'package:get/get.dart';
2 5
3 -import '../controllers/membership_offer_controller.dart';  
4 -  
5 class MembershipOfferBinding extends Bindings { 6 class MembershipOfferBinding extends Bindings {
6 @override 7 @override
7 void dependencies() { 8 void dependencies() {
8 - Get.lazyPut<MembershipOfferController>(  
9 - () => MembershipOfferController(), 9 + Get.put(
  10 + MembershipOfferController(Get.find<VipApi>(), Get.find<PayApi>()),
10 ); 11 );
11 } 12 }
12 } 13 }
  1 +import 'package:doublefeel_flutter/core/logging/app_logger.dart';
  2 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
  3 +import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
  4 +import 'package:doublefeel_flutter/core/result/app_result.dart';
  5 +import 'package:doublefeel_flutter/core/util/app_toast.dart';
  6 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  7 +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
  8 +import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
  9 +import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
  10 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
  11 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
  12 +import 'package:get/get.dart';
  13 +
  14 +import '../../../routes/app_pages.dart';
  15 +
  16 +class MembershipDetailController extends GetxController {
  17 + MembershipDetailController(this._vipApi, this._payApi);
  18 +
  19 + final VipApi _vipApi;
  20 + final PayApi _payApi;
  21 +
  22 + final UserPreferencesStorage _userPreferences =
  23 + Get.find<UserPreferencesStorage>();
  24 + final PlatformHostApi _platformHostApi = PlatformHostApi();
  25 +
  26 + final isRestoring = false.obs;
  27 +
  28 + final isUnlocking = false.obs;
  29 + Future<void> unlock() async {
  30 + isUnlocking.value = true;
  31 + try {
  32 + final orderResult = await _payApi.createOrderByApple(123);
  33 + if (orderResult is! AppSuccess<ApplePayOrderResponse>) return;
  34 +
  35 + final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();
  36 + if (orderUuid == null || orderUuid.isEmpty) {
  37 + AppToast.show(l10n.purchaseOrderInfoUnavailable);
  38 + return;
  39 + }
  40 +
  41 + final paymentResult = await _performApplePayment(
  42 + productId: 'appleProductId',
  43 + orderUuid: orderUuid,
  44 + );
  45 + await _handleApplePaymentResult(paymentResult);
  46 + } finally {
  47 + isUnlocking.value = false;
  48 + }
  49 + }
  50 +
  51 + Future<void> _handleApplePaymentResult(
  52 + AppleProductPaymentResult? result,
  53 + ) async {
  54 + if (result?.success == true) {
  55 + await _completeSuccessfulPurchase();
  56 + return;
  57 + }
  58 + if (result?.success != false) return;
  59 +
  60 + AppToast.show(_applePaymentFailureMessage(result!));
  61 + }
  62 +
  63 + String _applePaymentFailureMessage(AppleProductPaymentResult result) {
  64 + final errorCode = result.errorCode;
  65 + if (errorCode != null) {
  66 + return _localizedApplePaymentErrorCode(errorCode);
  67 + }
  68 +
  69 + final errorMessage = _nonEmpty(result.errorMessage);
  70 + if (errorMessage == null) return l10n.purchaseApplePaymentFailed;
  71 + return _localizedApplePaymentErrorMessage(errorMessage);
  72 + }
  73 +
  74 + String _localizedApplePaymentErrorCode(int errorCode) {
  75 + return switch (errorCode) {
  76 + -1 => l10n.purchaseApplePaymentInvalidOrder,
  77 + -2 => l10n.purchaseApplePaymentProductNotFound,
  78 + -3 => l10n.purchaseApplePaymentCancelled,
  79 + -4 => l10n.purchaseApplePaymentVerificationFailed,
  80 + -5 => l10n.purchaseApplePaymentFailed,
  81 + _ => l10n.purchaseApplePaymentFailed,
  82 + };
  83 + }
  84 +
  85 + String _localizedApplePaymentErrorMessage(String errorMessage) {
  86 + if (errorMessage == 'missingUUID') {
  87 + return l10n.purchaseApplePaymentInvalidOrder;
  88 + }
  89 + if (errorMessage == 'productNotFound') {
  90 + return l10n.purchaseApplePaymentProductNotFound;
  91 + }
  92 + if (errorMessage == 'userCancelled') {
  93 + return l10n.purchaseApplePaymentCancelled;
  94 + }
  95 + if (errorMessage == 'failedVerification') {
  96 + return l10n.purchaseApplePaymentVerificationFailed;
  97 + }
  98 + if (errorMessage == 'unknown') {
  99 + return l10n.purchaseApplePaymentFailed;
  100 + }
  101 + return errorMessage;
  102 + }
  103 +
  104 + Future<void> _completeSuccessfulPurchase() async {
  105 + try {
  106 + await _refreshVipInfo();
  107 + } finally {
  108 + if (Get.arguments['from'] == 'onboarding') {
  109 + Get.offAllNamed(AppRoutes.home);
  110 + } else {
  111 + Get.offNamed(Routes.PREMIUM_ACTIVATED);
  112 + }
  113 + }
  114 + }
  115 +
  116 + Future<void> _refreshVipInfo() async {
  117 + final result = await _vipApi.getVipInfo();
  118 + if (result case AppSuccess(data: final vipInfo)) {
  119 + await _userPreferences.updateVipInfo(
  120 + UserPreferencesVipInfo.fromVipInfo(vipInfo),
  121 + );
  122 + }
  123 + }
  124 +
  125 + Future<AppleProductPaymentResult?> _performApplePayment({
  126 + required String productId,
  127 + required String orderUuid,
  128 + }) async {
  129 + try {
  130 + return await _platformHostApi.performApplePayment(productId, orderUuid);
  131 + } catch (_) {
  132 + return null;
  133 + }
  134 + }
  135 +
  136 + @override
  137 + void onInit() {
  138 + super.onInit();
  139 + _refreshVipInfo();
  140 + }
  141 +
  142 + String? _nonEmpty(String? value) {
  143 + final trimmed = value?.trim();
  144 + if (trimmed == null || trimmed.isEmpty) return null;
  145 + return trimmed;
  146 + }
  147 +
  148 + Future<void> restorePurchase() async {
  149 + if (isRestoring.value || isUnlocking.value) return;
  150 +
  151 + isRestoring.value = true;
  152 + try {
  153 + final success = await _performRestore();
  154 + if (success) {
  155 + await _completeSuccessfulPurchase();
  156 + }
  157 + } finally {
  158 + isRestoring.value = false;
  159 + }
  160 + }
  161 +
  162 + Future<bool> _performRestore() async {
  163 + try {
  164 + return await _platformHostApi.performRestore();
  165 + } catch (_) {
  166 + return false;
  167 + }
  168 + }
  169 +
  170 + void openContactUs() {
  171 + Get.toNamed(Routes.SUBMIT_FEEDBACK);
  172 + }
  173 +
  174 + Future<void> manageSubscription() async {
  175 + try {
  176 + await _platformHostApi.requestAppReview();
  177 + } catch (_) {}
  178 + }
  179 +}
  1 +import 'dart:math';
  2 +
  3 +import 'package:doublefeel_flutter/core/logging/app_logger.dart';
  4 +import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
  5 +import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
  6 +import 'package:doublefeel_flutter/core/result/app_result.dart';
  7 +import 'package:doublefeel_flutter/core/util/app_toast.dart';
  8 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  9 +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
  10 +import 'package:doublefeel_flutter/data/models/pay/apple_pay_order_response.dart';
  11 +import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
  12 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
  13 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
1 import 'package:get/get.dart'; 14 import 'package:get/get.dart';
2 15
3 import '../../../routes/app_pages.dart'; 16 import '../../../routes/app_pages.dart';
4 17
5 class MembershipOfferController extends GetxController { 18 class MembershipOfferController extends GetxController {
6 - void finish() {  
7 - Get.offAllNamed(AppRoutes.home); 19 + MembershipOfferController(this._vipApi, this._payApi);
  20 +
  21 + final VipApi _vipApi;
  22 + final PayApi _payApi;
  23 +
  24 + final UserPreferencesStorage _userPreferences =
  25 + Get.find<UserPreferencesStorage>();
  26 + final PlatformHostApi _platformHostApi = PlatformHostApi();
  27 +
  28 + final isUnlocking = false.obs;
  29 + Future<void> unlock() async {
  30 + if (isUnlocking.value) return;
  31 +
  32 + final productId = yearlyProduct.value?.id;
  33 + final appleProductId = yearlyProduct.value?.appleId;
  34 + if (productId == null || appleProductId == null || appleProductId.isEmpty) {
  35 + AppToast.show(l10n.purchaseProductInfoUnavailable);
  36 + return;
  37 + }
  38 + isUnlocking.value = true;
  39 + try {
  40 + final orderResult = await _payApi.createOrderByApple(productId);
  41 + if (orderResult is! AppSuccess<ApplePayOrderResponse>) {
  42 + if (isFromOnboard) {
  43 + Get.offAllNamed(AppRoutes.home);
  44 + }
  45 + return;
  46 + }
  47 +
  48 + final orderUuid = orderResult.data.orderInfo?.orderUuid?.trim();
  49 + if (orderUuid == null || orderUuid.isEmpty) {
  50 + AppToast.show(l10n.purchaseOrderInfoUnavailable);
  51 + if (isFromOnboard) {
  52 + Get.offAllNamed(AppRoutes.home);
  53 + }
  54 + return;
  55 + }
  56 +
  57 + final paymentResult = await _performApplePayment(
  58 + productId: appleProductId,
  59 + orderUuid: orderUuid,
  60 + );
  61 + await _handleApplePaymentResult(paymentResult);
  62 + } finally {
  63 + isUnlocking.value = false;
  64 + }
  65 + }
  66 +
  67 + Future<void> _handleApplePaymentResult(
  68 + AppleProductPaymentResult? result,
  69 + ) async {
  70 + if (result?.success == true) {
  71 + await _completeSuccessfulPurchase();
  72 + return;
  73 + }
  74 + if (result?.success != false) return;
  75 +
  76 + AppToast.show(_applePaymentFailureMessage(result!));
  77 + if (isFromOnboard) {
  78 + Get.offAllNamed(AppRoutes.home);
  79 + }
  80 + }
  81 +
  82 + String _applePaymentFailureMessage(AppleProductPaymentResult result) {
  83 + final errorCode = result.errorCode;
  84 + if (errorCode != null) {
  85 + return _localizedApplePaymentErrorCode(errorCode);
  86 + }
  87 +
  88 + final errorMessage = _nonEmpty(result.errorMessage);
  89 + if (errorMessage == null) return l10n.purchaseApplePaymentFailed;
  90 + return _localizedApplePaymentErrorMessage(errorMessage);
  91 + }
  92 +
  93 + String _localizedApplePaymentErrorCode(int errorCode) {
  94 + return switch (errorCode) {
  95 + -1 => l10n.purchaseApplePaymentInvalidOrder,
  96 + -2 => l10n.purchaseApplePaymentProductNotFound,
  97 + -3 => l10n.purchaseApplePaymentCancelled,
  98 + -4 => l10n.purchaseApplePaymentVerificationFailed,
  99 + -5 => l10n.purchaseApplePaymentFailed,
  100 + _ => l10n.purchaseApplePaymentFailed,
  101 + };
  102 + }
  103 +
  104 + String _localizedApplePaymentErrorMessage(String errorMessage) {
  105 + if (errorMessage == 'missingUUID') {
  106 + return l10n.purchaseApplePaymentInvalidOrder;
  107 + }
  108 + if (errorMessage == 'productNotFound') {
  109 + return l10n.purchaseApplePaymentProductNotFound;
  110 + }
  111 + if (errorMessage == 'userCancelled') {
  112 + return l10n.purchaseApplePaymentCancelled;
  113 + }
  114 + if (errorMessage == 'failedVerification') {
  115 + return l10n.purchaseApplePaymentVerificationFailed;
  116 + }
  117 + if (errorMessage == 'unknown') {
  118 + return l10n.purchaseApplePaymentFailed;
  119 + }
  120 + return errorMessage;
  121 + }
  122 +
  123 + Future<void> _completeSuccessfulPurchase() async {
  124 + try {
  125 + await _refreshVipInfo();
  126 + } finally {
  127 + if (isFromOnboard) {
  128 + Get.offAllNamed(AppRoutes.home);
  129 + } else {
  130 + Get.offNamed(Routes.PREMIUM_ACTIVATED);
  131 + }
  132 + }
  133 + }
  134 +
  135 + Future<void> _refreshVipInfo() async {
  136 + final result = await _vipApi.getVipInfo();
  137 + if (result case AppSuccess(data: final vipInfo)) {
  138 + await _userPreferences.updateVipInfo(
  139 + UserPreferencesVipInfo.fromVipInfo(vipInfo),
  140 + );
  141 + }
  142 + }
  143 +
  144 + Future<AppleProductPaymentResult?> _performApplePayment({
  145 + required String productId,
  146 + required String orderUuid,
  147 + }) async {
  148 + try {
  149 + return await _platformHostApi.performApplePayment(productId, orderUuid);
  150 + } catch (_) {
  151 + return null;
  152 + }
  153 + }
  154 +
  155 + @override
  156 + void onInit() {
  157 + super.onInit();
  158 + isFromOnboard = Get.arguments?['from'] == 'onboarding';
  159 + // getProductList();
  160 + _refreshVipInfo();
  161 + getProductList();
  162 + }
  163 +
  164 + var isFromOnboard = false;
  165 +
  166 + final yearlyProduct = Rxn<PayProduct>();
  167 + final yearlyProductAppleInfo = Rxn<AppleProductInfo>();
  168 +
  169 + Future<void> getProductList() async {
  170 + final result = await _payApi.getProductList(10);
  171 + if (result is! AppSuccess<PayProductListResponse>) return;
  172 +
  173 + final products = (result.data.productList ?? const <PayProduct>[])
  174 + .where((product) => _nonEmpty(product.appleId) != null)
  175 + .toList();
  176 + yearlyProduct.value = products
  177 + // .firstWhereOrNull((p) => p.appleId == 'com.doublefeel.yearly_v1');
  178 + .firstWhereOrNull((p) => p.appleId == 'com.doublefeel.yearly');
  179 +
  180 + if (yearlyProduct.value != null) {
  181 + final appleProductId = _nonEmpty(yearlyProduct.value!.appleId);
  182 + if (appleProductId == null) return;
  183 +
  184 + yearlyProductAppleInfo.value = await _requestAppleProductInfo(
  185 + productId: appleProductId,
  186 + baseUnit: yearlyProduct.value!.content?.baseUnit() ?? 1);
  187 + }
  188 + }
  189 +
  190 + Future<AppleProductInfo?> _requestAppleProductInfo({
  191 + required String productId,
  192 + required int baseUnit,
  193 + }) async {
  194 + try {
  195 + return await _platformHostApi.requestAppleProductInfo(
  196 + productId,
  197 + baseUnit,
  198 + );
  199 + } catch (_) {
  200 + return null;
  201 + }
  202 + }
  203 +
  204 + String? _nonEmpty(String? value) {
  205 + final trimmed = value?.trim();
  206 + if (trimmed == null || trimmed.isEmpty) return null;
  207 + return trimmed;
8 } 208 }
9 } 209 }
  1 +import 'package:doublefeel_flutter/app/modules/home/views/tabs/my_tab.dart';
  2 +import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_detail_controller.dart';
  3 +import 'package:doublefeel_flutter/app/modules/purchase/widgets/purchase_colors.dart';
  4 +import 'package:doublefeel_flutter/app/modules/purchase/widgets/refund_explanation_bottom_sheet.dart';
  5 +import 'package:doublefeel_flutter/app/widget/premium_benefit_list_view.dart';
  6 +import 'package:doublefeel_flutter/core/theme/app_theme.dart';
  7 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  8 +import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
  9 +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
  10 +import 'package:flutter/material.dart';
  11 +import 'package:get/get.dart';
  12 +
  13 +class MembershipDetailView extends GetView<MembershipDetailController> {
  14 + const MembershipDetailView({super.key});
  15 +
  16 + @override
  17 + Widget build(BuildContext context) {
  18 + final userPrefs = Get.find<UserPreferencesStorage>();
  19 + return Scaffold(
  20 + appBar: AppBar(
  21 + leading: IconButton(
  22 + highlightColor: Colors.transparent,
  23 + splashColor: Colors.transparent,
  24 + padding: EdgeInsets.zero,
  25 + onPressed: Get.back,
  26 + icon: Image.asset(
  27 + 'assets/images/common/ic_nav_back.webp',
  28 + width: 24,
  29 + height: 24,
  30 + ),
  31 + ),
  32 + actions: [
  33 + Padding(
  34 + padding: const EdgeInsets.only(right: 20.0),
  35 + child: TextButton(
  36 + onPressed: controller.isRestoring.value
  37 + ? null
  38 + : controller.restorePurchase,
  39 + style: TextButton.styleFrom(
  40 + foregroundColor: PurchaseColors.title,
  41 + padding: const EdgeInsets.symmetric(horizontal: 16),
  42 + textStyle: const TextStyle(fontSize: 14, height: 1.2),
  43 + ),
  44 + child: controller.isRestoring.value
  45 + ? const SizedBox(
  46 + width: 16,
  47 + height: 16,
  48 + child: CircularProgressIndicator(strokeWidth: 2),
  49 + )
  50 + : Text(context.l10n.purchaseRestore),
  51 + ),
  52 + ),
  53 + ],
  54 + ),
  55 + extendBodyBehindAppBar: true,
  56 + body: SingleChildScrollView(
  57 + physics: const ClampingScrollPhysics(),
  58 + padding: EdgeInsets.only(
  59 + bottom: MediaQuery.paddingOf(context).bottom + 16,
  60 + ),
  61 + child: Column(
  62 + crossAxisAlignment: CrossAxisAlignment.start,
  63 + children: [
  64 + Image.asset(
  65 + 'assets/images/today/bg_stress_state_4.png',
  66 + width: Get.width,
  67 + fit: BoxFit.cover,
  68 + ),
  69 + Padding(
  70 + padding: const EdgeInsets.only(top: 31, left: 18),
  71 + child: Text(
  72 + 'DoubleFeel Pro',
  73 + style: TextStyle(
  74 + color: Colors.black,
  75 + fontSize: 24,
  76 + fontWeight: FontWeight.w600,
  77 + ),
  78 + ),
  79 + ),
  80 + Padding(
  81 + padding: const EdgeInsets.only(top: 8, left: 18),
  82 + child: Row(
  83 + spacing: 4,
  84 + children: [
  85 + Container(
  86 + width: 20,
  87 + height: 20,
  88 + clipBehavior: Clip.antiAlias,
  89 + padding: EdgeInsets.all(1),
  90 + decoration: ShapeDecoration(
  91 + shape: RoundedRectangleBorder(
  92 + borderRadius: BorderRadius.circular(20),
  93 + ),
  94 + color: context.colors.primary),
  95 + child: Image.asset('assets/images/common/ic_pro.png'),
  96 + ),
  97 + Obx(() {
  98 + final preferences = userPrefs.preferences.value;
  99 + final vipInfo = preferences.vipInfo;
  100 + return Text(
  101 + _vipSubtitle(vipInfo),
  102 + style: TextStyle(
  103 + color: context.colors.textPrimary,
  104 + fontSize: 14,
  105 + fontWeight: FontWeight.w500,
  106 + ),
  107 + );
  108 + })
  109 + ],
  110 + ),
  111 + ),
  112 + Container(
  113 + color: Color(0xFFD9D9D9),
  114 + height: 1,
  115 + width: Get.width,
  116 + margin: EdgeInsets.only(
  117 + left: 16,
  118 + right: 16,
  119 + top: 28,
  120 + ),
  121 + ),
  122 + Padding(
  123 + padding: const EdgeInsets.only(
  124 + top: 28, left: 16, right: 16, bottom: 16),
  125 + child: Text(
  126 + '畅享全部高级权益',
  127 + style: TextStyle(
  128 + color: Colors.black,
  129 + fontSize: 18,
  130 + fontWeight: FontWeight.w600,
  131 + ),
  132 + ),
  133 + ),
  134 + //todo
  135 + PremiumBenefitListView(),
  136 +
  137 + Obx(() {
  138 + final userPrefs = Get.find<UserPreferencesStorage>();
  139 + final preferences = userPrefs.preferences.value;
  140 + final vipInfo = preferences.vipInfo;
  141 + return (vipInfo == null || vipInfo.vipType == 0)
  142 + ? SizedBox()
  143 + : Column(
  144 + crossAxisAlignment: CrossAxisAlignment.start,
  145 + children: [
  146 + Padding(
  147 + padding: const EdgeInsets.only(
  148 + top: 28, left: 16, right: 16),
  149 + child: Text(
  150 + '会员管理',
  151 + style: TextStyle(
  152 + color: Colors.black,
  153 + fontSize: 18,
  154 + fontWeight: FontWeight.w600,
  155 + ),
  156 + ),
  157 + ),
  158 + Container(
  159 + margin: EdgeInsets.only(left: 16, right: 16, top: 16),
  160 + padding: const EdgeInsets.symmetric(horizontal: 20),
  161 + decoration: ShapeDecoration(
  162 + color: Colors.white,
  163 + shape: RoundedRectangleBorder(
  164 + borderRadius:
  165 + BorderRadius.all(Radius.circular(16)),
  166 + ),
  167 + ),
  168 + child: Column(
  169 + children: [
  170 + SizedBox(
  171 + height: 56,
  172 + child: Row(
  173 + children: [
  174 + Text(
  175 + '会员类型',
  176 + textAlign: TextAlign.center,
  177 + style: TextStyle(
  178 + color: context.colors.textPrimary,
  179 + fontSize: 14,
  180 + fontWeight: FontWeight.w400,
  181 + ),
  182 + ),
  183 + Spacer(),
  184 + Obx(() {
  185 + final preferences =
  186 + userPrefs.preferences.value;
  187 + final vipInfo = preferences.vipInfo;
  188 + return Text(
  189 + _vipType(vipInfo),
  190 + textAlign: TextAlign.center,
  191 + style: TextStyle(
  192 + color: context.colors.textPrimary,
  193 + fontSize: 14,
  194 + fontWeight: FontWeight.w400,
  195 + ),
  196 + );
  197 + })
  198 + ],
  199 + ),
  200 + ),
  201 + SizedBox(
  202 + height: 56,
  203 + child: Row(
  204 + children: [
  205 + Text(
  206 + '有效期至',
  207 + textAlign: TextAlign.center,
  208 + style: TextStyle(
  209 + color: context.colors.textPrimary,
  210 + fontSize: 14,
  211 + fontWeight: FontWeight.w400,
  212 + ),
  213 + ),
  214 + Spacer(),
  215 + Obx(() {
  216 + final preferences =
  217 + userPrefs.preferences.value;
  218 + final vipInfo = preferences.vipInfo;
  219 + return Text(
  220 + _vipExprieDate(vipInfo),
  221 + textAlign: TextAlign.center,
  222 + style: TextStyle(
  223 + color: context.colors.textPrimary,
  224 + fontSize: 14,
  225 + fontWeight: FontWeight.w400,
  226 + ),
  227 + );
  228 + })
  229 + ],
  230 + ),
  231 + ),
  232 + if (vipInfo?.vipType == 1 ||
  233 + vipInfo?.vipType == 2 ||
  234 + vipInfo?.vipType == 3 ||
  235 + vipInfo?.vipType == 4)
  236 + GestureDetector(
  237 + onTap: () {
  238 + controller.manageSubscription();
  239 + },
  240 + child: SizedBox(
  241 + height: 56,
  242 + child: Row(
  243 + children: [
  244 + Text(
  245 + '管理订阅',
  246 + textAlign: TextAlign.center,
  247 + style: TextStyle(
  248 + color: context.colors.textPrimary,
  249 + fontSize: 14,
  250 + fontWeight: FontWeight.w400,
  251 + ),
  252 + ),
  253 + Spacer(),
  254 + Image.asset(
  255 + 'assets/images/common/ic_more_gray.png',
  256 + width: 16,
  257 + height: 16,
  258 + ),
  259 + ],
  260 + ),
  261 + ),
  262 + ),
  263 + ],
  264 + ),
  265 + ),
  266 + ],
  267 + );
  268 + }),
  269 + SizedBox(
  270 + height: 28,
  271 + ),
  272 + _LegalNotes(controller: controller)
  273 + ],
  274 + ),
  275 + ),
  276 + );
  277 + }
  278 +
  279 + String _vipSubtitle(UserPreferencesVipInfo? vipInfo) {
  280 + final endDate = vipInfo?.vipEndDate ?? 0;
  281 + if (vipInfo?.isVip != true || endDate <= 0) {
  282 + return '立即解锁';
  283 + }
  284 +
  285 + if (vipInfo?.isForeverVip ?? false) {
  286 + return '终身使用,免费更新';
  287 + }
  288 +
  289 + final milliseconds = endDate > 1000000000000 ? endDate : endDate * 1000;
  290 + final date = DateTime.fromMillisecondsSinceEpoch(milliseconds);
  291 + final month = date.month.toString().padLeft(2, '0');
  292 + final day = date.day.toString().padLeft(2, '0');
  293 + return '有效期至 ${date.year}-$month-$day';
  294 + }
  295 +
  296 + String _vipExprieDate(UserPreferencesVipInfo? vipInfo) {
  297 + final endDate = vipInfo?.vipEndDate ?? 0;
  298 + if (vipInfo?.isVip != true || endDate <= 0) {
  299 + return '立即解锁';
  300 + }
  301 +
  302 + if (vipInfo?.isForeverVip ?? false) {
  303 + return '永不过期';
  304 + }
  305 +
  306 + final milliseconds = endDate > 1000000000000 ? endDate : endDate * 1000;
  307 + final date = DateTime.fromMillisecondsSinceEpoch(milliseconds);
  308 + final month = date.month.toString().padLeft(2, '0');
  309 + final day = date.day.toString().padLeft(2, '0');
  310 + return '${date.year}-$month-$day';
  311 + }
  312 +
  313 + String _vipType(UserPreferencesVipInfo? vipInfo) {
  314 + if (vipInfo == null || vipInfo.isVip != true || vipInfo.vipEndDate <= 0) {
  315 + return '立即解锁';
  316 + }
  317 +
  318 + if (vipInfo.isForeverVip) {
  319 + return '终身会员';
  320 + }
  321 + switch (vipInfo.vipType) {
  322 + case 1:
  323 + return '月度会员';
  324 + case 2:
  325 + return '季度会员';
  326 + case 3:
  327 + return '年度会员';
  328 + case 10:
  329 + return '终身会员';
  330 + default:
  331 + return '';
  332 + }
  333 + }
  334 +}
  335 +
  336 +class _LegalNotes extends StatelessWidget {
  337 + const _LegalNotes({required this.controller});
  338 +
  339 + final MembershipDetailController controller;
  340 +
  341 + @override
  342 + Widget build(BuildContext context) {
  343 + return Padding(
  344 + padding: const EdgeInsets.symmetric(horizontal: 16),
  345 + child: Column(
  346 + crossAxisAlignment: CrossAxisAlignment.start,
  347 + children: [
  348 + Text(
  349 + context.l10n.purchaseNotesTitle,
  350 + style: const TextStyle(
  351 + color: Colors.black,
  352 + fontSize: 18,
  353 + fontWeight: FontWeight.w600,
  354 + height: 1.2,
  355 + ),
  356 + ),
  357 + const SizedBox(height: 24),
  358 + _LegalText(
  359 + prefix: '1. ',
  360 + text: context.l10n.purchaseNoteSubscription,
  361 + linkText: '了解更多',
  362 + linkSuffix: '。',
  363 + onLinkTap: showRefundExplanationBottomSheet,
  364 + ),
  365 + const SizedBox(height: 12),
  366 + _LegalText(
  367 + prefix: '2. ',
  368 + text: context.l10n.purchaseNoteRestore,
  369 + linkText: context.l10n.purchaseRestore,
  370 + linkSuffix: '。',
  371 + onLinkTap: controller.restorePurchase,
  372 + ),
  373 + const SizedBox(height: 12),
  374 + _LegalText(
  375 + prefix: '3. ',
  376 + text: context.l10n.purchaseNoteContact,
  377 + linkText: '联系我们',
  378 + linkSuffix: '。',
  379 + onLinkTap: controller.openContactUs,
  380 + ),
  381 + ],
  382 + ),
  383 + );
  384 + }
  385 +}
  386 +
  387 +class _LegalText extends StatelessWidget {
  388 + const _LegalText({
  389 + required this.prefix,
  390 + required this.text,
  391 + this.linkText,
  392 + this.linkSuffix,
  393 + this.onLinkTap,
  394 + });
  395 +
  396 + final String prefix;
  397 + final String text;
  398 + final String? linkText;
  399 + final String? linkSuffix;
  400 + final VoidCallback? onLinkTap;
  401 +
  402 + @override
  403 + Widget build(BuildContext context) {
  404 + return Row(
  405 + crossAxisAlignment: CrossAxisAlignment.start,
  406 + children: [
  407 + Text(
  408 + prefix,
  409 + style: const TextStyle(
  410 + color: PurchaseColors.subtitle,
  411 + fontSize: 12,
  412 + height: 1.4,
  413 + ),
  414 + ),
  415 + Expanded(
  416 + child: Text.rich(
  417 + TextSpan(
  418 + children: [
  419 + TextSpan(text: text),
  420 + if (linkText case final linkText?)
  421 + WidgetSpan(
  422 + alignment: PlaceholderAlignment.baseline,
  423 + baseline: TextBaseline.alphabetic,
  424 + child: GestureDetector(
  425 + behavior: HitTestBehavior.opaque,
  426 + onTap: onLinkTap,
  427 + child: Text(
  428 + linkText,
  429 + style: const TextStyle(
  430 + color: Color(0xFF845EEE),
  431 + fontSize: 12,
  432 + height: 1.4,
  433 + fontWeight: FontWeight.w500,
  434 + decoration: TextDecoration.underline,
  435 + decorationColor: Color(0xFF845EEE),
  436 + ),
  437 + ),
  438 + ),
  439 + ),
  440 + if (linkSuffix case final linkSuffix?)
  441 + TextSpan(text: linkSuffix),
  442 + ],
  443 + ),
  444 + style: const TextStyle(
  445 + color: PurchaseColors.subtitle,
  446 + fontSize: 12,
  447 + height: 1.4,
  448 + ),
  449 + ),
  450 + ),
  451 + ],
  452 + );
  453 + }
  454 +}
  1 +import 'package:doublefeel_flutter/app/modules/membership_offer/controllers/membership_offer_controller.dart';
  2 +import 'package:doublefeel_flutter/app/routes/app_pages.dart';
  3 +import 'package:doublefeel_flutter/core/services/user_state_service.dart';
1 import 'package:doublefeel_flutter/core/theme/app_theme.dart'; 4 import 'package:doublefeel_flutter/core/theme/app_theme.dart';
  5 +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
  6 +import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 7 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
  8 +import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
3 import 'package:flutter/material.dart'; 9 import 'package:flutter/material.dart';
4 import 'package:get/get.dart'; 10 import 'package:get/get.dart';
  11 +import 'package:lottie/lottie.dart';
5 12
6 import '../../user_onboarding/widget/guide_common_scaffold.dart'; 13 import '../../user_onboarding/widget/guide_common_scaffold.dart';
7 import '../../user_onboarding/widget/onboarding_common_widgets.dart'; 14 import '../../user_onboarding/widget/onboarding_common_widgets.dart';
8 -import '../../user_onboarding/widget/onboarding_illustrations.dart';  
9 -import '../controllers/membership_offer_controller.dart';  
10 15
11 class MembershipOfferView extends GetView<MembershipOfferController> { 16 class MembershipOfferView extends GetView<MembershipOfferController> {
12 const MembershipOfferView({super.key}); 17 const MembershipOfferView({super.key});
@@ -14,12 +19,14 @@ class MembershipOfferView extends GetView<MembershipOfferController> { @@ -14,12 +19,14 @@ class MembershipOfferView extends GetView<MembershipOfferController> {
14 @override 19 @override
15 Widget build(BuildContext context) { 20 Widget build(BuildContext context) {
16 final l10n = context.l10n; 21 final l10n = context.l10n;
17 -  
18 return GuideCommonScaffold( 22 return GuideCommonScaffold(
  23 + showGradientBg: true,
19 bottom: OnboardingBottomButton( 24 bottom: OnboardingBottomButton(
20 - label: l10n.continueButton, 25 + label: controller.isFromOnboard ? l10n.continueButton : '免费兑换优惠',
21 enabled: true, 26 enabled: true,
22 - onPressed: controller.finish, 27 + onPressed: () {
  28 + controller.unlock();
  29 + },
23 ), 30 ),
24 child: SingleChildScrollView( 31 child: SingleChildScrollView(
25 physics: const ClampingScrollPhysics(), 32 physics: const ClampingScrollPhysics(),
@@ -29,71 +36,129 @@ class MembershipOfferView extends GetView<MembershipOfferController> { @@ -29,71 +36,129 @@ class MembershipOfferView extends GetView<MembershipOfferController> {
29 0, 36 0,
30 140, 37 140,
31 ), 38 ),
32 - child: Column( 39 + child: Stack(
33 children: [ 40 children: [
34 - Padding(  
35 - padding: const EdgeInsets.symmetric(horizontal: 16),  
36 - child: Column(  
37 - children: [  
38 - OnboardingTitleText(l10n.onboardingMemberTitle),  
39 - const SizedBox(height: 12),  
40 - OnboardingBodyText(  
41 - l10n.onboardingMemberBody,  
42 - fontSize: 14, 41 + Lottie.asset('assets/lottie/scatter_flowers.json'),
  42 + Column(
  43 + mainAxisSize: MainAxisSize.max,
  44 + children: [
  45 + Padding(
  46 + padding: const EdgeInsets.symmetric(horizontal: 16),
  47 + child: Column(
  48 + children: [
  49 + SizedBox(height: 48),
  50 + OnboardingTitleText(l10n.onboardingMemberTitle),
  51 + const SizedBox(height: 12),
  52 + OnboardingBodyText(
  53 + l10n.onboardingMemberBody,
  54 + fontSize: 14,
  55 + ),
  56 + ],
43 ), 57 ),
44 - ],  
45 - ),  
46 - ),  
47 - const SizedBox(height: 12),  
48 - const GiftIllustration(),  
49 - const SizedBox(height: 4),  
50 - Text(  
51 - l10n.onboardingMemberOriginalPrice,  
52 - textAlign: TextAlign.center,  
53 - style: const TextStyle(  
54 - color: Color(0xFFB0B0B6),  
55 - fontSize: 16,  
56 - decoration: TextDecoration.lineThrough,  
57 - decorationColor: Color(0xFFB0B0B6),  
58 - letterSpacing: 0,  
59 - ),  
60 - ),  
61 - const SizedBox(height: 6),  
62 - Text(  
63 - l10n.onboardingMemberCurrentPrice,  
64 - textAlign: TextAlign.center,  
65 - style: TextStyle(  
66 - color: context.colors.primary,  
67 - fontSize: 28,  
68 - fontWeight: FontWeight.w600,  
69 - letterSpacing: 0,  
70 - ),  
71 - ),  
72 - const SizedBox(height: 6),  
73 - Text(  
74 - l10n.onboardingMemberMonthlyPrice,  
75 - textAlign: TextAlign.center,  
76 - style: const TextStyle(  
77 - color: Color(0xFFB0B0B6),  
78 - fontSize: 12,  
79 - letterSpacing: 0,  
80 - ),  
81 - ),  
82 - const SizedBox(height: 70),  
83 - Text(  
84 - l10n.onboardingMemberAllOptions,  
85 - textAlign: TextAlign.center,  
86 - style: TextStyle(  
87 - color: context.colors.textPrimary,  
88 - fontSize: 12,  
89 - decoration: TextDecoration.underline,  
90 - decorationColor: context.colors.textPrimary,  
91 - letterSpacing: 0,  
92 - ), 58 + ),
  59 + const SizedBox(height: 12),
  60 + Image.asset(
  61 + 'assets/images/user_onboarding/ic_membership_box.png'),
  62 + const SizedBox(height: 4),
  63 + Obx(() {
  64 + final yearInfo = controller.yearlyProductAppleInfo.value;
  65 + final yearProduct = controller.yearlyProduct.value;
  66 +
  67 + return yearInfo == null || yearProduct == null
  68 + ? const SizedBox(child: CircularProgressIndicator())
  69 + : Column(
  70 + children: [
  71 + Text(
  72 + '原价${_originDisplayPrice(yearInfo)}/年',
  73 + textAlign: TextAlign.center,
  74 + style: const TextStyle(
  75 + color: Color(0xFFB0B0B6),
  76 + fontSize: 16,
  77 + decoration: TextDecoration.lineThrough,
  78 + decorationColor: Color(0xFFB0B0B6),
  79 + letterSpacing: 0,
  80 + ),
  81 + ),
  82 + const SizedBox(height: 6),
  83 + Text(
  84 + _displayPrice(yearInfo),
  85 + textAlign: TextAlign.center,
  86 + style: TextStyle(
  87 + color: context.colors.primary,
  88 + fontSize: 28,
  89 + fontWeight: FontWeight.w600,
  90 + letterSpacing: 0,
  91 + ),
  92 + ),
  93 + const SizedBox(height: 6),
  94 + Text(
  95 + _planSubtitle(yearProduct, yearInfo),
  96 + textAlign: TextAlign.center,
  97 + style: const TextStyle(
  98 + color: Color(0xFFB0B0B6),
  99 + fontSize: 12,
  100 + letterSpacing: 0,
  101 + ),
  102 + ),
  103 + ],
  104 + );
  105 + }),
  106 + const SizedBox(height: 70),
  107 + GestureDetector(
  108 + onTap: () {
  109 + Get.offNamed(Routes.PURCHASE);
  110 + },
  111 + child: Text(
  112 + l10n.onboardingMemberAllOptions,
  113 + textAlign: TextAlign.center,
  114 + style: TextStyle(
  115 + color: context.colors.textPrimary,
  116 + fontSize: 12,
  117 + decoration: TextDecoration.underline,
  118 + decorationColor: context.colors.textPrimary,
  119 + letterSpacing: 0,
  120 + ),
  121 + ),
  122 + ),
  123 + ],
93 ), 124 ),
94 ], 125 ],
95 ), 126 ),
96 ), 127 ),
97 ); 128 );
98 } 129 }
  130 +
  131 + String _originDisplayPrice(AppleProductInfo appleInfo) {
  132 + final actual =
  133 + appleInfo.price > 0 ? appleInfo.price : appleInfo.originPrice;
  134 + final inflated = actual / 100.0 * 1.2;
  135 + // 保留两位小数,单位与 currencyCode 一致
  136 + return '${appleInfo.currencyCode}${inflated.toStringAsFixed(2)}';
  137 + }
  138 +
  139 + String _displayPrice(AppleProductInfo appleInfo) {
  140 + final price = appleInfo.price;
  141 + if (price > 0) {
  142 + final priceDescription = appleInfo.priceDescription.trim();
  143 + if (priceDescription.isNotEmpty) {
  144 + return priceDescription;
  145 + }
  146 + }
  147 + return appleInfo.originPriceDescription;
  148 + }
  149 +
  150 + String? _nonEmpty(String? value) {
  151 + final trimmed = value?.trim();
  152 + if (trimmed == null || trimmed.isEmpty) return null;
  153 + return trimmed;
  154 + }
  155 +
  156 + String _planSubtitle(PayProduct product, AppleProductInfo? appleInfo) {
  157 + if (product.content?.isYearProduct() != true) {
  158 + return _nonEmpty(product.content?.description) ?? '';
  159 + }
  160 +
  161 + final unitPrice = _nonEmpty(appleInfo?.unitPrice);
  162 + return unitPrice == null ? '' : l10n.purchaseMonthlyUnitPrice(unitPrice);
  163 + }
99 } 164 }
  1 +import 'package:doublefeel_flutter/core/network/api/user_api.dart';
1 import 'package:get/get.dart'; 2 import 'package:get/get.dart';
2 3
3 import '../controllers/user_onboarding_controller.dart'; 4 import '../controllers/user_onboarding_controller.dart';
@@ -6,7 +7,7 @@ class UserOnboardingBinding extends Bindings { @@ -6,7 +7,7 @@ class UserOnboardingBinding extends Bindings {
6 @override 7 @override
7 void dependencies() { 8 void dependencies() {
8 Get.lazyPut<UserOnboardingController>( 9 Get.lazyPut<UserOnboardingController>(
9 - () => UserOnboardingController(), 10 + () => UserOnboardingController(Get.find<UserApi>()),
10 ); 11 );
11 } 12 }
12 } 13 }
  1 +import 'package:doublefeel_flutter/core/network/api/user_api.dart';
1 import 'package:doublefeel_flutter/core/services/user_state_service.dart'; 2 import 'package:doublefeel_flutter/core/services/user_state_service.dart';
2 import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; 3 import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
3 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; 4 import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
@@ -16,6 +17,9 @@ class UserOnboardingController extends GetxController { @@ -16,6 +17,9 @@ class UserOnboardingController extends GetxController {
16 final selections = <int, Set<int>>{}.obs; 17 final selections = <int, Set<int>>{}.obs;
17 final selectionVersion = 0.obs; 18 final selectionVersion = 0.obs;
18 19
  20 + UserOnboardingController(this._userApi);
  21 + final UserApi _userApi;
  22 +
19 bool hasSelection(int pageIndex) => 23 bool hasSelection(int pageIndex) =>
20 selections[pageIndex]?.isNotEmpty ?? false; 24 selections[pageIndex]?.isNotEmpty ?? false;
21 25
@@ -106,9 +110,72 @@ class UserOnboardingController extends GetxController { @@ -106,9 +110,72 @@ class UserOnboardingController extends GetxController {
106 finishOnboarding(); 110 finishOnboarding();
107 return; 111 return;
108 } 112 }
  113 + try {
  114 + report();
  115 + } on Exception catch (e) {}
109 currentPageIndex.value++; 116 currentPageIndex.value++;
110 } 117 }
111 118
  119 + void report() {
  120 + const page1Labels = [
  121 + 'stressAnxiety', // index 0
  122 + 'tired', // index 1
  123 + 'poorRest', // index 2
  124 + 'needStimulants', // index 3
  125 + 'none', // index 4
  126 + ];
  127 + // page 2: 压力目标(单选)
  128 + const page2Labels = [
  129 + 'findSource', // index 0
  130 + 'reminder', // index 1
  131 + 'lovedOnes', // index 2
  132 + 'relax', // index 3
  133 + 'bodyTalk', // index 4
  134 + ];
  135 + // page 3: 缓解方式(多选)
  136 + const page3Labels = [
  137 + 'sleep', // index 0
  138 + 'care', // index 1
  139 + 'exercise', // index 2
  140 + 'sun', // index 3
  141 + 'water', // index 4
  142 + 'meditation', // index 5
  143 + ];
  144 +
  145 + late List<String> labels;
  146 + switch (currentPageIndex.value) {
  147 + case 1:
  148 + labels = page1Labels;
  149 + break;
  150 + case 2:
  151 + labels = page2Labels;
  152 + break;
  153 + case 3:
  154 + labels = page3Labels;
  155 + break;
  156 + }
  157 +
  158 + final selected = (selections[currentPageIndex.value] ?? {})
  159 + .map((i) => labels[i])
  160 + .toList();
  161 + var key = '';
  162 + switch (currentPageIndex.value) {
  163 + case 1:
  164 + key = 'what_happened';
  165 + break;
  166 + case 2:
  167 + key = 'want_messages';
  168 + break;
  169 + case 3:
  170 + key = 'vent_ways';
  171 + break;
  172 + }
  173 +
  174 + if (selected.isNotEmpty && key.isNotEmpty) {
  175 + _userApi.updateUserOnboarding({key: selected});
  176 + }
  177 + }
  178 +
112 Future<bool> prepareContinue(int pageIndex) async { 179 Future<bool> prepareContinue(int pageIndex) async {
113 if (!canContinue(pageIndex)) { 180 if (!canContinue(pageIndex)) {
114 return false; 181 return false;
@@ -125,6 +192,7 @@ class UserOnboardingController extends GetxController { @@ -125,6 +192,7 @@ class UserOnboardingController extends GetxController {
125 192
126 void finishOnboarding() { 193 void finishOnboarding() {
127 // 标记当前账号引导已全部完成 194 // 标记当前账号引导已全部完成
  195 + _userApi.updateOwnerUserInfo(isPassNoviceGuide: 1);
128 final userId = 196 final userId =
129 Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ?? 197 Get.find<UserPreferencesStorage>().preferences.value.meUserInfo?.id ??
130 0; 198 0;
1 -import 'package:flutter/material.dart';  
2 -  
3 class OnboardingOptionData { 1 class OnboardingOptionData {
4 const OnboardingOptionData({ 2 const OnboardingOptionData({
5 required this.label, 3 required this.label,
6 - required this.icon,  
7 - required this.iconColor, 4 + required this.iconPath,
8 this.exclusive = false, 5 this.exclusive = false,
9 }); 6 });
10 7
11 final String label; 8 final String label;
12 - final IconData icon;  
13 - final Color iconColor; 9 + final String iconPath;
14 final bool exclusive; 10 final bool exclusive;
15 } 11 }
@@ -25,28 +25,23 @@ List<Widget> buildUserOnboardingPages(BuildContext context, @@ -25,28 +25,23 @@ List<Widget> buildUserOnboardingPages(BuildContext context,
25 options: [ 25 options: [
26 OnboardingOptionData( 26 OnboardingOptionData(
27 label: l10n.onboardingStateStressAnxiety, 27 label: l10n.onboardingStateStressAnxiety,
28 - icon: Icons.sentiment_very_dissatisfied_rounded,  
29 - iconColor: const Color(0xFFFF985D), 28 + iconPath: 'assets/images/user_onboarding/ic_stressful.png',
30 ), 29 ),
31 OnboardingOptionData( 30 OnboardingOptionData(
32 label: l10n.onboardingStateTired, 31 label: l10n.onboardingStateTired,
33 - icon: Icons.battery_2_bar_rounded,  
34 - iconColor: const Color(0xFFFFC85C), 32 + iconPath: 'assets/images/user_onboarding/ic_tire.png',
35 ), 33 ),
36 OnboardingOptionData( 34 OnboardingOptionData(
37 label: l10n.onboardingStatePoorRest, 35 label: l10n.onboardingStatePoorRest,
38 - icon: Icons.bedtime_rounded,  
39 - iconColor: const Color(0xFF45C89F), 36 + iconPath: 'assets/images/user_onboarding/ic_restless.png',
40 ), 37 ),
41 OnboardingOptionData( 38 OnboardingOptionData(
42 label: l10n.onboardingStateNeedStimulants, 39 label: l10n.onboardingStateNeedStimulants,
43 - icon: Icons.local_cafe_rounded,  
44 - iconColor: const Color(0xFFFF8AB6), 40 + iconPath: 'assets/images/user_onboarding/ic_wine.png',
45 ), 41 ),
46 OnboardingOptionData( 42 OnboardingOptionData(
47 label: l10n.onboardingStateNone, 43 label: l10n.onboardingStateNone,
48 - icon: Icons.more_horiz_rounded,  
49 - iconColor: context.colors.primary, 44 + iconPath: 'assets/images/user_onboarding/ic_none.png',
50 exclusive: true, 45 exclusive: true,
51 ), 46 ),
52 ], 47 ],
@@ -58,28 +53,23 @@ List<Widget> buildUserOnboardingPages(BuildContext context, @@ -58,28 +53,23 @@ List<Widget> buildUserOnboardingPages(BuildContext context,
58 options: [ 53 options: [
59 OnboardingOptionData( 54 OnboardingOptionData(
60 label: l10n.onboardingStressGoalSource, 55 label: l10n.onboardingStressGoalSource,
61 - icon: Icons.radar_rounded,  
62 - iconColor: const Color(0xFF6D9DFF), 56 + iconPath: 'assets/images/user_onboarding/ic_target.png',
63 ), 57 ),
64 OnboardingOptionData( 58 OnboardingOptionData(
65 label: l10n.onboardingStressGoalReminder, 59 label: l10n.onboardingStressGoalReminder,
66 - icon: Icons.notifications_rounded,  
67 - iconColor: const Color(0xFFFFA63F), 60 + iconPath: 'assets/images/user_onboarding/ic_alarm.png',
68 ), 61 ),
69 OnboardingOptionData( 62 OnboardingOptionData(
70 label: l10n.onboardingStressGoalLovedOnes, 63 label: l10n.onboardingStressGoalLovedOnes,
71 - icon: Icons.favorite_rounded,  
72 - iconColor: const Color(0xFFFF72A9), 64 + iconPath: 'assets/images/user_onboarding/ic_heart.png',
73 ), 65 ),
74 OnboardingOptionData( 66 OnboardingOptionData(
75 label: l10n.onboardingStressGoalRelax, 67 label: l10n.onboardingStressGoalRelax,
76 - icon: Icons.self_improvement_rounded,  
77 - iconColor: const Color(0xFFFFC158), 68 + iconPath: 'assets/images/user_onboarding/ic_relax.png',
78 ), 69 ),
79 OnboardingOptionData( 70 OnboardingOptionData(
80 label: l10n.onboardingStressGoalBodyTalk, 71 label: l10n.onboardingStressGoalBodyTalk,
81 - icon: Icons.accessibility_new_rounded,  
82 - iconColor: const Color(0xFFFFA24E), 72 + iconPath: 'assets/images/user_onboarding/ic_health_green.png',
83 ), 73 ),
84 ], 74 ],
85 ), 75 ),
@@ -90,33 +80,27 @@ List<Widget> buildUserOnboardingPages(BuildContext context, @@ -90,33 +80,27 @@ List<Widget> buildUserOnboardingPages(BuildContext context,
90 options: [ 80 options: [
91 OnboardingOptionData( 81 OnboardingOptionData(
92 label: l10n.onboardingReliefSleep, 82 label: l10n.onboardingReliefSleep,
93 - icon: Icons.nightlight_round,  
94 - iconColor: const Color(0xFF8C6DFF), 83 + iconPath: 'assets/images/user_onboarding/ic_sleep.png',
95 ), 84 ),
96 OnboardingOptionData( 85 OnboardingOptionData(
97 label: l10n.onboardingReliefCare, 86 label: l10n.onboardingReliefCare,
98 - icon: Icons.volunteer_activism_rounded,  
99 - iconColor: const Color(0xFFFF72B6), 87 + iconPath: 'assets/images/user_onboarding/ic_care.png',
100 ), 88 ),
101 OnboardingOptionData( 89 OnboardingOptionData(
102 label: l10n.onboardingReliefExercise, 90 label: l10n.onboardingReliefExercise,
103 - icon: Icons.fitness_center_rounded,  
104 - iconColor: const Color(0xFF7ACD74), 91 + iconPath: 'assets/images/user_onboarding/ic_step.png',
105 ), 92 ),
106 OnboardingOptionData( 93 OnboardingOptionData(
107 label: l10n.onboardingReliefSun, 94 label: l10n.onboardingReliefSun,
108 - icon: Icons.wb_sunny_rounded,  
109 - iconColor: const Color(0xFFFFC13D), 95 + iconPath: 'assets/images/user_onboarding/ic_sun.png',
110 ), 96 ),
111 OnboardingOptionData( 97 OnboardingOptionData(
112 label: l10n.onboardingReliefWater, 98 label: l10n.onboardingReliefWater,
113 - icon: Icons.water_drop_rounded,  
114 - iconColor: const Color(0xFF76A9FF), 99 + iconPath: 'assets/images/user_onboarding/ic_water.png',
115 ), 100 ),
116 OnboardingOptionData( 101 OnboardingOptionData(
117 label: l10n.onboardingReliefMeditation, 102 label: l10n.onboardingReliefMeditation,
118 - icon: Icons.spa_rounded,  
119 - iconColor: const Color(0xFF8A78F6), 103 + iconPath: 'assets/images/user_onboarding/ic_meditation.png',
120 ), 104 ),
121 ], 105 ],
122 ), 106 ),
@@ -152,13 +136,10 @@ class _IntroPage extends StatelessWidget { @@ -152,13 +136,10 @@ class _IntroPage extends StatelessWidget {
152 children: [ 136 children: [
153 OnboardingTitleText(title), 137 OnboardingTitleText(title),
154 const SizedBox(height: 12), 138 const SizedBox(height: 12),
155 - Opacity(  
156 - opacity: 0.30,  
157 - child: Container(  
158 - width: 375,  
159 - height: 384,  
160 - decoration: BoxDecoration(color: const Color(0xFFFF0000)),  
161 - ), 139 + Image.asset(
  140 + 'assets/images/user_onboarding/bg_onboarding_watch.png',
  141 + height: 384,
  142 + fit: BoxFit.fitHeight,
162 ), 143 ),
163 const SizedBox(height: 12), 144 const SizedBox(height: 12),
164 OnboardingEmRichText( 145 OnboardingEmRichText(
@@ -271,20 +252,20 @@ class _HrvIntroPage extends StatelessWidget { @@ -271,20 +252,20 @@ class _HrvIntroPage extends StatelessWidget {
271 '${l10n.onboardingHrvTitle}\n${l10n.onboardingHrvSubtitle}', 252 '${l10n.onboardingHrvTitle}\n${l10n.onboardingHrvSubtitle}',
272 maxWidth: 320, 253 maxWidth: 320,
273 ), 254 ),
274 - const SizedBox(height: 4), 255 + const SizedBox(height: 57),
275 Container( 256 Container(
276 - height: 393, 257 + height: 210,
277 decoration: 258 decoration:
278 BoxDecoration(color: const Color(0xFFFF0000).withAlpha(120)), 259 BoxDecoration(color: const Color(0xFFFF0000).withAlpha(120)),
279 ), 260 ),
280 - const SizedBox(height: 17), 261 + const SizedBox(height: 31),
281 Padding( 262 Padding(
282 padding: const EdgeInsets.symmetric(horizontal: 16), 263 padding: const EdgeInsets.symmetric(horizontal: 16),
283 child: Text( 264 child: Text(
284 l10n.onboardingHrvDescription, 265 l10n.onboardingHrvDescription,
285 textAlign: TextAlign.center, 266 textAlign: TextAlign.center,
286 - style: const TextStyle(  
287 - color: Color(0xFFA0A3BC), 267 + style: TextStyle(
  268 + color: context.colors.textSecondary,
288 fontSize: 14, 269 fontSize: 14,
289 fontWeight: FontWeight.w400, 270 fontWeight: FontWeight.w400,
290 height: 1.35, 271 height: 1.35,
@@ -365,18 +346,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> { @@ -365,18 +346,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> {
365 leftCard: ResearchCard( 346 leftCard: ResearchCard(
366 title: l10n.onboardingResearchFatigue, 347 title: l10n.onboardingResearchFatigue,
367 isHRVup: false, 348 isHRVup: false,
368 - backgroundColor: const Color(0xFFFFE2E3),  
369 - height: 320,  
370 - image: 'assets/images/onboarding/research_fatigue.png', 349 + backgroundColor: const Color(0xFFFFDBEF),
  350 + image: 'assets/images/user_onboarding/bg_hrv_down_1.png',
371 ), 351 ),
372 rightCard: Padding( 352 rightCard: Padding(
373 padding: const EdgeInsets.only(bottom: 41), 353 padding: const EdgeInsets.only(bottom: 41),
374 child: ResearchCard( 354 child: ResearchCard(
375 title: l10n.onboardingResearchEnergy, 355 title: l10n.onboardingResearchEnergy,
376 isHRVup: true, 356 isHRVup: true,
377 - backgroundColor: const Color(0xFFD6F2E5),  
378 - height: 320,  
379 - image: 'assets/images/onboarding/research_energy.png', 357 + backgroundColor: const Color(0xFFC9EADE),
  358 + image: 'assets/images/user_onboarding/bg_hrv_up_1.png',
380 ), 359 ),
381 ), 360 ),
382 ), 361 ),
@@ -384,18 +363,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> { @@ -384,18 +363,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> {
384 leftCard: ResearchCard( 363 leftCard: ResearchCard(
385 title: l10n.onboardingResearchStress, 364 title: l10n.onboardingResearchStress,
386 isHRVup: false, 365 isHRVup: false,
387 - backgroundColor: const Color(0xFFFFE2E3),  
388 - height: 320,  
389 - image: 'assets/images/onboarding/research_fatigue.png', 366 + backgroundColor: const Color(0xFFFFDBEF),
  367 + image: 'assets/images/user_onboarding/bg_hrv_down_2.png',
390 ), 368 ),
391 rightCard: Padding( 369 rightCard: Padding(
392 padding: const EdgeInsets.only(bottom: 41), 370 padding: const EdgeInsets.only(bottom: 41),
393 child: ResearchCard( 371 child: ResearchCard(
394 title: l10n.onboardingResearchRelaxed, 372 title: l10n.onboardingResearchRelaxed,
395 isHRVup: true, 373 isHRVup: true,
396 - backgroundColor: const Color(0xFFD6F2E5),  
397 - height: 320,  
398 - image: 'assets/images/onboarding/research_energy.png', 374 + backgroundColor: const Color(0xFFC9EADE),
  375 + image: 'assets/images/user_onboarding/bg_hrv_up_2.png',
399 ), 376 ),
400 ), 377 ),
401 ), 378 ),
@@ -403,18 +380,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> { @@ -403,18 +380,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> {
403 leftCard: ResearchCard( 380 leftCard: ResearchCard(
404 title: l10n.onboardingResearchSick, 381 title: l10n.onboardingResearchSick,
405 isHRVup: false, 382 isHRVup: false,
406 - backgroundColor: const Color(0xFFFFE2E3),  
407 - height: 320,  
408 - image: 'assets/images/onboarding/research_fatigue.png', 383 + backgroundColor: const Color(0xFFFFDBEF),
  384 + image: 'assets/images/user_onboarding/bg_hrv_down_3.png',
409 ), 385 ),
410 rightCard: Padding( 386 rightCard: Padding(
411 padding: const EdgeInsets.only(bottom: 41), 387 padding: const EdgeInsets.only(bottom: 41),
412 child: ResearchCard( 388 child: ResearchCard(
413 title: l10n.onboardingResearchHealthy, 389 title: l10n.onboardingResearchHealthy,
414 isHRVup: true, 390 isHRVup: true,
415 - backgroundColor: const Color(0xFFD6F2E5),  
416 - height: 320,  
417 - image: 'assets/images/onboarding/research_energy.png', 391 + backgroundColor: const Color(0xFFC9EADE),
  392 + image: 'assets/images/user_onboarding/bg_hrv_up_3.png',
418 ), 393 ),
419 ), 394 ),
420 ), 395 ),
@@ -422,18 +397,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> { @@ -422,18 +397,16 @@ class _HrvResearchPageState extends State<_HrvResearchPage> {
422 leftCard: ResearchCard( 397 leftCard: ResearchCard(
423 title: l10n.onboardingResearchPoorSleep, 398 title: l10n.onboardingResearchPoorSleep,
424 isHRVup: false, 399 isHRVup: false,
425 - backgroundColor: const Color(0xFFFFE2E3),  
426 - height: 320,  
427 - image: 'assets/images/onboarding/research_fatigue.png', 400 + backgroundColor: const Color(0xFFFFDBEF),
  401 + image: 'assets/images/user_onboarding/bg_hrv_down_4.png',
428 ), 402 ),
429 rightCard: Padding( 403 rightCard: Padding(
430 padding: const EdgeInsets.only(bottom: 41), 404 padding: const EdgeInsets.only(bottom: 41),
431 child: ResearchCard( 405 child: ResearchCard(
432 title: l10n.onboardingResearchGoodSleep, 406 title: l10n.onboardingResearchGoodSleep,
433 isHRVup: true, 407 isHRVup: true,
434 - backgroundColor: const Color(0xFFD6F2E5),  
435 - height: 320,  
436 - image: 'assets/images/onboarding/research_energy.png', 408 + backgroundColor: const Color(0xFFC9EADE),
  409 + image: 'assets/images/user_onboarding/bg_hrv_up_4.png',
437 ), 410 ),
438 ), 411 ),
439 ), 412 ),
@@ -543,14 +516,13 @@ class HealthPermissionPage extends StatelessWidget { @@ -543,14 +516,13 @@ class HealthPermissionPage extends StatelessWidget {
543 fontSize: 14, 516 fontSize: 14,
544 ), 517 ),
545 ), 518 ),
546 - const SizedBox(height: 58),  
547 - Container(  
548 - height: 383,  
549 - width: 280,  
550 - decoration:  
551 - BoxDecoration(color: const Color(0xFFFF0000).withAlpha(120)), 519 + const SizedBox(height: 28),
  520 + Image.asset(
  521 + 'assets/images/permission/apple_open_health_kit_cn.png',
  522 + height: 295,
  523 + fit: BoxFit.fitHeight,
552 ), 524 ),
553 - const SizedBox(height: 8), 525 + const SizedBox(height: 16),
554 Padding( 526 Padding(
555 padding: const EdgeInsets.symmetric(horizontal: 34), 527 padding: const EdgeInsets.symmetric(horizontal: 34),
556 child: Text( 528 child: Text(
@@ -594,21 +566,13 @@ class _NotificationPermissionPage extends StatelessWidget { @@ -594,21 +566,13 @@ class _NotificationPermissionPage extends StatelessWidget {
594 ), 566 ),
595 ), 567 ),
596 const SizedBox(height: 20), 568 const SizedBox(height: 20),
597 -  
598 - Container( 569 + Image.asset(
  570 + 'assets/images/user_onboarding/bg_onboarding_notification.png',
599 height: 383, 571 height: 383,
600 - decoration:  
601 - BoxDecoration(color: const Color(0xFFFF0000).withAlpha(120)), 572 + fit: BoxFit.fitHeight,
602 ), 573 ),
603 -  
604 - // const SizedBox(height: 100),  
605 - // const OnboardingMotionPlaceholder(  
606 - // icon: Icons.notifications_active_rounded,  
607 - // height: 250,  
608 - // ),  
609 ], 574 ],
610 ), 575 ),
611 ); 576 );
612 } 577 }
613 } 578 }
614 -  
@@ -9,12 +9,14 @@ class GuideCommonScaffold extends StatelessWidget { @@ -9,12 +9,14 @@ class GuideCommonScaffold extends StatelessWidget {
9 this.bottom, 9 this.bottom,
10 this.actions = const [], 10 this.actions = const [],
11 this.onBackPressed, 11 this.onBackPressed,
  12 + this.showGradientBg = false,
12 }); 13 });
13 14
14 final Widget child; 15 final Widget child;
15 final Widget? bottom; 16 final Widget? bottom;
16 final List<Widget> actions; 17 final List<Widget> actions;
17 final VoidCallback? onBackPressed; 18 final VoidCallback? onBackPressed;
  19 + final bool showGradientBg;
18 20
19 @override 21 @override
20 Widget build(BuildContext context) { 22 Widget build(BuildContext context) {
@@ -29,7 +31,6 @@ class GuideCommonScaffold extends StatelessWidget { @@ -29,7 +31,6 @@ class GuideCommonScaffold extends StatelessWidget {
29 systemNavigationBarIconBrightness: Brightness.dark, 31 systemNavigationBarIconBrightness: Brightness.dark,
30 ), 32 ),
31 child: Scaffold( 33 child: Scaffold(
32 - backgroundColor: Colors.white,  
33 extendBodyBehindAppBar: true, 34 extendBodyBehindAppBar: true,
34 appBar: AppBar( 35 appBar: AppBar(
35 backgroundColor: Colors.transparent, 36 backgroundColor: Colors.transparent,
@@ -57,23 +58,24 @@ class GuideCommonScaffold extends StatelessWidget { @@ -57,23 +58,24 @@ class GuideCommonScaffold extends StatelessWidget {
57 body: Stack( 58 body: Stack(
58 fit: StackFit.expand, 59 fit: StackFit.expand,
59 children: [ 60 children: [
60 - const DecoratedBox(  
61 - decoration: BoxDecoration(  
62 - gradient: LinearGradient(  
63 - begin: Alignment(0.50, -0.00),  
64 - end: Alignment(0.50, 1.00), 61 + if (showGradientBg)
  62 + const DecoratedBox(
  63 + decoration: BoxDecoration(
  64 + gradient: LinearGradient(
  65 + begin: Alignment(0.50, -0.00),
  66 + end: Alignment(0.50, 1.00),
65 67
66 - colors: [  
67 - const Color(0xFFE6DAFF),  
68 - const Color(0xFFECE4FF),  
69 - const Color(0xFFEEE6FF),  
70 - const Color(0xFFF8F6FF)  
71 - ], 68 + colors: [
  69 + const Color(0xFFE6DAFF),
  70 + const Color(0xFFECE4FF),
  71 + const Color(0xFFEEE6FF),
  72 + const Color(0xFFF8F6FF)
  73 + ],
72 74
73 - // stops: [0, 0.33173, 0.74038, 1], 75 + // stops: [0, 0.33173, 0.74038, 1],
  76 + ),
74 ), 77 ),
75 ), 78 ),
76 - ),  
77 child, 79 child,
78 if (bottom != null) 80 if (bottom != null)
79 Positioned( 81 Positioned(
@@ -68,7 +68,7 @@ class OnboardingEmRichText extends StatelessWidget { @@ -68,7 +68,7 @@ class OnboardingEmRichText extends StatelessWidget {
68 final normalStyle = const TextStyle( 68 final normalStyle = const TextStyle(
69 color: _normalColor, 69 color: _normalColor,
70 fontSize: fontSize, 70 fontSize: fontSize,
71 - fontWeight: FontWeight.w400, 71 + fontWeight: FontWeight.w600,
72 ); 72 );
73 final emphasisStyle = const TextStyle( 73 final emphasisStyle = const TextStyle(
74 color: _emphasisColor, 74 color: _emphasisColor,
@@ -2,64 +2,12 @@ import 'package:doublefeel_flutter/core/theme/app_theme.dart'; @@ -2,64 +2,12 @@ import 'package:doublefeel_flutter/core/theme/app_theme.dart';
2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; 2 import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
3 import 'package:flutter/material.dart'; 3 import 'package:flutter/material.dart';
4 4
5 -class OnboardingMotionPlaceholder extends StatelessWidget {  
6 - const OnboardingMotionPlaceholder({  
7 - super.key,  
8 - required this.icon,  
9 - required this.height,  
10 - });  
11 -  
12 - final IconData icon;  
13 - final double height;  
14 -  
15 - @override  
16 - Widget build(BuildContext context) {  
17 - return SizedBox(  
18 - height: height,  
19 - child: Center(  
20 - child: TweenAnimationBuilder<double>(  
21 - tween: Tween(begin: 0.96, end: 1),  
22 - duration: const Duration(milliseconds: 900),  
23 - curve: Curves.easeInOut,  
24 - builder: (context, scale, child) {  
25 - return Transform.scale(scale: scale, child: child);  
26 - },  
27 - child: Container(  
28 - width: 170,  
29 - height: 170,  
30 - decoration: BoxDecoration(  
31 - color: Colors.white.withValues(alpha: 0.42),  
32 - borderRadius: BorderRadius.circular(32),  
33 - border: Border.all(  
34 - color: Colors.white.withValues(alpha: 0.6),  
35 - ),  
36 - boxShadow: [  
37 - BoxShadow(  
38 - color: context.colors.primary.withValues(alpha: 0.1),  
39 - blurRadius: 26,  
40 - offset: const Offset(0, 12),  
41 - ),  
42 - ],  
43 - ),  
44 - child: Icon(  
45 - icon,  
46 - color: context.colors.primary,  
47 - size: 72,  
48 - ),  
49 - ),  
50 - ),  
51 - ),  
52 - );  
53 - }  
54 -}  
55 -  
56 class ResearchCard extends StatelessWidget { 5 class ResearchCard extends StatelessWidget {
57 const ResearchCard({ 6 const ResearchCard({
58 super.key, 7 super.key,
59 required this.title, 8 required this.title,
60 required this.isHRVup, 9 required this.isHRVup,
61 required this.backgroundColor, 10 required this.backgroundColor,
62 - required this.height,  
63 required this.image, 11 required this.image,
64 }); 12 });
65 13
@@ -67,20 +15,20 @@ class ResearchCard extends StatelessWidget { @@ -67,20 +15,20 @@ class ResearchCard extends StatelessWidget {
67 final bool isHRVup; 15 final bool isHRVup;
68 16
69 final Color backgroundColor; 17 final Color backgroundColor;
70 - final double height;  
71 final String image; 18 final String image;
72 @override 19 @override
73 Widget build(BuildContext context) { 20 Widget build(BuildContext context) {
74 return Container( 21 return Container(
75 - height: height, 22 + height: 320,
  23 + width: 171,
76 decoration: BoxDecoration( 24 decoration: BoxDecoration(
77 color: backgroundColor, 25 color: backgroundColor,
78 borderRadius: BorderRadius.circular(32), 26 borderRadius: BorderRadius.circular(32),
  27 + image: DecorationImage(image: AssetImage(image), fit: BoxFit.fill),
79 ), 28 ),
80 padding: const EdgeInsets.fromLTRB(12, 24, 12, 28), 29 padding: const EdgeInsets.fromLTRB(12, 24, 12, 28),
81 child: Stack( 30 child: Stack(
82 children: [ 31 children: [
83 - Image.asset(image),  
84 Positioned( 32 Positioned(
85 top: 0, 33 top: 0,
86 left: 0, 34 left: 0,
@@ -126,89 +74,3 @@ class ResearchCard extends StatelessWidget { @@ -126,89 +74,3 @@ class ResearchCard extends StatelessWidget {
126 ); 74 );
127 } 75 }
128 } 76 }
129 -  
130 -class GiftIllustration extends StatelessWidget {  
131 - const GiftIllustration({super.key});  
132 -  
133 - @override  
134 - Widget build(BuildContext context) {  
135 - return Container(  
136 - height: 332,  
137 - width: double.infinity,  
138 - color: const Color(0xFFF17F96),  
139 - child: Center(  
140 - child: SizedBox(  
141 - width: 240,  
142 - height: 240,  
143 - child: Stack(  
144 - alignment: Alignment.center,  
145 - children: [  
146 - Positioned(  
147 - top: 36,  
148 - child: Container(  
149 - width: 190,  
150 - height: 52,  
151 - decoration: BoxDecoration(  
152 - color: const Color(0xFFFF4F21),  
153 - borderRadius: BorderRadius.circular(8),  
154 - ),  
155 - ),  
156 - ),  
157 - Positioned(  
158 - top: 76,  
159 - child: Container(  
160 - width: 184,  
161 - height: 132,  
162 - decoration: BoxDecoration(  
163 - color: const Color(0xFFFF4F21),  
164 - borderRadius: BorderRadius.circular(12),  
165 - ),  
166 - ),  
167 - ),  
168 - Positioned(  
169 - top: 72,  
170 - bottom: 28,  
171 - child: Container(  
172 - width: 12,  
173 - color: const Color(0xFFFF8D1A),  
174 - ),  
175 - ),  
176 - Positioned(  
177 - top: 44,  
178 - child: Transform.rotate(  
179 - angle: -0.08,  
180 - child: Container(  
181 - width: 168,  
182 - height: 100,  
183 - alignment: Alignment.center,  
184 - decoration: BoxDecoration(  
185 - color: const Color(0xFFB538A6),  
186 - borderRadius: BorderRadius.circular(8),  
187 - ),  
188 - child: const Text(  
189 - '50%',  
190 - style: TextStyle(  
191 - color: Color(0xFFFFC38E),  
192 - fontSize: 56,  
193 - fontWeight: FontWeight.w800,  
194 - letterSpacing: 0,  
195 - ),  
196 - ),  
197 - ),  
198 - ),  
199 - ),  
200 - const Positioned(  
201 - top: 0,  
202 - child: Icon(  
203 - Icons.card_giftcard_rounded,  
204 - color: Color(0xFFFF8D1A),  
205 - size: 84,  
206 - ),  
207 - ),  
208 - ],  
209 - ),  
210 - ),  
211 - ),  
212 - );  
213 - }  
214 -}  
@@ -82,18 +82,10 @@ class _OptionIcon extends StatelessWidget { @@ -82,18 +82,10 @@ class _OptionIcon extends StatelessWidget {
82 82
83 @override 83 @override
84 Widget build(BuildContext context) { 84 Widget build(BuildContext context) {
85 - return Container( 85 + return Image.asset(
  86 + data.iconPath,
86 width: 48, 87 width: 48,
87 height: 48, 88 height: 48,
88 - decoration: const BoxDecoration(  
89 - shape: BoxShape.circle,  
90 - color: Color(0xFFF0F0F6),  
91 - ),  
92 - child: Icon(  
93 - data.icon,  
94 - color: data.iconColor,  
95 - size: 24,  
96 - ),  
97 ); 89 );
98 } 90 }
99 } 91 }
1 import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart'; 1 import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart';
2 import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart'; 2 import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart';
  3 +import 'package:doublefeel_flutter/app/modules/membership_offer/bindings/membership_detail_binding.dart';
  4 +import 'package:doublefeel_flutter/app/modules/membership_offer/views/membership_detail_view.dart';
3 import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; 5 import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
4 6
5 import 'package:get/get.dart'; 7 import 'package:get/get.dart';
@@ -114,6 +116,11 @@ abstract final class AppPages { @@ -114,6 +116,11 @@ abstract final class AppPages {
114 binding: MembershipOfferBinding(), 116 binding: MembershipOfferBinding(),
115 ), 117 ),
116 GetPage( 118 GetPage(
  119 + name: Routes.MEMBERSHIP_DETAIL,
  120 + page: () => const MembershipDetailView(),
  121 + binding: MembershipDetailBinding(),
  122 + ),
  123 + GetPage(
117 name: AppRoutes.bindPartner, 124 name: AppRoutes.bindPartner,
118 page: () => const BindPartnerView(), 125 page: () => const BindPartnerView(),
119 binding: BindPartnerBinding(), 126 binding: BindPartnerBinding(),
@@ -5,6 +5,7 @@ abstract class Routes { @@ -5,6 +5,7 @@ abstract class Routes {
5 Routes._(); 5 Routes._();
6 static const USER_ONBOARDING = _Paths.USER_ONBOARDING; 6 static const USER_ONBOARDING = _Paths.USER_ONBOARDING;
7 static const MEMBERSHIP_OFFER = _Paths.MEMBERSHIP_OFFER; 7 static const MEMBERSHIP_OFFER = _Paths.MEMBERSHIP_OFFER;
  8 + static const MEMBERSHIP_DETAIL = _Paths.MEMBERSHIP_DETAIL;
8 static const LOGIN = _Paths.LOGIN; 9 static const LOGIN = _Paths.LOGIN;
9 static const DEVELOPER_OPTIONS = _Paths.DEVELOPER_OPTIONS; 10 static const DEVELOPER_OPTIONS = _Paths.DEVELOPER_OPTIONS;
10 static const USER_INFORMATION = _Paths.USER_INFORMATION; 11 static const USER_INFORMATION = _Paths.USER_INFORMATION;
@@ -31,6 +32,7 @@ abstract class _Paths { @@ -31,6 +32,7 @@ abstract class _Paths {
31 _Paths._(); 32 _Paths._();
32 static const USER_ONBOARDING = '/user-onboarding'; 33 static const USER_ONBOARDING = '/user-onboarding';
33 static const MEMBERSHIP_OFFER = '/membership-offer'; 34 static const MEMBERSHIP_OFFER = '/membership-offer';
  35 + static const MEMBERSHIP_DETAIL = '/membership-detail';
34 static const LOGIN = '/login'; 36 static const LOGIN = '/login';
35 static const DEVELOPER_OPTIONS = '/developer-options'; 37 static const DEVELOPER_OPTIONS = '/developer-options';
36 static const USER_INFORMATION = '/developer-options/user-information'; 38 static const USER_INFORMATION = '/developer-options/user-information';
  1 +import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
  2 +
1 import '../../../data/models/friend/friend_models.dart'; 3 import '../../../data/models/friend/friend_models.dart';
2 import '../../result/app_result.dart'; 4 import '../../result/app_result.dart';
3 import '../../result/safe_call.dart'; 5 import '../../result/safe_call.dart';
@@ -20,6 +22,22 @@ class FriendApi { @@ -20,6 +22,22 @@ class FriendApi {
20 ); 22 );
21 } 23 }
22 24
  25 + Future<AppResult<FriendItem>> addFriendHandleError(
  26 + String uniqueCode, HttpErrorHandlingPolicy policy) {
  27 + return safeCall(
  28 + call: () async {
  29 + final response = await _dioClient.dio.post(
  30 + ApiPaths.friends,
  31 + data: {'unique_code': uniqueCode},
  32 + );
  33 + return FriendItem.fromJson(
  34 + response.data as Map<String, dynamic>,
  35 + );
  36 + },
  37 + errorHandlingPolicy: policy,
  38 + );
  39 + }
  40 +
23 Future<AppResult<void>> deleteFriend(int friendUserId) { 41 Future<AppResult<void>> deleteFriend(int friendUserId) {
24 return safeCall( 42 return safeCall(
25 call: () async { 43 call: () async {
@@ -147,12 +147,12 @@ class HealthApi { @@ -147,12 +147,12 @@ class HealthApi {
147 } 147 }
148 148
149 Future<AppResult<SleepStatisticsData>> getSleepStatistics( 149 Future<AppResult<SleepStatisticsData>> getSleepStatistics(
150 - int dateRangeType,  
151 - int startDate, {  
152 - int? queryUserId,  
153 - HttpErrorHandlingPolicy? errorHandlingPolicy =  
154 - HttpErrorHandlingPolicy.defaultPolicy,  
155 - }) { 150 + int dateRangeType,
  151 + int startDate, {
  152 + int? queryUserId,
  153 + HttpErrorHandlingPolicy? errorHandlingPolicy =
  154 + HttpErrorHandlingPolicy.defaultPolicy,
  155 + }) {
156 return safeCall( 156 return safeCall(
157 call: () async { 157 call: () async {
158 final response = await _dioClient.dio.get( 158 final response = await _dioClient.dio.get(
@@ -345,8 +345,17 @@ class HealthApi { @@ -345,8 +345,17 @@ class HealthApi {
345 .get(ApiPaths.v2ActivityTarget, queryParameters: { 345 .get(ApiPaths.v2ActivityTarget, queryParameters: {
346 if (friendUserId != null) 'query_user_id': friendUserId, 346 if (friendUserId != null) 'query_user_id': friendUserId,
347 }); 347 });
348 - return V2ActivityTarget.fromJson(  
349 - response.data as Map<String, dynamic>); 348 + return V2ActivityTarget.fromJson(response.data as Map<String, dynamic>);
  349 + },
  350 + errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
  351 + );
  352 + }
  353 +
  354 + Future<AppResult<bool>> getHealthDataEverUploaded() {
  355 + return safeCall(
  356 + call: () async {
  357 + final response = await _dioClient.dio.get(ApiPaths.weatherUploadedData);
  358 + return response.data['flag'];
350 }, 359 },
351 errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy, 360 errorHandlingPolicy: HttpErrorHandlingPolicy.defaultPolicy,
352 ); 361 );
@@ -135,28 +135,42 @@ class UserApi { @@ -135,28 +135,42 @@ class UserApi {
135 ); 135 );
136 } 136 }
137 137
138 - Future<AppResult<UserInfoResponse>> updateOwnerUserInfo({  
139 - String? avatar,  
140 - String? nickname,  
141 - int? persona,  
142 - int? enableAddWithUcode  
143 - }) { 138 + Future<AppResult<UserInfoResponse>> updateOwnerUserInfo(
  139 + {String? avatar,
  140 + String? nickname,
  141 + int? persona,
  142 + int? enableAddWithUcode,
  143 + int? isPassNoviceGuide}) {
144 return safeCall( 144 return safeCall(
145 call: () async { 145 call: () async {
146 final response = await _dioClient.dio.put( 146 final response = await _dioClient.dio.put(
147 ApiPaths.userInfo, 147 ApiPaths.userInfo,
148 data: UserInfoUpdateRequest( 148 data: UserInfoUpdateRequest(
149 - avatar: avatar,  
150 - nickname: nickname,  
151 - persona: persona,  
152 - enableAddWithUcode: enableAddWithUcode  
153 - ).toJson(), 149 + avatar: avatar,
  150 + nickname: nickname,
  151 + persona: persona,
  152 + enableAddWithUcode: enableAddWithUcode,
  153 + isPassNoviceGuide: isPassNoviceGuide)
  154 + .toJson(),
154 ); 155 );
155 return UserInfoResponse.fromJson(response.data as Map<String, dynamic>); 156 return UserInfoResponse.fromJson(response.data as Map<String, dynamic>);
156 }, 157 },
157 ); 158 );
158 } 159 }
159 160
  161 + Future<AppResult<void>> updateUserOnboarding(
  162 + Object? data,
  163 + ) {
  164 + return safeCall(
  165 + call: () async {
  166 + await _dioClient.dio.put(
  167 + ApiPaths.userInfo,
  168 + data: data,
  169 + );
  170 + },
  171 + );
  172 + }
  173 +
160 Future<AppResult<void>> bindPartner(String bindCode) { 174 Future<AppResult<void>> bindPartner(String bindCode) {
161 return safeCall( 175 return safeCall(
162 call: () async { 176 call: () async {
@@ -23,7 +23,8 @@ abstract final class ApiPaths { @@ -23,7 +23,8 @@ abstract final class ApiPaths {
23 '/client/doublefeel/health/data_upload/sleep/'; 23 '/client/doublefeel/health/data_upload/sleep/';
24 static const healthActivityTarget = 24 static const healthActivityTarget =
25 '/client/doublefeel/health/activity_target/'; 25 '/client/doublefeel/health/activity_target/';
26 - static const healthStatsSleep = '/client/doublefeel/health/v2/statistics/sleep/'; 26 + static const healthStatsSleep =
  27 + '/client/doublefeel/health/v2/statistics/sleep/';
27 static const healthStatsActivity = 28 static const healthStatsActivity =
28 '/client/doublefeel/health/v2/statistics/activity/'; 29 '/client/doublefeel/health/v2/statistics/activity/';
29 static const healthStatsHrv = '/client/doublefeel/health/v2/statistics/hrv/'; 30 static const healthStatsHrv = '/client/doublefeel/health/v2/statistics/hrv/';
@@ -38,6 +39,8 @@ abstract final class ApiPaths { @@ -38,6 +39,8 @@ abstract final class ApiPaths {
38 '/client/doublefeel/health/v2/realtime_stress/'; 39 '/client/doublefeel/health/v2/realtime_stress/';
39 static const v2ActivityTarget = 40 static const v2ActivityTarget =
40 '/client/doublefeel/health/v2/activity_target/'; 41 '/client/doublefeel/health/v2/activity_target/';
  42 + static const weatherUploadedData =
  43 + '/client/doublefeel/health/v2/weather_uploaded_data/';
41 44
42 // Pay 45 // Pay
43 static const paymentProducts = '/client/doublefeel/payment/products/'; 46 static const paymentProducts = '/client/doublefeel/payment/products/';
@@ -64,6 +64,15 @@ class UserAccountStorage { @@ -64,6 +64,15 @@ class UserAccountStorage {
64 Future<void> markOnboardingCompleted(int userId) => 64 Future<void> markOnboardingCompleted(int userId) =>
65 _prefs.setInt(_onboardingKey(userId), _kOnboardingCompleted); 65 _prefs.setInt(_onboardingKey(userId), _kOnboardingCompleted);
66 66
  67 + /// 首页添加好友引导间隔
  68 + Future<void> saveLastAddFriendBannerShowTime(int userId) => _prefs.setInt(
  69 + 'last_add_friend_banner_show_time_$userId',
  70 + DateTime.now().millisecondsSinceEpoch);
  71 +
  72 + /// 首页添加好友引导间隔
  73 + int? getLastAddFriendBannerShowTime(int userId) =>
  74 + _prefs.getInt('last_add_friend_banner_show_time_$userId');
  75 +
67 // ─── Apple Health 上传记录 ──────────────────────────────────────────────── 76 // ─── Apple Health 上传记录 ────────────────────────────────────────────────
68 77
69 String? appleHealthUploadLocalRecordJson(int userId) => 78 String? appleHealthUploadLocalRecordJson(int userId) =>
1 // ─── Responses ─────────────────────────────────────────────────────────────── 1 // ─── Responses ───────────────────────────────────────────────────────────────
2 2
3 class FriendListResponse { 3 class FriendListResponse {
4 - const FriendListResponse({this.list = const []}); 4 + const FriendListResponse({this.list = const [], this.limit});
5 5
6 final List<FriendItem> list; 6 final List<FriendItem> list;
  7 + final int? limit;
7 8
8 factory FriendListResponse.fromJson(Map<String, dynamic> json) { 9 factory FriendListResponse.fromJson(Map<String, dynamic> json) {
9 return FriendListResponse( 10 return FriendListResponse(
@@ -11,11 +12,13 @@ class FriendListResponse { @@ -11,11 +12,13 @@ class FriendListResponse {
11 ?.map((e) => FriendItem.fromJson(e as Map<String, dynamic>)) 12 ?.map((e) => FriendItem.fromJson(e as Map<String, dynamic>))
12 .toList() ?? 13 .toList() ??
13 const [], 14 const [],
  15 + limit: json['limit'] as int?,
14 ); 16 );
15 } 17 }
16 18
17 Map<String, dynamic> toJson() => { 19 Map<String, dynamic> toJson() => {
18 'list': list.map((e) => e.toJson()).toList(), 20 'list': list.map((e) => e.toJson()).toList(),
  21 + 'limit': limit,
19 }; 22 };
20 } 23 }
21 24
@@ -76,6 +76,7 @@ class UserPreferencesVipInfo { @@ -76,6 +76,7 @@ class UserPreferencesVipInfo {
76 this.isShare = false, 76 this.isShare = false,
77 this.vipStartDate = 0, 77 this.vipStartDate = 0,
78 this.vipEndDate = 0, 78 this.vipEndDate = 0,
  79 + this.vipType = 0,
79 }); 80 });
80 81
81 final bool isVip; 82 final bool isVip;
@@ -83,6 +84,7 @@ class UserPreferencesVipInfo { @@ -83,6 +84,7 @@ class UserPreferencesVipInfo {
83 final bool isShare; 84 final bool isShare;
84 final int vipStartDate; 85 final int vipStartDate;
85 final int vipEndDate; 86 final int vipEndDate;
  87 + final int vipType;
86 88
87 factory UserPreferencesVipInfo.fromJson(Map<String, dynamic> json) { 89 factory UserPreferencesVipInfo.fromJson(Map<String, dynamic> json) {
88 return UserPreferencesVipInfo( 90 return UserPreferencesVipInfo(
@@ -91,6 +93,7 @@ class UserPreferencesVipInfo { @@ -91,6 +93,7 @@ class UserPreferencesVipInfo {
91 isShare: (json['is_share'] as bool?) ?? false, 93 isShare: (json['is_share'] as bool?) ?? false,
92 vipStartDate: (json['vip_start_date'] as num?)?.toInt() ?? 0, 94 vipStartDate: (json['vip_start_date'] as num?)?.toInt() ?? 0,
93 vipEndDate: (json['vip_end_date'] as num?)?.toInt() ?? 0, 95 vipEndDate: (json['vip_end_date'] as num?)?.toInt() ?? 0,
  96 + vipType: (json['vip_type'] as num?)?.toInt() ?? 0,
94 ); 97 );
95 } 98 }
96 99
@@ -101,6 +104,7 @@ class UserPreferencesVipInfo { @@ -101,6 +104,7 @@ class UserPreferencesVipInfo {
101 'is_share': isShare, 104 'is_share': isShare,
102 'vip_start_date': vipStartDate, 105 'vip_start_date': vipStartDate,
103 'vip_end_date': vipEndDate, 106 'vip_end_date': vipEndDate,
  107 + 'vip_type': vipType,
104 }; 108 };
105 } 109 }
106 110
@@ -111,6 +115,7 @@ class UserPreferencesVipInfo { @@ -111,6 +115,7 @@ class UserPreferencesVipInfo {
111 isShare: info.isShare ?? false, 115 isShare: info.isShare ?? false,
112 vipStartDate: info.vipStartDate ?? 0, 116 vipStartDate: info.vipStartDate ?? 0,
113 vipEndDate: info.vipEndDate ?? 0, 117 vipEndDate: info.vipEndDate ?? 0,
  118 + vipType: info.vipType ?? 0,
114 ); 119 );
115 } 120 }
116 } 121 }
@@ -87,19 +87,30 @@ class PushTokenRequest { @@ -87,19 +87,30 @@ class PushTokenRequest {
87 } 87 }
88 88
89 class UserInfoUpdateRequest { 89 class UserInfoUpdateRequest {
90 - const UserInfoUpdateRequest({this.avatar, this.nickname, this.persona, this.enableAddWithUcode}); 90 + const UserInfoUpdateRequest(
  91 + {this.avatar,
  92 + this.nickname,
  93 + this.persona,
  94 + this.enableAddWithUcode,
  95 + this.isPassNoviceGuide});
91 96
92 final String? avatar; 97 final String? avatar;
93 final String? nickname; 98 final String? nickname;
94 final int? persona; 99 final int? persona;
95 final int? enableAddWithUcode; 100 final int? enableAddWithUcode;
  101 + final int? isPassNoviceGuide;
96 102
97 Map<String, dynamic> toJson() { 103 Map<String, dynamic> toJson() {
98 final json = <String, dynamic>{}; 104 final json = <String, dynamic>{};
99 if (avatar != null) json['avatar'] = avatar; 105 if (avatar != null) json['avatar'] = avatar;
100 if (nickname != null) json['nickname'] = nickname; 106 if (nickname != null) json['nickname'] = nickname;
101 if (persona != null) json['persona'] = persona; 107 if (persona != null) json['persona'] = persona;
102 - if (enableAddWithUcode != null) json['enable_add_with_ucode'] = enableAddWithUcode; 108 + if (enableAddWithUcode != null) {
  109 + json['enable_add_with_ucode'] = enableAddWithUcode;
  110 + }
  111 + if (isPassNoviceGuide != null) {
  112 + json['is_pass_novice_guide'] = isPassNoviceGuide;
  113 + }
103 return json; 114 return json;
104 } 115 }
105 } 116 }
@@ -193,7 +204,8 @@ class UserInfoResponse { @@ -193,7 +204,8 @@ class UserInfoResponse {
193 this.createTime, 204 this.createTime,
194 this.isBot, 205 this.isBot,
195 this.uniqueCode, 206 this.uniqueCode,
196 - this.enableAddWithUCode}); 207 + this.enableAddWithUCode,
  208 + this.isPassNoviceGuide});
197 209
198 final int? id; 210 final int? id;
199 final String? pairCode; 211 final String? pairCode;
@@ -208,6 +220,7 @@ class UserInfoResponse { @@ -208,6 +220,7 @@ class UserInfoResponse {
208 final int? isBot; 220 final int? isBot;
209 final String? uniqueCode; 221 final String? uniqueCode;
210 final int? enableAddWithUCode; 222 final int? enableAddWithUCode;
  223 + final int? isPassNoviceGuide;
211 224
212 factory UserInfoResponse.fromJson(Map<String, dynamic> json) { 225 factory UserInfoResponse.fromJson(Map<String, dynamic> json) {
213 return UserInfoResponse( 226 return UserInfoResponse(
@@ -224,6 +237,7 @@ class UserInfoResponse { @@ -224,6 +237,7 @@ class UserInfoResponse {
224 isBot: json['is_bot'] as int?, 237 isBot: json['is_bot'] as int?,
225 uniqueCode: json['unique_code'] as String?, 238 uniqueCode: json['unique_code'] as String?,
226 enableAddWithUCode: json['enable_add_with_ucode'] as int?, 239 enableAddWithUCode: json['enable_add_with_ucode'] as int?,
  240 + isPassNoviceGuide: json['is_pass_novice_guide'] as int?,
227 ); 241 );
228 } 242 }
229 243
@@ -244,6 +258,9 @@ class UserInfoResponse { @@ -244,6 +258,9 @@ class UserInfoResponse {
244 if (enableAddWithUCode != null) { 258 if (enableAddWithUCode != null) {
245 val['enable_add_with_ucode'] = enableAddWithUCode; 259 val['enable_add_with_ucode'] = enableAddWithUCode;
246 } 260 }
  261 + if (isPassNoviceGuide != null) {
  262 + val['is_pass_novice_guide'] = isPassNoviceGuide;
  263 + }
247 return val; 264 return val;
248 } 265 }
249 } 266 }
@@ -5,6 +5,7 @@ class VipInfo { @@ -5,6 +5,7 @@ class VipInfo {
5 this.isShare, 5 this.isShare,
6 this.vipStartDate, 6 this.vipStartDate,
7 this.vipEndDate, 7 this.vipEndDate,
  8 + this.vipType,
8 }); 9 });
9 10
10 final bool? isVip; 11 final bool? isVip;
@@ -12,6 +13,7 @@ class VipInfo { @@ -12,6 +13,7 @@ class VipInfo {
12 final bool? isShare; 13 final bool? isShare;
13 final int? vipStartDate; 14 final int? vipStartDate;
14 final int? vipEndDate; 15 final int? vipEndDate;
  16 + final int? vipType;
15 17
16 factory VipInfo.fromJson(Map<String, dynamic> json) { 18 factory VipInfo.fromJson(Map<String, dynamic> json) {
17 return VipInfo( 19 return VipInfo(
@@ -20,6 +22,7 @@ class VipInfo { @@ -20,6 +22,7 @@ class VipInfo {
20 isShare: json['is_share'] as bool?, 22 isShare: json['is_share'] as bool?,
21 vipStartDate: (json['vip_start_date'] as num?)?.toInt(), 23 vipStartDate: (json['vip_start_date'] as num?)?.toInt(),
22 vipEndDate: (json['vip_end_date'] as num?)?.toInt(), 24 vipEndDate: (json['vip_end_date'] as num?)?.toInt(),
  25 + vipType: (json['vip_type'] as num?)?.toInt(),
23 ); 26 );
24 } 27 }
25 28
@@ -30,6 +33,7 @@ class VipInfo { @@ -30,6 +33,7 @@ class VipInfo {
30 if (isShare != null) val['is_share'] = isShare; 33 if (isShare != null) val['is_share'] = isShare;
31 if (vipStartDate != null) val['vip_start_date'] = vipStartDate; 34 if (vipStartDate != null) val['vip_start_date'] = vipStartDate;
32 if (vipEndDate != null) val['vip_end_date'] = vipEndDate; 35 if (vipEndDate != null) val['vip_end_date'] = vipEndDate;
  36 + if (vipType != null) val['vip_type'] = vipType;
33 return val; 37 return val;
34 } 38 }
35 } 39 }
@@ -66,9 +66,6 @@ @@ -66,9 +66,6 @@
66 "onboardingNotificationBody": "After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.", 66 "onboardingNotificationBody": "After Apple Watch data updates, we can remind you in time and help you act to improve your stress state.",
67 "onboardingMemberTitle": "Get an annual membership offer", 67 "onboardingMemberTitle": "Get an annual membership offer",
68 "onboardingMemberBody": "Start your pressure alert and health companion journey, so love and care are always present.", 68 "onboardingMemberBody": "Start your pressure alert and health companion journey, so love and care are always present.",
69 - "onboardingMemberOriginalPrice": "Original ¥72.00/year",  
70 - "onboardingMemberCurrentPrice": "Now ¥60.00/year",  
71 - "onboardingMemberMonthlyPrice": "Only ¥5.00/month",  
72 "onboardingMemberAllOptions": "View all purchase options", 69 "onboardingMemberAllOptions": "View all purchase options",
73 "healthCompanionIsNowAvailable": "Health Companion is now available", 70 "healthCompanionIsNowAvailable": "Health Companion is now available",
74 "youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired": "You can now view each other's HRV, stress levels, and sleep patterns, and reach out to check in when the other person seems tired.", 71 "youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired": "You can now view each other's HRV, stress levels, and sleep patterns, and reach out to check in when the other person seems tired.",
@@ -93,7 +90,7 @@ @@ -93,7 +90,7 @@
93 "loginTerms": "Terms of Service", 90 "loginTerms": "Terms of Service",
94 "loginAgreementAnd": " and ", 91 "loginAgreementAnd": " and ",
95 "loginPrivacy": "Privacy Policy", 92 "loginPrivacy": "Privacy Policy",
96 - "phoneLoginHello": "Hello", 93 + "phoneLoginHello": "Hello",
97 "phoneLoginWelcome": "Welcome to Double Feel", 94 "phoneLoginWelcome": "Welcome to Double Feel",
98 "phoneLoginPhoneHint": "Enter phone number", 95 "phoneLoginPhoneHint": "Enter phone number",
99 "phoneLoginSendCode": "Send Code", 96 "phoneLoginSendCode": "Send Code",
@@ -498,4 +495,4 @@ @@ -498,4 +495,4 @@
498 "appReviewFeedbackSendAction": "Send Feedback", 495 "appReviewFeedbackSendAction": "Send Feedback",
499 "appReviewFeedbackLaterAction": "Maybe Later", 496 "appReviewFeedbackLaterAction": "Maybe Later",
500 "appReviewIllustrationPlaceholder": "Illustration Placeholder" 497 "appReviewIllustrationPlaceholder": "Illustration Placeholder"
501 -} 498 +}
@@ -69,9 +69,6 @@ @@ -69,9 +69,6 @@
69 "onboardingNotificationBody": "AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态", 69 "onboardingNotificationBody": "AppleWatch数据更新后会及时提醒你,帮助你及时行动,改善压力状态",
70 "onboardingMemberTitle": "获得年度会员优惠", 70 "onboardingMemberTitle": "获得年度会员优惠",
71 "onboardingMemberBody": "开启压力预警与健康陪伴之旅,让爱与关心从不缺席", 71 "onboardingMemberBody": "开启压力预警与健康陪伴之旅,让爱与关心从不缺席",
72 - "onboardingMemberOriginalPrice": "原价¥72.00/年",  
73 - "onboardingMemberCurrentPrice": "现价 ¥60.00/年",  
74 - "onboardingMemberMonthlyPrice": "仅相当于¥5.00/月",  
75 "onboardingMemberAllOptions": "查看所有购买选项", 72 "onboardingMemberAllOptions": "查看所有购买选项",
76 "healthCompanionIsNowAvailable": "健康陪伴已开启", 73 "healthCompanionIsNowAvailable": "健康陪伴已开启",
77 "youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired": "你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。", 74 "youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired": "你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。",
@@ -364,21 +361,72 @@ @@ -364,21 +361,72 @@
364 "reportPeriodMonth": "月", 361 "reportPeriodMonth": "月",
365 "reportPeriodYear": "年", 362 "reportPeriodYear": "年",
366 "reportDateYear": "{year}年", 363 "reportDateYear": "{year}年",
367 - "@reportDateYear": {"placeholders": {"year": {"type": "int"}}}, 364 + "@reportDateYear": {
  365 + "placeholders": {
  366 + "year": {
  367 + "type": "int"
  368 + }
  369 + }
  370 + },
368 "reportDateMonth": "{month}月", 371 "reportDateMonth": "{month}月",
369 - "@reportDateMonth": {"placeholders": {"month": {"type": "int"}}}, 372 + "@reportDateMonth": {
  373 + "placeholders": {
  374 + "month": {
  375 + "type": "int"
  376 + }
  377 + }
  378 + },
370 "reportDateMonthDay": "{month}月{day}日", 379 "reportDateMonthDay": "{month}月{day}日",
371 - "@reportDateMonthDay": {"placeholders": {"month": {"type": "int"}, "day": {"type": "int"}}}, 380 + "@reportDateMonthDay": {
  381 + "placeholders": {
  382 + "month": {
  383 + "type": "int"
  384 + },
  385 + "day": {
  386 + "type": "int"
  387 + }
  388 + }
  389 + },
372 "reportDateYearMonthDay": "{year}年{month}月{day}日", 390 "reportDateYearMonthDay": "{year}年{month}月{day}日",
373 - "@reportDateYearMonthDay": {"placeholders": {"year": {"type": "int"}, "month": {"type": "int"}, "day": {"type": "int"}}}, 391 + "@reportDateYearMonthDay": {
  392 + "placeholders": {
  393 + "year": {
  394 + "type": "int"
  395 + },
  396 + "month": {
  397 + "type": "int"
  398 + },
  399 + "day": {
  400 + "type": "int"
  401 + }
  402 + }
  403 + },
374 "reportDatePickerTitle": "选择时间", 404 "reportDatePickerTitle": "选择时间",
375 "reportDatePickerConfirm": "确定", 405 "reportDatePickerConfirm": "确定",
376 "reportDatePickerYearOption": "{year} 年", 406 "reportDatePickerYearOption": "{year} 年",
377 - "@reportDatePickerYearOption": {"placeholders": {"year": {"type": "int"}}}, 407 + "@reportDatePickerYearOption": {
  408 + "placeholders": {
  409 + "year": {
  410 + "type": "int"
  411 + }
  412 + }
  413 + },
378 "reportDatePickerMonthOption": "{month} 月", 414 "reportDatePickerMonthOption": "{month} 月",
379 - "@reportDatePickerMonthOption": {"placeholders": {"month": {"type": "int"}}}, 415 + "@reportDatePickerMonthOption": {
  416 + "placeholders": {
  417 + "month": {
  418 + "type": "int"
  419 + }
  420 + }
  421 + },
380 "reportDatePickerDayOption": "{day} 日", 422 "reportDatePickerDayOption": "{day} 日",
381 - "@reportDatePickerDayOption": {"placeholders": {"day": {"type": "int"}}}, 423 + "@reportDatePickerDayOption": {
  424 + "placeholders": {
  425 + "day": {
  426 + "type": "int"
  427 + }
  428 + }
  429 + },
382 "reportWaitingForData": "等待数据", 430 "reportWaitingForData": "等待数据",
383 "reportNoData": "暂无数据", 431 "reportNoData": "暂无数据",
384 "reportNoDataToday": "暂无本日数据", 432 "reportNoDataToday": "暂无本日数据",
@@ -395,14 +443,41 @@ @@ -395,14 +443,41 @@
395 "reportWeekdaySaturday": "六", 443 "reportWeekdaySaturday": "六",
396 "reportWeekdaySunday": "日", 444 "reportWeekdaySunday": "日",
397 "reportDateWithWeekday": "{date} 星期{weekday}", 445 "reportDateWithWeekday": "{date} 星期{weekday}",
398 - "@reportDateWithWeekday": {"placeholders": {"date": {"type": "String"}, "weekday": {"type": "String"}}}, 446 + "@reportDateWithWeekday": {
  447 + "placeholders": {
  448 + "date": {
  449 + "type": "String"
  450 + },
  451 + "weekday": {
  452 + "type": "String"
  453 + }
  454 + }
  455 + },
399 "reportOtherPossessiveTitle": "Ta的{title}", 456 "reportOtherPossessiveTitle": "Ta的{title}",
400 - "@reportOtherPossessiveTitle": {"placeholders": {"title": {"type": "String"}}}, 457 + "@reportOtherPossessiveTitle": {
  458 + "placeholders": {
  459 + "title": {
  460 + "type": "String"
  461 + }
  462 + }
  463 + },
401 "reportOtherTitle": "Ta{title}", 464 "reportOtherTitle": "Ta{title}",
402 - "@reportOtherTitle": {"placeholders": {"title": {"type": "String"}}}, 465 + "@reportOtherTitle": {
  466 + "placeholders": {
  467 + "title": {
  468 + "type": "String"
  469 + }
  470 + }
  471 + },
403 "reportTrend": "趋势", 472 "reportTrend": "趋势",
404 "reportExampleTitle": "{title}(示例)", 473 "reportExampleTitle": "{title}(示例)",
405 - "@reportExampleTitle": {"placeholders": {"title": {"type": "String"}}}, 474 + "@reportExampleTitle": {
  475 + "placeholders": {
  476 + "title": {
  477 + "type": "String"
  478 + }
  479 + }
  480 + },
406 "hrvStressExcellent": "状态优秀", 481 "hrvStressExcellent": "状态优秀",
407 "hrvStressNormal": "状态正常", 482 "hrvStressNormal": "状态正常",
408 "hrvStressAttention": "注意压力", 483 "hrvStressAttention": "注意压力",
@@ -427,9 +502,21 @@ @@ -427,9 +502,21 @@
427 "hrvComparedLastWeekUnavailable": "比上周少-天", 502 "hrvComparedLastWeekUnavailable": "比上周少-天",
428 "hrvSameAsLastWeek": "与上周持平", 503 "hrvSameAsLastWeek": "与上周持平",
429 "hrvMoreDaysThanLastWeek": "比上周多{count}天", 504 "hrvMoreDaysThanLastWeek": "比上周多{count}天",
430 - "@hrvMoreDaysThanLastWeek": {"placeholders": {"count": {"type": "int"}}}, 505 + "@hrvMoreDaysThanLastWeek": {
  506 + "placeholders": {
  507 + "count": {
  508 + "type": "int"
  509 + }
  510 + }
  511 + },
431 "hrvFewerDaysThanLastWeek": "比上周少{count}天", 512 "hrvFewerDaysThanLastWeek": "比上周少{count}天",
432 - "@hrvFewerDaysThanLastWeek": {"placeholders": {"count": {"type": "int"}}}, 513 + "@hrvFewerDaysThanLastWeek": {
  514 + "placeholders": {
  515 + "count": {
  516 + "type": "int"
  517 + }
  518 + }
  519 + },
433 "hrvUnlockNow": "立即解锁", 520 "hrvUnlockNow": "立即解锁",
434 "activityTotalBurn": "活动总消耗", 521 "activityTotalBurn": "活动总消耗",
435 "activityExerciseTotalDuration": "锻炼总时长", 522 "activityExerciseTotalDuration": "锻炼总时长",
@@ -463,13 +550,46 @@ @@ -463,13 +550,46 @@
463 "sleepPreviousWeek": "上周", 550 "sleepPreviousWeek": "上周",
464 "sleepPreviousMonth": "上月", 551 "sleepPreviousMonth": "上月",
465 "sleepComparedPercent": "比{period}{value}%", 552 "sleepComparedPercent": "比{period}{value}%",
466 - "@sleepComparedPercent": {"placeholders": {"period": {"type": "String"}, "value": {"type": "String"}}}, 553 + "@sleepComparedPercent": {
  554 + "placeholders": {
  555 + "period": {
  556 + "type": "String"
  557 + },
  558 + "value": {
  559 + "type": "String"
  560 + }
  561 + }
  562 + },
467 "sleepDurationValue": "{hours}小时{minutes}分钟", 563 "sleepDurationValue": "{hours}小时{minutes}分钟",
468 - "@sleepDurationValue": {"placeholders": {"hours": {"type": "int"}, "minutes": {"type": "int"}}}, 564 + "@sleepDurationValue": {
  565 + "placeholders": {
  566 + "hours": {
  567 + "type": "int"
  568 + },
  569 + "minutes": {
  570 + "type": "int"
  571 + }
  572 + }
  573 + },
469 "sleepQualityScore": "{level}:{score}分", 574 "sleepQualityScore": "{level}:{score}分",
470 - "@sleepQualityScore": {"placeholders": {"level": {"type": "String"}, "score": {"type": "int"}}}, 575 + "@sleepQualityScore": {
  576 + "placeholders": {
  577 + "level": {
  578 + "type": "String"
  579 + },
  580 + "score": {
  581 + "type": "int"
  582 + }
  583 + }
  584 + },
471 "sleepFellAsleepAt": "{time}入睡", 585 "sleepFellAsleepAt": "{time}入睡",
472 - "@sleepFellAsleepAt": {"placeholders": {"time": {"type": "String"}}}, 586 + "@sleepFellAsleepAt": {
  587 + "placeholders": {
  588 + "time": {
  589 + "type": "String"
  590 + }
  591 + }
  592 + },
473 "sleepAverage": "平均", 593 "sleepAverage": "平均",
474 "sleepTarget": "目标", 594 "sleepTarget": "目标",
475 "sleepHighest": "最高", 595 "sleepHighest": "最高",
@@ -482,17 +602,53 @@ @@ -482,17 +602,53 @@
482 "friendsLimitReached": "好友数量已达上限", 602 "friendsLimitReached": "好友数量已达上限",
483 "friendsAddCloseContact": "添加亲密联系人", 603 "friendsAddCloseContact": "添加亲密联系人",
484 "friendsAddCloseContactWithCount": "添加亲密联系人({count}/{max})", 604 "friendsAddCloseContactWithCount": "添加亲密联系人({count}/{max})",
485 - "@friendsAddCloseContactWithCount": {"placeholders": {"count": {"type": "int"}, "max": {"type": "int"}}}, 605 + "@friendsAddCloseContactWithCount": {
  606 + "placeholders": {
  607 + "count": {
  608 + "type": "int"
  609 + },
  610 + "max": {
  611 + "type": "int"
  612 + }
  613 + }
  614 + },
486 "friendsMe": "我", 615 "friendsMe": "我",
487 "friendsRemarkedDisplayName": "{remark}({name})", 616 "friendsRemarkedDisplayName": "{remark}({name})",
488 - "@friendsRemarkedDisplayName": {"placeholders": {"remark": {"type": "String"}, "name": {"type": "String"}}}, 617 + "@friendsRemarkedDisplayName": {
  618 + "placeholders": {
  619 + "remark": {
  620 + "type": "String"
  621 + },
  622 + "name": {
  623 + "type": "String"
  624 + }
  625 + }
  626 + },
489 "friendsRemarkSuffix": "({remark})", 627 "friendsRemarkSuffix": "({remark})",
490 - "@friendsRemarkSuffix": {"placeholders": {"remark": {"type": "String"}}}, 628 + "@friendsRemarkSuffix": {
  629 + "placeholders": {
  630 + "remark": {
  631 + "type": "String"
  632 + }
  633 + }
  634 + },
491 "friendsUnknownFriend": "未知好友", 635 "friendsUnknownFriend": "未知好友",
492 "friendsUpdatedAt": "更新于{time}", 636 "friendsUpdatedAt": "更新于{time}",
493 - "@friendsUpdatedAt": {"placeholders": {"time": {"type": "String"}}}, 637 + "@friendsUpdatedAt": {
  638 + "placeholders": {
  639 + "time": {
  640 + "type": "String"
  641 + }
  642 + }
  643 + },
494 "friendsStepCount": "{count}步", 644 "friendsStepCount": "{count}步",
495 - "@friendsStepCount": {"placeholders": {"count": {"type": "int"}}}, 645 + "@friendsStepCount": {
  646 + "placeholders": {
  647 + "count": {
  648 + "type": "int"
  649 + }
  650 + }
  651 + },
496 "friendsStressAttention": "注意压力", 652 "friendsStressAttention": "注意压力",
497 "friendsWaitingForData": "等待数据", 653 "friendsWaitingForData": "等待数据",
498 "friendsSleepQuality": "睡眠质量", 654 "friendsSleepQuality": "睡眠质量",
@@ -508,7 +664,13 @@ @@ -508,7 +664,13 @@
508 "friendsSelectAndSync": "选择并同步至表盘", 664 "friendsSelectAndSync": "选择并同步至表盘",
509 "friendsBack": "返回", 665 "friendsBack": "返回",
510 "friendsTrendTitle": "{name}的趋势", 666 "friendsTrendTitle": "{name}的趋势",
511 - "@friendsTrendTitle": {"placeholders": {"name": {"type": "String"}}}, 667 + "@friendsTrendTitle": {
  668 + "placeholders": {
  669 + "name": {
  670 + "type": "String"
  671 + }
  672 + }
  673 + },
512 "friendsAddAction": "添加", 674 "friendsAddAction": "添加",
513 "friendsEnterId": "输入ID", 675 "friendsEnterId": "输入ID",
514 "friendsPromptGotIt": "我知道了", 676 "friendsPromptGotIt": "我知道了",
@@ -522,7 +684,13 @@ @@ -522,7 +684,13 @@
522 "friendsEditRemarkHint": "请输入昵称", 684 "friendsEditRemarkHint": "请输入昵称",
523 "friendsSave": "保存", 685 "friendsSave": "保存",
524 "friendsDeleteConfirmTitle": "确认要和{name}解除好友关系吗?", 686 "friendsDeleteConfirmTitle": "确认要和{name}解除好友关系吗?",
525 - "@friendsDeleteConfirmTitle": {"placeholders": {"name": {"type": "String"}}}, 687 + "@friendsDeleteConfirmTitle": {
  688 + "placeholders": {
  689 + "name": {
  690 + "type": "String"
  691 + }
  692 + }
  693 + },
526 "friendsDeleteConfirmMessage": "解除后你将无法查看对方的情绪、健康状态", 694 "friendsDeleteConfirmMessage": "解除后你将无法查看对方的情绪、健康状态",
527 "friendsDeleteConfirmAction": "确认解除", 695 "friendsDeleteConfirmAction": "确认解除",
528 "privacySettingsTitle": "隐私设置", 696 "privacySettingsTitle": "隐私设置",
@@ -612,4 +780,4 @@ @@ -612,4 +780,4 @@
612 "appReviewFeedbackSendAction": "发送反馈", 780 "appReviewFeedbackSendAction": "发送反馈",
613 "appReviewFeedbackLaterAction": "稍后再说", 781 "appReviewFeedbackLaterAction": "稍后再说",
614 "appReviewIllustrationPlaceholder": "插图占位" 782 "appReviewIllustrationPlaceholder": "插图占位"
615 -} 783 +}
@@ -62,7 +62,8 @@ import 'app_localizations_zh.dart'; @@ -62,7 +62,8 @@ import 'app_localizations_zh.dart';
62 /// be consistent with the languages listed in the AppLocalizations.supportedLocales 62 /// be consistent with the languages listed in the AppLocalizations.supportedLocales
63 /// property. 63 /// property.
64 abstract class AppLocalizations { 64 abstract class AppLocalizations {
65 - AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); 65 + AppLocalizations(String locale)
  66 + : localeName = intl.Intl.canonicalizedLocale(locale.toString());
66 67
67 final String localeName; 68 final String localeName;
68 69
@@ -70,7 +71,8 @@ abstract class AppLocalizations { @@ -70,7 +71,8 @@ abstract class AppLocalizations {
70 return Localizations.of<AppLocalizations>(context, AppLocalizations); 71 return Localizations.of<AppLocalizations>(context, AppLocalizations);
71 } 72 }
72 73
73 - static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate(); 74 + static const LocalizationsDelegate<AppLocalizations> delegate =
  75 + _AppLocalizationsDelegate();
74 76
75 /// A list of this localizations delegate along with the default localizations 77 /// A list of this localizations delegate along with the default localizations
76 /// delegates. 78 /// delegates.
@@ -82,7 +84,8 @@ abstract class AppLocalizations { @@ -82,7 +84,8 @@ abstract class AppLocalizations {
82 /// Additional delegates can be added by appending to this list in 84 /// Additional delegates can be added by appending to this list in
83 /// MaterialApp. This list does not have to be used at all if a custom list 85 /// MaterialApp. This list does not have to be used at all if a custom list
84 /// of delegates is preferred or required. 86 /// of delegates is preferred or required.
85 - static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[ 87 + static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
  88 + <LocalizationsDelegate<dynamic>>[
86 delegate, 89 delegate,
87 GlobalMaterialLocalizations.delegate, 90 GlobalMaterialLocalizations.delegate,
88 GlobalCupertinoLocalizations.delegate, 91 GlobalCupertinoLocalizations.delegate,
@@ -491,24 +494,6 @@ abstract class AppLocalizations { @@ -491,24 +494,6 @@ abstract class AppLocalizations {
491 /// **'开启压力预警与健康陪伴之旅,让爱与关心从不缺席'** 494 /// **'开启压力预警与健康陪伴之旅,让爱与关心从不缺席'**
492 String get onboardingMemberBody; 495 String get onboardingMemberBody;
493 496
494 - /// No description provided for @onboardingMemberOriginalPrice.  
495 - ///  
496 - /// In zh, this message translates to:  
497 - /// **'原价¥72.00/年'**  
498 - String get onboardingMemberOriginalPrice;  
499 -  
500 - /// No description provided for @onboardingMemberCurrentPrice.  
501 - ///  
502 - /// In zh, this message translates to:  
503 - /// **'现价 ¥60.00/年'**  
504 - String get onboardingMemberCurrentPrice;  
505 -  
506 - /// No description provided for @onboardingMemberMonthlyPrice.  
507 - ///  
508 - /// In zh, this message translates to:  
509 - /// **'仅相当于¥5.00/月'**  
510 - String get onboardingMemberMonthlyPrice;  
511 -  
512 /// No description provided for @onboardingMemberAllOptions. 497 /// No description provided for @onboardingMemberAllOptions.
513 /// 498 ///
514 /// In zh, this message translates to: 499 /// In zh, this message translates to:
@@ -525,7 +510,8 @@ abstract class AppLocalizations { @@ -525,7 +510,8 @@ abstract class AppLocalizations {
525 /// 510 ///
526 /// In zh, this message translates to: 511 /// In zh, this message translates to:
527 /// **'你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'** 512 /// **'你们现在可以查看彼此的 HRV、压力与睡眠变化,并在对方疲惫时及时送上关心。'**
528 - String get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired; 513 + String
  514 + get youCanNowViewEachOtherSHrvStressLevelsAndSleepPatternsAndReachOutToCheckInWhenTheOtherPersonSeemsTired;
529 515
530 /// No description provided for @bindPartnerTitle. 516 /// No description provided for @bindPartnerTitle.
531 /// 517 ///
@@ -3084,7 +3070,8 @@ abstract class AppLocalizations { @@ -3084,7 +3070,8 @@ abstract class AppLocalizations {
3084 String get appReviewIllustrationPlaceholder; 3070 String get appReviewIllustrationPlaceholder;
3085 } 3071 }
3086 3072
3087 -class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> { 3073 +class _AppLocalizationsDelegate
  3074 + extends LocalizationsDelegate<AppLocalizations> {
3088 const _AppLocalizationsDelegate(); 3075 const _AppLocalizationsDelegate();
3089 3076
3090 @override 3077 @override
@@ -3093,25 +3080,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> @@ -3093,25 +3080,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations>
3093 } 3080 }
3094 3081
3095 @override 3082 @override
3096 - bool isSupported(Locale locale) => <String>['en', 'zh'].contains(locale.languageCode); 3083 + bool isSupported(Locale locale) =>
  3084 + <String>['en', 'zh'].contains(locale.languageCode);
3097 3085
3098 @override 3086 @override
3099 bool shouldReload(_AppLocalizationsDelegate old) => false; 3087 bool shouldReload(_AppLocalizationsDelegate old) => false;
3100 } 3088 }
3101 3089
3102 AppLocalizations lookupAppLocalizations(Locale locale) { 3090 AppLocalizations lookupAppLocalizations(Locale locale) {
3103 -  
3104 -  
3105 // Lookup logic when only language code is specified. 3091 // Lookup logic when only language code is specified.
3106 switch (locale.languageCode) { 3092 switch (locale.languageCode) {
3107 - case 'en': return AppLocalizationsEn();  
3108 - case 'zh': return AppLocalizationsZh(); 3093 + case 'en':
  3094 + return AppLocalizationsEn();
  3095 + case 'zh':
  3096 + return AppLocalizationsZh();
3109 } 3097 }
3110 3098
3111 throw FlutterError( 3099 throw FlutterError(
3112 - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '  
3113 - 'an issue with the localizations generation tool. Please file an issue '  
3114 - 'on GitHub with a reproducible sample app and the gen-l10n configuration '  
3115 - 'that was used.'  
3116 - ); 3100 + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
  3101 + 'an issue with the localizations generation tool. Please file an issue '
  3102 + 'on GitHub with a reproducible sample app and the gen-l10n configuration '
  3103 + 'that was used.');
3117 } 3104 }