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.
assets/images/my/bg_my_unlock_vip.png
0 → 100644
10.1 KB
assets/images/today/ic_gift_box.png
0 → 100644
3.38 KB
4.91 KB
5.49 KB
5.6 KB
4.72 KB
4.79 KB
13.9 KB
5.54 KB
13.4 KB
48.3 KB
43 KB
assets/images/user_onboarding/ic_alarm.png
0 → 100644
931 Bytes
2.06 KB
2.61 KB
assets/images/user_onboarding/ic_care.png
0 → 100644
734 Bytes
530 Bytes
assets/images/user_onboarding/ic_heart.png
0 → 100644
738 Bytes
1.79 KB
6.56 KB
assets/images/user_onboarding/ic_none.png
0 → 100644
780 Bytes
assets/images/user_onboarding/ic_relax.png
0 → 100644
901 Bytes
543 Bytes
assets/images/user_onboarding/ic_sleep.png
0 → 100644
737 Bytes
assets/images/user_onboarding/ic_step.png
0 → 100644
795 Bytes
849 Bytes
assets/images/user_onboarding/ic_sun.png
0 → 100644
705 Bytes
assets/images/user_onboarding/ic_target.png
0 → 100644
888 Bytes
assets/images/user_onboarding/ic_tire.png
0 → 100644
1002 Bytes
assets/images/user_onboarding/ic_water.png
0 → 100644
756 Bytes
assets/images/user_onboarding/ic_wine.png
0 → 100644
646 Bytes
assets/lottie/scatter_flowers.json
0 → 100644
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 | } |
-
Please register or login to post a comment