Showing
25 changed files
with
1995 additions
and
341 deletions
assets/images/common/ic_contact_us.png
0 → 100644
591 Bytes
807 Bytes
assets/images/common/ic_warning.png
0 → 100644
2.39 KB
26 KB
| @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_ | @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_ | ||
| 7 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; | 7 | import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart'; |
| 8 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; | 8 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; |
| 9 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 9 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 10 | +import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | ||
| 10 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | 11 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; |
| 11 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 12 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 12 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 13 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| @@ -120,6 +121,8 @@ class TodayController extends GetMaterialController { | @@ -120,6 +121,8 @@ class TodayController extends GetMaterialController { | ||
| 120 | showPartnerAdBanner.value = false; | 121 | showPartnerAdBanner.value = false; |
| 121 | } | 122 | } |
| 122 | 123 | ||
| 124 | + final environmentConfig = Get.find<AppEnvironmentConfig>(); | ||
| 125 | + | ||
| 123 | // ── HRV 趋势图 ──────────────────────────── | 126 | // ── HRV 趋势图 ──────────────────────────── |
| 124 | final hrvChartData = <V2HrvTrendItem>[].obs; | 127 | final hrvChartData = <V2HrvTrendItem>[].obs; |
| 125 | final stressChartData = <V2RealtimeStressItem>[].obs; | 128 | final stressChartData = <V2RealtimeStressItem>[].obs; |
| @@ -516,10 +519,14 @@ class TodayController extends GetMaterialController { | @@ -516,10 +519,14 @@ class TodayController extends GetMaterialController { | ||
| 516 | Future<void> _refreshHealthDataForDate(DateTime date) async { | 519 | Future<void> _refreshHealthDataForDate(DateTime date) async { |
| 517 | _clearRealTimeData(); | 520 | _clearRealTimeData(); |
| 518 | final intDate = int.parse(DateFormat('yyyyMMdd').format(date)); | 521 | final intDate = int.parse(DateFormat('yyyyMMdd').format(date)); |
| 519 | - | ||
| 520 | final isToday = DateUtils.isSameDay(date, lastSelectableDay.value); | 522 | final isToday = DateUtils.isSameDay(date, lastSelectableDay.value); |
| 521 | - stressSubtitle.value = (isFriend ? 'Ta' : '') + | ||
| 522 | - (isToday ? l10n.overallStressLevelToday : l10n.stressLevelsOnThatDay); | 523 | + stressSubtitle.value = environmentConfig.region.value == AppRegion.china |
| 524 | + ? 'Hi, ${isFriend ? 'Ta' : ''}${isToday ? l10n.overallStressLevelToday : l10n.stressLevelsOnThatDay}' | ||
| 525 | + : isFriend | ||
| 526 | + ? isToday | ||
| 527 | + ? l10n.stressLevelsToday | ||
| 528 | + : l10n.stressLevelsOnThatDay | ||
| 529 | + : 'Hi, ${l10n.overallStressLevelToday}'; | ||
| 523 | switch (await _healthApi.getV2HealthData(friendUserId, intDate)) { | 530 | switch (await _healthApi.getV2HealthData(friendUserId, intDate)) { |
| 524 | case AppSuccess(:final data): | 531 | case AppSuccess(:final data): |
| 525 | v2HealthData.value = data; | 532 | v2HealthData.value = data; |
| @@ -2,7 +2,9 @@ import 'package:cached_network_image/cached_network_image.dart'; | @@ -2,7 +2,9 @@ import 'package:cached_network_image/cached_network_image.dart'; | ||
| 2 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | 2 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; |
| 3 | import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart'; | 3 | import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart'; |
| 4 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; |
| 5 | +import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_cn_view.dart'; | ||
| 5 | import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart'; |
| 7 | +import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart'; | ||
| 6 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 8 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; |
| 7 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 9 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 8 | import 'package:doublefeel_flutter/app/utils/assets_helper.dart'; | 10 | import 'package:doublefeel_flutter/app/utils/assets_helper.dart'; |
| @@ -59,12 +61,25 @@ class MyTab extends GetView<MyController> { | @@ -59,12 +61,25 @@ class MyTab extends GetView<MyController> { | ||
| 59 | const SizedBox(height: 12), | 61 | const SizedBox(height: 12), |
| 60 | _SettingsRow( | 62 | _SettingsRow( |
| 61 | title: context.l10n.accountInformation, | 63 | title: context.l10n.accountInformation, |
| 62 | - onTap: () => Get.to(() => const AccountSettingView()), | 64 | + onTap: () => Get.to( |
| 65 | + () => environmentConfig.region.value == AppRegion.china | ||
| 66 | + ? const AccountSettingCNView() | ||
| 67 | + : const AccountSettingView(), | ||
| 68 | + ), | ||
| 63 | ), | 69 | ), |
| 64 | const SizedBox(height: 12), | 70 | const SizedBox(height: 12), |
| 65 | _SettingsRow( | 71 | _SettingsRow( |
| 66 | title: context.l10n.help, | 72 | title: context.l10n.help, |
| 67 | - onTap: () => Get.toNamed(Routes.HELP), | 73 | + onTap: (() { |
| 74 | + if (environmentConfig.region.value == AppRegion.china) { | ||
| 75 | + Get.toNamed(Routes.HELP); | ||
| 76 | + } else { | ||
| 77 | + showEmailLoginContactUsBottomSheet( | ||
| 78 | + userId: (me?.id ?? 0).toString(), | ||
| 79 | + pageName: 'Setting page', | ||
| 80 | + ); | ||
| 81 | + } | ||
| 82 | + }), | ||
| 68 | ), | 83 | ), |
| 69 | if (environmentConfig.isDebug) ...[ | 84 | if (environmentConfig.isDebug) ...[ |
| 70 | const SizedBox(height: 12), | 85 | const SizedBox(height: 12), |
| @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measuremen | @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measuremen | ||
| 4 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart'; |
| 5 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart'; | 5 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart'; |
| 6 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; | 6 | import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart'; |
| 7 | +import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | ||
| 7 | import 'package:doublefeel_flutter/core/services/app_config_service.dart'; | 8 | import 'package:doublefeel_flutter/core/services/app_config_service.dart'; |
| 8 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 9 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 9 | 10 | ||
| @@ -159,7 +160,7 @@ class TodayTabBody extends StatelessWidget { | @@ -159,7 +160,7 @@ class TodayTabBody extends StatelessWidget { | ||
| 159 | mainAxisAlignment: MainAxisAlignment.center, | 160 | mainAxisAlignment: MainAxisAlignment.center, |
| 160 | children: [ | 161 | children: [ |
| 161 | Text( | 162 | Text( |
| 162 | - 'Hi,${controller.stressSubtitle.value}', | 163 | + controller.stressSubtitle.value, |
| 163 | textAlign: TextAlign.center, | 164 | textAlign: TextAlign.center, |
| 164 | style: const TextStyle( | 165 | style: const TextStyle( |
| 165 | color: Color(0xFF78787D), | 166 | color: Color(0xFF78787D), |
| @@ -422,7 +423,8 @@ class TodayTabBody extends StatelessWidget { | @@ -422,7 +423,8 @@ class TodayTabBody extends StatelessWidget { | ||
| 422 | ) | 423 | ) |
| 423 | : null, | 424 | : null, |
| 424 | child: Text( | 425 | child: Text( |
| 425 | - isSameDay(day, controller.lastSelectableDay.value) | 426 | + isSameDay(day, controller.lastSelectableDay.value) && |
| 427 | + controller.environmentConfig.region.value == AppRegion.china | ||
| 426 | ? l10n.todayOnWeeklyCalendar | 428 | ? l10n.todayOnWeeklyCalendar |
| 427 | : _weekdayText(context, day), | 429 | : _weekdayText(context, day), |
| 428 | style: TextStyle( | 430 | style: TextStyle( |
| 1 | +import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | ||
| 2 | +import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | ||
| 3 | +import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | ||
| 5 | +import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | ||
| 6 | + | ||
| 7 | +import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | ||
| 8 | +import 'package:doublefeel_flutter/core/result/app_result.dart'; | ||
| 9 | +import 'package:doublefeel_flutter/core/services/loading_service.dart'; | ||
| 10 | +import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | ||
| 11 | +import 'package:doublefeel_flutter/core/theme/app_colors.dart'; | ||
| 12 | +import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 13 | +import 'package:doublefeel_flutter/core/util/app_toast.dart'; | ||
| 14 | + | ||
| 15 | +import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | ||
| 16 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 17 | +import 'package:flutter/material.dart'; | ||
| 18 | + | ||
| 19 | +import 'package:get/get.dart'; | ||
| 20 | + | ||
| 21 | +class AccountSettingCNView extends GetView<MyController> { | ||
| 22 | + const AccountSettingCNView({super.key}); | ||
| 23 | + | ||
| 24 | + @override | ||
| 25 | + Widget build(BuildContext context) { | ||
| 26 | + final userPrefs = Get.find<UserPreferencesStorage>(); | ||
| 27 | + | ||
| 28 | + return Scaffold( | ||
| 29 | + appBar: AppBar( | ||
| 30 | + title: Text( | ||
| 31 | + context.l10n.accountSettings, | ||
| 32 | + ), | ||
| 33 | + leading: IconButton( | ||
| 34 | + highlightColor: Colors.transparent, | ||
| 35 | + padding: EdgeInsets.zero, | ||
| 36 | + onPressed: Get.back, | ||
| 37 | + icon: Image.asset( | ||
| 38 | + 'assets/images/common/ic_nav_back.webp', | ||
| 39 | + width: 24, | ||
| 40 | + height: 24, | ||
| 41 | + ), | ||
| 42 | + ), | ||
| 43 | + ), | ||
| 44 | + body: Obx(() { | ||
| 45 | + final telephone = | ||
| 46 | + userPrefs.preferences.value.meUserInfo?.telephone ?? ''; | ||
| 47 | + final thirdAccountId = | ||
| 48 | + userPrefs.preferences.value.meUserInfo?.thirdAccountId ?? ''; | ||
| 49 | + return Column( | ||
| 50 | + children: [ | ||
| 51 | + SizedBox(height: 16), | ||
| 52 | + if (thirdAccountId.isNotEmpty) | ||
| 53 | + _buildAccountCard( | ||
| 54 | + context: context, | ||
| 55 | + title: 'Apple ID', | ||
| 56 | + content: thirdAccountId, | ||
| 57 | + ) | ||
| 58 | + else | ||
| 59 | + _buildAccountCard( | ||
| 60 | + context: context, | ||
| 61 | + title: context.l10n.mobilePhoneNumber, | ||
| 62 | + content: telephone, | ||
| 63 | + ), | ||
| 64 | + SizedBox(height: 20), | ||
| 65 | + GestureDetector( | ||
| 66 | + behavior: HitTestBehavior.opaque, | ||
| 67 | + onTap: () { | ||
| 68 | + Get.bottomSheet( | ||
| 69 | + _DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount), | ||
| 70 | + barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 71 | + enableDrag: true, | ||
| 72 | + isScrollControlled: true, | ||
| 73 | + persistent: false, | ||
| 74 | + ); | ||
| 75 | + }, | ||
| 76 | + child: Padding( | ||
| 77 | + padding: EdgeInsets.symmetric( | ||
| 78 | + horizontal: 24, | ||
| 79 | + vertical: 8, | ||
| 80 | + ), | ||
| 81 | + child: Text( | ||
| 82 | + context.l10n.deleteAccount, | ||
| 83 | + style: TextStyle( | ||
| 84 | + color: AppColors.warning, | ||
| 85 | + fontSize: 12, | ||
| 86 | + fontWeight: FontWeight.w400, | ||
| 87 | + height: 1.4, | ||
| 88 | + ), | ||
| 89 | + ), | ||
| 90 | + ), | ||
| 91 | + ), | ||
| 92 | + ], | ||
| 93 | + ); | ||
| 94 | + }), | ||
| 95 | + ); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + Widget _buildAccountCard({ | ||
| 99 | + required BuildContext context, | ||
| 100 | + required String title, | ||
| 101 | + required String content, | ||
| 102 | + }) { | ||
| 103 | + return Container( | ||
| 104 | + height: 110, | ||
| 105 | + margin: EdgeInsets.symmetric(horizontal: 15), | ||
| 106 | + decoration: BoxDecoration( | ||
| 107 | + color: Colors.white, | ||
| 108 | + borderRadius: BorderRadius.circular(16), | ||
| 109 | + ), | ||
| 110 | + child: Column( | ||
| 111 | + children: [ | ||
| 112 | + SizedBox( | ||
| 113 | + height: 54, | ||
| 114 | + child: Padding( | ||
| 115 | + padding: EdgeInsets.symmetric(horizontal: 20), | ||
| 116 | + child: Row( | ||
| 117 | + children: [ | ||
| 118 | + Text( | ||
| 119 | + title, | ||
| 120 | + style: TextStyle( | ||
| 121 | + color: AppColors.textPrimary, | ||
| 122 | + fontSize: 14, | ||
| 123 | + fontWeight: FontWeight.w400, | ||
| 124 | + height: 1.4, | ||
| 125 | + ), | ||
| 126 | + ), | ||
| 127 | + SizedBox(width: 16), | ||
| 128 | + Expanded( | ||
| 129 | + child: Text( | ||
| 130 | + content, | ||
| 131 | + maxLines: 1, | ||
| 132 | + overflow: TextOverflow.ellipsis, | ||
| 133 | + textAlign: TextAlign.right, | ||
| 134 | + style: const TextStyle( | ||
| 135 | + color: AppColors.textSecondary, | ||
| 136 | + fontSize: 14, | ||
| 137 | + fontWeight: FontWeight.w400, | ||
| 138 | + height: 1.4, | ||
| 139 | + ), | ||
| 140 | + ), | ||
| 141 | + ), | ||
| 142 | + ], | ||
| 143 | + ), | ||
| 144 | + ), | ||
| 145 | + ), | ||
| 146 | + Padding( | ||
| 147 | + padding: EdgeInsets.symmetric(horizontal: 21), | ||
| 148 | + child: const Divider( | ||
| 149 | + height: 1, | ||
| 150 | + thickness: 0.5, | ||
| 151 | + color: Color(0xFFF3F3F3), | ||
| 152 | + ), | ||
| 153 | + ), | ||
| 154 | + Expanded( | ||
| 155 | + child: GestureDetector( | ||
| 156 | + behavior: HitTestBehavior.opaque, | ||
| 157 | + onTap: _logout, | ||
| 158 | + child: Center( | ||
| 159 | + child: Text( | ||
| 160 | + context.l10n.logOut, | ||
| 161 | + style: TextStyle( | ||
| 162 | + color: AppColors.primary, | ||
| 163 | + fontSize: 14, | ||
| 164 | + fontWeight: FontWeight.w500, | ||
| 165 | + height: 1.4, | ||
| 166 | + ), | ||
| 167 | + ), | ||
| 168 | + ), | ||
| 169 | + ), | ||
| 170 | + ), | ||
| 171 | + ], | ||
| 172 | + ), | ||
| 173 | + ); | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + Future<void> _logout() async { | ||
| 177 | + final confirmResult = await DialogUtils.showCommonDialog(DialogMetaData( | ||
| 178 | + title: l10n.logOut, | ||
| 179 | + message: l10n.confirmLogoutPrompt, | ||
| 180 | + confirmText: l10n.confirmLogout, | ||
| 181 | + iconAsset: null)); | ||
| 182 | + if (confirmResult.action != DialogAction.confirm) return; | ||
| 183 | + | ||
| 184 | + await LoadingService.instance.run(() async { | ||
| 185 | + await Get.find<UserStateService>().onLogout(); | ||
| 186 | + }); | ||
| 187 | + | ||
| 188 | + AppToast.show(l10n.loggedOutSuccessfully); | ||
| 189 | + Get.offAllNamed(AppRoutes.login); | ||
| 190 | + } | ||
| 191 | + | ||
| 192 | + Future<void> _deleteAccount() async { | ||
| 193 | + await LoadingService.instance.run(() async { | ||
| 194 | + final deleteResult = await Get.find<UserApi>().deleteAccount(); | ||
| 195 | + if (deleteResult is! AppSuccess<void>) return; | ||
| 196 | + | ||
| 197 | + await Get.find<UserStateService>().onLogout(callServerLogout: false); | ||
| 198 | + }); | ||
| 199 | + AppToast.show(l10n.accountDeletedSuccessfully); | ||
| 200 | + Get.offAllNamed(AppRoutes.login); | ||
| 201 | + } | ||
| 202 | +} | ||
| 203 | + | ||
| 204 | +class _DeleteAccountBottomSheet extends StatelessWidget { | ||
| 205 | + const _DeleteAccountBottomSheet({required this.onDeleteAccount}); | ||
| 206 | + | ||
| 207 | + final VoidCallback onDeleteAccount; | ||
| 208 | + | ||
| 209 | + @override | ||
| 210 | + Widget build(BuildContext context) { | ||
| 211 | + return Container( | ||
| 212 | + decoration: BoxDecoration( | ||
| 213 | + color: context.colors.backgroundPage, | ||
| 214 | + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | ||
| 215 | + ), | ||
| 216 | + child: SafeArea( | ||
| 217 | + top: false, | ||
| 218 | + child: Column( | ||
| 219 | + mainAxisSize: MainAxisSize.min, | ||
| 220 | + children: [ | ||
| 221 | + SizedBox(height: 8), | ||
| 222 | + SizedBox( | ||
| 223 | + height: 56, | ||
| 224 | + child: Stack( | ||
| 225 | + alignment: Alignment.center, | ||
| 226 | + children: [ | ||
| 227 | + Center( | ||
| 228 | + child: Padding( | ||
| 229 | + padding: EdgeInsets.symmetric(horizontal: 72), | ||
| 230 | + child: Text( | ||
| 231 | + AppLocalizations.of(context)! | ||
| 232 | + .areYouSureYouWantToDeleteYourAccount, | ||
| 233 | + maxLines: 1, | ||
| 234 | + overflow: TextOverflow.ellipsis, | ||
| 235 | + textAlign: TextAlign.center, | ||
| 236 | + style: TextStyle( | ||
| 237 | + color: context.colors.textPrimary, | ||
| 238 | + fontSize: 16, | ||
| 239 | + fontWeight: FontWeight.w600, | ||
| 240 | + height: 1.2, | ||
| 241 | + ), | ||
| 242 | + ), | ||
| 243 | + ), | ||
| 244 | + ), | ||
| 245 | + Positioned( | ||
| 246 | + left: 16, | ||
| 247 | + child: GestureDetector( | ||
| 248 | + behavior: HitTestBehavior.opaque, | ||
| 249 | + onTap: Get.back, | ||
| 250 | + child: SizedBox( | ||
| 251 | + width: 44, | ||
| 252 | + height: 44, | ||
| 253 | + child: Center( | ||
| 254 | + child: Image.asset( | ||
| 255 | + 'assets/images/common/ic_close.png', | ||
| 256 | + width: 20, | ||
| 257 | + height: 20, | ||
| 258 | + color: context.colors.chartPurple, | ||
| 259 | + ), | ||
| 260 | + ), | ||
| 261 | + ), | ||
| 262 | + ), | ||
| 263 | + ), | ||
| 264 | + ], | ||
| 265 | + ), | ||
| 266 | + ), | ||
| 267 | + Container( | ||
| 268 | + margin: EdgeInsets.symmetric(horizontal: 16), | ||
| 269 | + padding: EdgeInsets.fromLTRB(20, 20, 20, 18), | ||
| 270 | + decoration: BoxDecoration( | ||
| 271 | + color: Colors.white, | ||
| 272 | + borderRadius: BorderRadius.circular(16), | ||
| 273 | + ), | ||
| 274 | + child: Column( | ||
| 275 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 276 | + children: [ | ||
| 277 | + Text( | ||
| 278 | + context.l10n.deleteAccountWarningTitle, | ||
| 279 | + textAlign: TextAlign.center, | ||
| 280 | + style: TextStyle( | ||
| 281 | + color: context.colors.warning, | ||
| 282 | + fontSize: 14, | ||
| 283 | + fontWeight: FontWeight.w600, | ||
| 284 | + ), | ||
| 285 | + ), | ||
| 286 | + SizedBox( | ||
| 287 | + height: 12, | ||
| 288 | + ), | ||
| 289 | + Text( | ||
| 290 | + context.l10n.deleteAccountWarningPrompt, | ||
| 291 | + style: TextStyle( | ||
| 292 | + color: context.colors.textSecondary, | ||
| 293 | + fontSize: 12, | ||
| 294 | + fontWeight: FontWeight.w400, | ||
| 295 | + ), | ||
| 296 | + ), | ||
| 297 | + SizedBox( | ||
| 298 | + height: 12, | ||
| 299 | + ), | ||
| 300 | + Text( | ||
| 301 | + context.l10n.deleteAccountWarningNote1, | ||
| 302 | + style: TextStyle( | ||
| 303 | + color: context.colors.textSecondary, | ||
| 304 | + fontSize: 12, | ||
| 305 | + fontWeight: FontWeight.w400, | ||
| 306 | + ), | ||
| 307 | + ), | ||
| 308 | + SizedBox( | ||
| 309 | + height: 12, | ||
| 310 | + ), | ||
| 311 | + Text( | ||
| 312 | + context.l10n.deleteAccountWarningNote2, | ||
| 313 | + style: TextStyle( | ||
| 314 | + color: context.colors.textSecondary, | ||
| 315 | + fontSize: 12, | ||
| 316 | + fontWeight: FontWeight.w400, | ||
| 317 | + ), | ||
| 318 | + ) | ||
| 319 | + ], | ||
| 320 | + ), | ||
| 321 | + ), | ||
| 322 | + SizedBox( | ||
| 323 | + height: 45, | ||
| 324 | + ), | ||
| 325 | + GestureDetector( | ||
| 326 | + onTap: onDeleteAccount, | ||
| 327 | + child: Text( | ||
| 328 | + AppLocalizations.of(context)!.confirmDeletion, | ||
| 329 | + style: TextStyle( | ||
| 330 | + color: context.colors.warning, | ||
| 331 | + fontSize: 16, | ||
| 332 | + fontWeight: FontWeight.w500, | ||
| 333 | + ), | ||
| 334 | + ), | ||
| 335 | + ), | ||
| 336 | + GestureDetector( | ||
| 337 | + onTap: () { | ||
| 338 | + Get.back(); | ||
| 339 | + }, | ||
| 340 | + child: Container( | ||
| 341 | + width: 280, | ||
| 342 | + height: 48, | ||
| 343 | + margin: EdgeInsets.only(top: 17, bottom: 20), | ||
| 344 | + padding: | ||
| 345 | + const EdgeInsets.symmetric(horizontal: 24, vertical: 8), | ||
| 346 | + decoration: ShapeDecoration( | ||
| 347 | + color: context.colors.primary, | ||
| 348 | + shape: RoundedRectangleBorder( | ||
| 349 | + borderRadius: BorderRadius.circular(24), | ||
| 350 | + ), | ||
| 351 | + ), | ||
| 352 | + child: Row( | ||
| 353 | + mainAxisSize: MainAxisSize.min, | ||
| 354 | + mainAxisAlignment: MainAxisAlignment.center, | ||
| 355 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
| 356 | + spacing: 24, | ||
| 357 | + children: [ | ||
| 358 | + Text( | ||
| 359 | + context.l10n.iLlThinkAboutItSomeMore, | ||
| 360 | + style: TextStyle( | ||
| 361 | + color: Colors.white, | ||
| 362 | + fontSize: 16, | ||
| 363 | + fontWeight: FontWeight.w600, | ||
| 364 | + ), | ||
| 365 | + ), | ||
| 366 | + ], | ||
| 367 | + ), | ||
| 368 | + ), | ||
| 369 | + ) | ||
| 370 | + ], | ||
| 371 | + ), | ||
| 372 | + ), | ||
| 373 | + ); | ||
| 374 | + } | ||
| 375 | +} |
| 1 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; | 1 | import 'package:doublefeel_flutter/app/actions/dialog_action.dart'; |
| 2 | import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | 2 | import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; |
| 3 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; | 3 | import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart'; |
| 4 | +import 'package:doublefeel_flutter/app/modules/home/widgets/my/security_email_views.dart'; | ||
| 4 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 5 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 5 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | 6 | import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; |
| 6 | 7 | ||
| @@ -13,6 +14,7 @@ import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | @@ -13,6 +14,7 @@ import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | ||
| 13 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 14 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 14 | 15 | ||
| 15 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 16 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 17 | +import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | ||
| 16 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 18 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 17 | import 'package:flutter/material.dart'; | 19 | import 'package:flutter/material.dart'; |
| 18 | 20 | ||
| @@ -26,150 +28,96 @@ class AccountSettingView extends GetView<MyController> { | @@ -26,150 +28,96 @@ class AccountSettingView extends GetView<MyController> { | ||
| 26 | final userPrefs = Get.find<UserPreferencesStorage>(); | 28 | final userPrefs = Get.find<UserPreferencesStorage>(); |
| 27 | 29 | ||
| 28 | return Scaffold( | 30 | return Scaffold( |
| 31 | + backgroundColor: context.colors.backgroundPage, | ||
| 29 | appBar: AppBar( | 32 | appBar: AppBar( |
| 30 | title: Text( | 33 | title: Text( |
| 31 | context.l10n.accountSettings, | 34 | context.l10n.accountSettings, |
| 32 | ), | 35 | ), |
| 36 | + leadingWidth: 60, | ||
| 33 | leading: IconButton( | 37 | leading: IconButton( |
| 34 | highlightColor: Colors.transparent, | 38 | highlightColor: Colors.transparent, |
| 35 | padding: EdgeInsets.zero, | 39 | padding: EdgeInsets.zero, |
| 36 | onPressed: Get.back, | 40 | onPressed: Get.back, |
| 37 | icon: Image.asset( | 41 | icon: Image.asset( |
| 38 | 'assets/images/common/ic_nav_back.webp', | 42 | 'assets/images/common/ic_nav_back.webp', |
| 39 | - width: 24, | ||
| 40 | - height: 24, | 43 | + width: 28, |
| 44 | + height: 28, | ||
| 41 | ), | 45 | ), |
| 42 | ), | 46 | ), |
| 43 | ), | 47 | ), |
| 44 | body: Obx(() { | 48 | body: Obx(() { |
| 45 | - final telephone = | ||
| 46 | - userPrefs.preferences.value.meUserInfo?.telephone ?? ''; | ||
| 47 | - final thirdAccountId = | ||
| 48 | - userPrefs.preferences.value.meUserInfo?.thirdAccountId ?? ''; | 49 | + final user = userPrefs.preferences.value.meUserInfo; |
| 50 | + final linkedAccount = _linkedAccount(context, user); | ||
| 51 | + final telephone = user?.telephone?.trim() ?? ''; | ||
| 52 | + final securityEmail = user?.securityEmail?.trim() ?? ''; | ||
| 53 | + | ||
| 49 | return Column( | 54 | return Column( |
| 50 | children: [ | 55 | children: [ |
| 51 | - SizedBox(height: 16), | ||
| 52 | - if (thirdAccountId.isNotEmpty) | ||
| 53 | - _buildAccountCard( | ||
| 54 | - context: context, | ||
| 55 | - title: 'Apple ID', | ||
| 56 | - content: thirdAccountId, | ||
| 57 | - ) | ||
| 58 | - else | ||
| 59 | - _buildAccountCard( | ||
| 60 | - context: context, | 56 | + const SizedBox(height: 16), |
| 57 | + if (linkedAccount != null) ...[ | ||
| 58 | + _AccountInfoRow( | ||
| 59 | + title: linkedAccount.title, | ||
| 60 | + content: linkedAccount.content, | ||
| 61 | + ), | ||
| 62 | + const SizedBox(height: 12), | ||
| 63 | + ] else if (telephone.isNotEmpty) ...[ | ||
| 64 | + _AccountInfoRow( | ||
| 61 | title: context.l10n.mobilePhoneNumber, | 65 | title: context.l10n.mobilePhoneNumber, |
| 62 | content: telephone, | 66 | content: telephone, |
| 63 | ), | 67 | ), |
| 64 | - SizedBox(height: 20), | ||
| 65 | - GestureDetector( | ||
| 66 | - behavior: HitTestBehavior.opaque, | ||
| 67 | - onTap: () { | ||
| 68 | - Get.bottomSheet( | ||
| 69 | - _DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount), | ||
| 70 | - barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 71 | - enableDrag: true, | ||
| 72 | - isScrollControlled: true, | ||
| 73 | - persistent: false, | ||
| 74 | - ); | ||
| 75 | - }, | ||
| 76 | - child: Padding( | ||
| 77 | - padding: EdgeInsets.symmetric( | ||
| 78 | - horizontal: 24, | ||
| 79 | - vertical: 8, | ||
| 80 | - ), | ||
| 81 | - child: Text( | ||
| 82 | - context.l10n.deleteAccount, | ||
| 83 | - style: TextStyle( | ||
| 84 | - color: AppColors.warning, | ||
| 85 | - fontSize: 12, | ||
| 86 | - fontWeight: FontWeight.w400, | ||
| 87 | - height: 1.4, | ||
| 88 | - ), | ||
| 89 | - ), | ||
| 90 | - ), | 68 | + const SizedBox(height: 12), |
| 69 | + ], | ||
| 70 | + _SecurityEmailRow( | ||
| 71 | + email: securityEmail, | ||
| 72 | + onTap: () => Get.to(() => const AddSecurityEmailView()), | ||
| 73 | + ), | ||
| 74 | + const Spacer(), | ||
| 75 | + _AccountActionButton( | ||
| 76 | + label: context.l10n.logOut, | ||
| 77 | + color: context.colors.primary, | ||
| 78 | + backgroundColor: Colors.white, | ||
| 79 | + onTap: _logout, | ||
| 80 | + ), | ||
| 81 | + const SizedBox(height: 12), | ||
| 82 | + _AccountActionButton( | ||
| 83 | + label: context.l10n.deleteAccount, | ||
| 84 | + color: AppColors.warning, | ||
| 85 | + onTap: _showDeleteAccountSheet, | ||
| 91 | ), | 86 | ), |
| 87 | + SizedBox(height: MediaQuery.paddingOf(context).bottom + 40), | ||
| 92 | ], | 88 | ], |
| 93 | ); | 89 | ); |
| 94 | }), | 90 | }), |
| 95 | ); | 91 | ); |
| 96 | } | 92 | } |
| 97 | 93 | ||
| 98 | - Widget _buildAccountCard({ | ||
| 99 | - required BuildContext context, | ||
| 100 | - required String title, | ||
| 101 | - required String content, | ||
| 102 | - }) { | ||
| 103 | - return Container( | ||
| 104 | - height: 110, | ||
| 105 | - margin: EdgeInsets.symmetric(horizontal: 15), | ||
| 106 | - decoration: BoxDecoration( | ||
| 107 | - color: Colors.white, | ||
| 108 | - borderRadius: BorderRadius.circular(16), | ||
| 109 | - ), | ||
| 110 | - child: Column( | ||
| 111 | - children: [ | ||
| 112 | - SizedBox( | ||
| 113 | - height: 54, | ||
| 114 | - child: Padding( | ||
| 115 | - padding: EdgeInsets.symmetric(horizontal: 20), | ||
| 116 | - child: Row( | ||
| 117 | - children: [ | ||
| 118 | - Text( | ||
| 119 | - title, | ||
| 120 | - style: TextStyle( | ||
| 121 | - color: AppColors.textPrimary, | ||
| 122 | - fontSize: 14, | ||
| 123 | - fontWeight: FontWeight.w400, | ||
| 124 | - height: 1.4, | ||
| 125 | - ), | ||
| 126 | - ), | ||
| 127 | - SizedBox(width: 16), | ||
| 128 | - Expanded( | ||
| 129 | - child: Text( | ||
| 130 | - content, | ||
| 131 | - maxLines: 1, | ||
| 132 | - overflow: TextOverflow.ellipsis, | ||
| 133 | - textAlign: TextAlign.right, | ||
| 134 | - style: const TextStyle( | ||
| 135 | - color: AppColors.textSecondary, | ||
| 136 | - fontSize: 14, | ||
| 137 | - fontWeight: FontWeight.w400, | ||
| 138 | - height: 1.4, | ||
| 139 | - ), | ||
| 140 | - ), | ||
| 141 | - ), | ||
| 142 | - ], | ||
| 143 | - ), | ||
| 144 | - ), | ||
| 145 | - ), | ||
| 146 | - Padding( | ||
| 147 | - padding: EdgeInsets.symmetric(horizontal: 21), | ||
| 148 | - child: const Divider( | ||
| 149 | - height: 1, | ||
| 150 | - thickness: 0.5, | ||
| 151 | - color: Color(0xFFF3F3F3), | ||
| 152 | - ), | ||
| 153 | - ), | ||
| 154 | - Expanded( | ||
| 155 | - child: GestureDetector( | ||
| 156 | - behavior: HitTestBehavior.opaque, | ||
| 157 | - onTap: _logout, | ||
| 158 | - child: Center( | ||
| 159 | - child: Text( | ||
| 160 | - context.l10n.logOut, | ||
| 161 | - style: TextStyle( | ||
| 162 | - color: AppColors.primary, | ||
| 163 | - fontSize: 14, | ||
| 164 | - fontWeight: FontWeight.w500, | ||
| 165 | - height: 1.4, | ||
| 166 | - ), | ||
| 167 | - ), | ||
| 168 | - ), | ||
| 169 | - ), | ||
| 170 | - ), | ||
| 171 | - ], | ||
| 172 | - ), | 94 | + _LinkedAccount? _linkedAccount( |
| 95 | + BuildContext context, | ||
| 96 | + UserInfoResponse? user, | ||
| 97 | + ) { | ||
| 98 | + final accountId = user?.thirdAccountId?.trim() ?? ''; | ||
| 99 | + if (accountId.isEmpty) return null; | ||
| 100 | + | ||
| 101 | + if (accountId.toLowerCase().endsWith('@privaterelay.appleid.com')) { | ||
| 102 | + return _LinkedAccount(title: 'Apple ID', content: accountId); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + final provider = user?.thirdAccountType?.toLowerCase(); | ||
| 106 | + return _LinkedAccount( | ||
| 107 | + title: provider?.contains('google') == true | ||
| 108 | + ? context.l10n.googleAccount | ||
| 109 | + : context.l10n.appleAccount, | ||
| 110 | + content: accountId, | ||
| 111 | + ); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + void _showDeleteAccountSheet() { | ||
| 115 | + Get.bottomSheet( | ||
| 116 | + _DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount), | ||
| 117 | + barrierColor: Colors.black.withValues(alpha: 0.7), | ||
| 118 | + enableDrag: true, | ||
| 119 | + isScrollControlled: true, | ||
| 120 | + persistent: false, | ||
| 173 | ); | 121 | ); |
| 174 | } | 122 | } |
| 175 | 123 | ||
| @@ -201,6 +149,164 @@ class AccountSettingView extends GetView<MyController> { | @@ -201,6 +149,164 @@ class AccountSettingView extends GetView<MyController> { | ||
| 201 | } | 149 | } |
| 202 | } | 150 | } |
| 203 | 151 | ||
| 152 | +class _LinkedAccount { | ||
| 153 | + const _LinkedAccount({required this.title, required this.content}); | ||
| 154 | + | ||
| 155 | + final String title; | ||
| 156 | + final String content; | ||
| 157 | +} | ||
| 158 | + | ||
| 159 | +class _AccountInfoRow extends StatelessWidget { | ||
| 160 | + const _AccountInfoRow({required this.title, required this.content}); | ||
| 161 | + | ||
| 162 | + final String title; | ||
| 163 | + final String content; | ||
| 164 | + | ||
| 165 | + @override | ||
| 166 | + Widget build(BuildContext context) { | ||
| 167 | + return Container( | ||
| 168 | + height: 74, | ||
| 169 | + margin: const EdgeInsets.symmetric(horizontal: 15), | ||
| 170 | + padding: const EdgeInsets.symmetric(horizontal: 20), | ||
| 171 | + decoration: BoxDecoration( | ||
| 172 | + color: Colors.white, | ||
| 173 | + borderRadius: BorderRadius.circular(16), | ||
| 174 | + ), | ||
| 175 | + child: Row( | ||
| 176 | + children: [ | ||
| 177 | + Text( | ||
| 178 | + title, | ||
| 179 | + style: const TextStyle( | ||
| 180 | + color: AppColors.textPrimary, | ||
| 181 | + fontSize: 14, | ||
| 182 | + fontWeight: FontWeight.w400, | ||
| 183 | + height: 1.4, | ||
| 184 | + ), | ||
| 185 | + ), | ||
| 186 | + const SizedBox(width: 16), | ||
| 187 | + Expanded( | ||
| 188 | + child: Text( | ||
| 189 | + content, | ||
| 190 | + maxLines: 1, | ||
| 191 | + overflow: TextOverflow.ellipsis, | ||
| 192 | + textAlign: TextAlign.right, | ||
| 193 | + style: const TextStyle( | ||
| 194 | + color: AppColors.textSecondary, | ||
| 195 | + fontSize: 14, | ||
| 196 | + fontWeight: FontWeight.w400, | ||
| 197 | + height: 1.4, | ||
| 198 | + ), | ||
| 199 | + ), | ||
| 200 | + ), | ||
| 201 | + ], | ||
| 202 | + ), | ||
| 203 | + ); | ||
| 204 | + } | ||
| 205 | +} | ||
| 206 | + | ||
| 207 | +class _SecurityEmailRow extends StatelessWidget { | ||
| 208 | + const _SecurityEmailRow({required this.email, required this.onTap}); | ||
| 209 | + | ||
| 210 | + final String email; | ||
| 211 | + final VoidCallback onTap; | ||
| 212 | + | ||
| 213 | + @override | ||
| 214 | + Widget build(BuildContext context) { | ||
| 215 | + return Semantics( | ||
| 216 | + button: true, | ||
| 217 | + label: context.l10n.securityEmail, | ||
| 218 | + child: GestureDetector( | ||
| 219 | + behavior: HitTestBehavior.opaque, | ||
| 220 | + onTap: onTap, | ||
| 221 | + child: Container( | ||
| 222 | + height: 56, | ||
| 223 | + margin: const EdgeInsets.symmetric(horizontal: 16), | ||
| 224 | + padding: const EdgeInsets.symmetric(horizontal: 20), | ||
| 225 | + decoration: BoxDecoration( | ||
| 226 | + color: Colors.white, | ||
| 227 | + borderRadius: BorderRadius.circular(16), | ||
| 228 | + ), | ||
| 229 | + child: Row( | ||
| 230 | + children: [ | ||
| 231 | + Text( | ||
| 232 | + context.l10n.securityEmail, | ||
| 233 | + style: const TextStyle( | ||
| 234 | + color: AppColors.textPrimary, | ||
| 235 | + fontSize: 14, | ||
| 236 | + fontWeight: FontWeight.w400, | ||
| 237 | + height: 1.4, | ||
| 238 | + ), | ||
| 239 | + ), | ||
| 240 | + const SizedBox(width: 16), | ||
| 241 | + Expanded( | ||
| 242 | + child: Text( | ||
| 243 | + email, | ||
| 244 | + maxLines: 1, | ||
| 245 | + overflow: TextOverflow.ellipsis, | ||
| 246 | + textAlign: TextAlign.right, | ||
| 247 | + style: const TextStyle( | ||
| 248 | + color: AppColors.textSecondary, | ||
| 249 | + fontSize: 14, | ||
| 250 | + fontWeight: FontWeight.w400, | ||
| 251 | + height: 1.4, | ||
| 252 | + ), | ||
| 253 | + ), | ||
| 254 | + ), | ||
| 255 | + const SizedBox(width: 8), | ||
| 256 | + Image.asset( | ||
| 257 | + 'assets/images/common/ic_more.png', | ||
| 258 | + width: 16, | ||
| 259 | + height: 16, | ||
| 260 | + ), | ||
| 261 | + ], | ||
| 262 | + ), | ||
| 263 | + ), | ||
| 264 | + ), | ||
| 265 | + ); | ||
| 266 | + } | ||
| 267 | +} | ||
| 268 | + | ||
| 269 | +class _AccountActionButton extends StatelessWidget { | ||
| 270 | + const _AccountActionButton({ | ||
| 271 | + required this.label, | ||
| 272 | + required this.color, | ||
| 273 | + required this.onTap, | ||
| 274 | + this.backgroundColor = Colors.transparent, | ||
| 275 | + }); | ||
| 276 | + | ||
| 277 | + final String label; | ||
| 278 | + final Color color; | ||
| 279 | + final Color backgroundColor; | ||
| 280 | + final VoidCallback onTap; | ||
| 281 | + | ||
| 282 | + @override | ||
| 283 | + Widget build(BuildContext context) { | ||
| 284 | + return SizedBox( | ||
| 285 | + width: 280, | ||
| 286 | + height: 48, | ||
| 287 | + child: Material( | ||
| 288 | + color: backgroundColor, | ||
| 289 | + borderRadius: BorderRadius.circular(24), | ||
| 290 | + child: InkWell( | ||
| 291 | + borderRadius: BorderRadius.circular(24), | ||
| 292 | + onTap: onTap, | ||
| 293 | + child: Center( | ||
| 294 | + child: Text( | ||
| 295 | + label, | ||
| 296 | + style: TextStyle( | ||
| 297 | + color: color, | ||
| 298 | + fontSize: 16, | ||
| 299 | + fontWeight: FontWeight.w600, | ||
| 300 | + height: 1.25, | ||
| 301 | + ), | ||
| 302 | + ), | ||
| 303 | + ), | ||
| 304 | + ), | ||
| 305 | + ), | ||
| 306 | + ); | ||
| 307 | + } | ||
| 308 | +} | ||
| 309 | + | ||
| 204 | class _DeleteAccountBottomSheet extends StatelessWidget { | 310 | class _DeleteAccountBottomSheet extends StatelessWidget { |
| 205 | const _DeleteAccountBottomSheet({required this.onDeleteAccount}); | 311 | const _DeleteAccountBottomSheet({required this.onDeleteAccount}); |
| 206 | 312 |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 4 | +import 'package:flutter/material.dart'; | ||
| 5 | +import 'package:flutter/services.dart'; | ||
| 6 | +import 'package:get/get.dart'; | ||
| 7 | + | ||
| 8 | +const _securityEmailPageColor = Color(0xFFF5F2FF); | ||
| 9 | +const _securityEmailTitleColor = Color(0xFF0F0F11); | ||
| 10 | +const _securityEmailDescriptionColor = Color(0xFF78787D); | ||
| 11 | +const _securityEmailBrandColor = Color(0xFF845EEE); | ||
| 12 | + | ||
| 13 | +class AddSecurityEmailView extends StatefulWidget { | ||
| 14 | + const AddSecurityEmailView({super.key}); | ||
| 15 | + | ||
| 16 | + @override | ||
| 17 | + State<AddSecurityEmailView> createState() => _AddSecurityEmailViewState(); | ||
| 18 | +} | ||
| 19 | + | ||
| 20 | +class _AddSecurityEmailViewState extends State<AddSecurityEmailView> { | ||
| 21 | + static final _emailPattern = RegExp( | ||
| 22 | + r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$', | ||
| 23 | + ); | ||
| 24 | + | ||
| 25 | + final _emailController = TextEditingController(); | ||
| 26 | + bool _hasSubmitted = false; | ||
| 27 | + | ||
| 28 | + bool get _isEmailValid => | ||
| 29 | + _emailPattern.hasMatch(_emailController.text.trim()); | ||
| 30 | + | ||
| 31 | + @override | ||
| 32 | + void dispose() { | ||
| 33 | + _emailController.dispose(); | ||
| 34 | + super.dispose(); | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + void _sendVerificationEmail() { | ||
| 38 | + setState(() => _hasSubmitted = true); | ||
| 39 | + if (!_isEmailValid) return; | ||
| 40 | + | ||
| 41 | + Get.to( | ||
| 42 | + () => SecurityEmailVerificationView( | ||
| 43 | + email: _emailController.text.trim(), | ||
| 44 | + ), | ||
| 45 | + ); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + @override | ||
| 49 | + Widget build(BuildContext context) { | ||
| 50 | + final l10n = context.l10n; | ||
| 51 | + final showError = _hasSubmitted && !_isEmailValid; | ||
| 52 | + | ||
| 53 | + return AnnotatedRegion<SystemUiOverlayStyle>( | ||
| 54 | + value: const SystemUiOverlayStyle( | ||
| 55 | + statusBarColor: Colors.transparent, | ||
| 56 | + statusBarIconBrightness: Brightness.dark, | ||
| 57 | + statusBarBrightness: Brightness.light, | ||
| 58 | + systemNavigationBarColor: _securityEmailPageColor, | ||
| 59 | + systemNavigationBarIconBrightness: Brightness.dark, | ||
| 60 | + ), | ||
| 61 | + child: Scaffold( | ||
| 62 | + backgroundColor: _securityEmailPageColor, | ||
| 63 | + appBar: const _SecurityEmailAppBar(), | ||
| 64 | + body: SafeArea( | ||
| 65 | + top: false, | ||
| 66 | + child: Padding( | ||
| 67 | + padding: const EdgeInsets.symmetric(horizontal: 28), | ||
| 68 | + child: Column( | ||
| 69 | + children: [ | ||
| 70 | + const SizedBox(height: 16), | ||
| 71 | + Text( | ||
| 72 | + l10n.addSecurityEmail, | ||
| 73 | + style: const TextStyle( | ||
| 74 | + color: _securityEmailTitleColor, | ||
| 75 | + fontSize: 24, | ||
| 76 | + fontWeight: FontWeight.w600, | ||
| 77 | + height: 1.25, | ||
| 78 | + ), | ||
| 79 | + ), | ||
| 80 | + const SizedBox(height: 8), | ||
| 81 | + Text( | ||
| 82 | + l10n.securityEmailDescription, | ||
| 83 | + textAlign: TextAlign.center, | ||
| 84 | + style: const TextStyle( | ||
| 85 | + color: _securityEmailDescriptionColor, | ||
| 86 | + fontSize: 14, | ||
| 87 | + fontWeight: FontWeight.w400, | ||
| 88 | + height: 1.43, | ||
| 89 | + ), | ||
| 90 | + ), | ||
| 91 | + const SizedBox(height: 32), | ||
| 92 | + TextField( | ||
| 93 | + controller: _emailController, | ||
| 94 | + keyboardType: TextInputType.emailAddress, | ||
| 95 | + autofillHints: const [AutofillHints.email], | ||
| 96 | + inputFormatters: [ | ||
| 97 | + FilteringTextInputFormatter.deny(RegExp(r'\s')), | ||
| 98 | + ], | ||
| 99 | + onChanged: (_) => setState(() {}), | ||
| 100 | + onSubmitted: (_) => _sendVerificationEmail(), | ||
| 101 | + style: const TextStyle( | ||
| 102 | + color: _securityEmailTitleColor, | ||
| 103 | + fontSize: 16, | ||
| 104 | + fontWeight: FontWeight.w400, | ||
| 105 | + ), | ||
| 106 | + decoration: InputDecoration( | ||
| 107 | + hintText: l10n.securityEmailHint, | ||
| 108 | + hintStyle: const TextStyle( | ||
| 109 | + color: Color(0xFFD9D9D9), | ||
| 110 | + fontSize: 16, | ||
| 111 | + ), | ||
| 112 | + filled: true, | ||
| 113 | + fillColor: Colors.white, | ||
| 114 | + contentPadding: const EdgeInsets.symmetric( | ||
| 115 | + horizontal: 20, | ||
| 116 | + ), | ||
| 117 | + errorText: showError ? l10n.invalidEmailFormat : null, | ||
| 118 | + errorStyle: const TextStyle(fontSize: 12), | ||
| 119 | + border: _inputBorder(), | ||
| 120 | + enabledBorder: _inputBorder(), | ||
| 121 | + focusedBorder: _inputBorder( | ||
| 122 | + color: _securityEmailBrandColor, | ||
| 123 | + ), | ||
| 124 | + ), | ||
| 125 | + ), | ||
| 126 | + const SizedBox(height: 32), | ||
| 127 | + SizedBox( | ||
| 128 | + width: double.infinity, | ||
| 129 | + height: 48, | ||
| 130 | + child: ElevatedButton( | ||
| 131 | + onPressed: _isEmailValid ? _sendVerificationEmail : null, | ||
| 132 | + style: ElevatedButton.styleFrom( | ||
| 133 | + backgroundColor: _securityEmailBrandColor, | ||
| 134 | + disabledBackgroundColor: | ||
| 135 | + _securityEmailBrandColor.withValues(alpha: 0.4), | ||
| 136 | + foregroundColor: Colors.white, | ||
| 137 | + disabledForegroundColor: Colors.white, | ||
| 138 | + elevation: 0, | ||
| 139 | + shape: const StadiumBorder(), | ||
| 140 | + textStyle: const TextStyle( | ||
| 141 | + fontSize: 16, | ||
| 142 | + fontWeight: FontWeight.w600, | ||
| 143 | + ), | ||
| 144 | + ), | ||
| 145 | + child: Text(l10n.sendVerificationEmail), | ||
| 146 | + ), | ||
| 147 | + ), | ||
| 148 | + ], | ||
| 149 | + ), | ||
| 150 | + ), | ||
| 151 | + ), | ||
| 152 | + ), | ||
| 153 | + ); | ||
| 154 | + } | ||
| 155 | +} | ||
| 156 | + | ||
| 157 | +class SecurityEmailVerificationView extends StatefulWidget { | ||
| 158 | + const SecurityEmailVerificationView({ | ||
| 159 | + super.key, | ||
| 160 | + required this.email, | ||
| 161 | + }); | ||
| 162 | + | ||
| 163 | + final String email; | ||
| 164 | + | ||
| 165 | + @override | ||
| 166 | + State<SecurityEmailVerificationView> createState() => | ||
| 167 | + _SecurityEmailVerificationViewState(); | ||
| 168 | +} | ||
| 169 | + | ||
| 170 | +class _SecurityEmailVerificationViewState | ||
| 171 | + extends State<SecurityEmailVerificationView> { | ||
| 172 | + static const _resendSeconds = 60; | ||
| 173 | + | ||
| 174 | + final _codeController = TextEditingController(); | ||
| 175 | + Timer? _resendTimer; | ||
| 176 | + int _secondsRemaining = _resendSeconds; | ||
| 177 | + | ||
| 178 | + bool get _canSubmit => _codeController.text.trim().isNotEmpty; | ||
| 179 | + | ||
| 180 | + @override | ||
| 181 | + void initState() { | ||
| 182 | + super.initState(); | ||
| 183 | + _startResendCountdown(); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + @override | ||
| 187 | + void dispose() { | ||
| 188 | + _resendTimer?.cancel(); | ||
| 189 | + _codeController.dispose(); | ||
| 190 | + super.dispose(); | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + void _startResendCountdown() { | ||
| 194 | + _resendTimer?.cancel(); | ||
| 195 | + setState(() => _secondsRemaining = _resendSeconds); | ||
| 196 | + _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { | ||
| 197 | + if (_secondsRemaining <= 1) { | ||
| 198 | + timer.cancel(); | ||
| 199 | + setState(() => _secondsRemaining = 0); | ||
| 200 | + return; | ||
| 201 | + } | ||
| 202 | + setState(() => _secondsRemaining -= 1); | ||
| 203 | + }); | ||
| 204 | + } | ||
| 205 | + | ||
| 206 | + void _submitCode() { | ||
| 207 | + if (!_canSubmit) return; | ||
| 208 | + FocusScope.of(context).unfocus(); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + @override | ||
| 212 | + Widget build(BuildContext context) { | ||
| 213 | + final l10n = context.l10n; | ||
| 214 | + | ||
| 215 | + return AnnotatedRegion<SystemUiOverlayStyle>( | ||
| 216 | + value: const SystemUiOverlayStyle( | ||
| 217 | + statusBarColor: Colors.transparent, | ||
| 218 | + statusBarIconBrightness: Brightness.dark, | ||
| 219 | + statusBarBrightness: Brightness.light, | ||
| 220 | + systemNavigationBarColor: _securityEmailPageColor, | ||
| 221 | + systemNavigationBarIconBrightness: Brightness.dark, | ||
| 222 | + ), | ||
| 223 | + child: Scaffold( | ||
| 224 | + backgroundColor: _securityEmailPageColor, | ||
| 225 | + appBar: const _SecurityEmailAppBar(), | ||
| 226 | + body: SafeArea( | ||
| 227 | + top: false, | ||
| 228 | + child: Padding( | ||
| 229 | + padding: const EdgeInsets.symmetric(horizontal: 28), | ||
| 230 | + child: Column( | ||
| 231 | + children: [ | ||
| 232 | + const SizedBox(height: 16), | ||
| 233 | + Text( | ||
| 234 | + l10n.confirmYourEmail, | ||
| 235 | + style: const TextStyle( | ||
| 236 | + color: _securityEmailTitleColor, | ||
| 237 | + fontSize: 24, | ||
| 238 | + fontWeight: FontWeight.w600, | ||
| 239 | + height: 1.25, | ||
| 240 | + ), | ||
| 241 | + ), | ||
| 242 | + const SizedBox(height: 7), | ||
| 243 | + Text.rich( | ||
| 244 | + _verificationInstruction( | ||
| 245 | + l10n.enterCodeSentTo(widget.email), | ||
| 246 | + widget.email, | ||
| 247 | + ), | ||
| 248 | + textAlign: TextAlign.center, | ||
| 249 | + ), | ||
| 250 | + const SizedBox(height: 47), | ||
| 251 | + Container( | ||
| 252 | + height: 52, | ||
| 253 | + decoration: BoxDecoration( | ||
| 254 | + color: Colors.white, | ||
| 255 | + borderRadius: BorderRadius.circular(27), | ||
| 256 | + ), | ||
| 257 | + child: Stack( | ||
| 258 | + children: [ | ||
| 259 | + TextField( | ||
| 260 | + controller: _codeController, | ||
| 261 | + keyboardType: TextInputType.number, | ||
| 262 | + textInputAction: TextInputAction.done, | ||
| 263 | + maxLength: 6, | ||
| 264 | + inputFormatters: [ | ||
| 265 | + FilteringTextInputFormatter.digitsOnly | ||
| 266 | + ], | ||
| 267 | + onChanged: (_) => setState(() {}), | ||
| 268 | + onSubmitted: (_) => _submitCode(), | ||
| 269 | + style: const TextStyle( | ||
| 270 | + color: _securityEmailTitleColor, | ||
| 271 | + fontSize: 16, | ||
| 272 | + fontWeight: FontWeight.w400, | ||
| 273 | + ), | ||
| 274 | + decoration: InputDecoration( | ||
| 275 | + hintText: l10n.verificationCode, | ||
| 276 | + hintStyle: const TextStyle( | ||
| 277 | + color: Color(0xFFD9D9D9), | ||
| 278 | + fontSize: 16, | ||
| 279 | + ), | ||
| 280 | + counterText: '', | ||
| 281 | + contentPadding: const EdgeInsets.only( | ||
| 282 | + left: 20, | ||
| 283 | + right: 112, | ||
| 284 | + ), | ||
| 285 | + border: InputBorder.none, | ||
| 286 | + ), | ||
| 287 | + ), | ||
| 288 | + Positioned( | ||
| 289 | + top: 0, | ||
| 290 | + right: 0, | ||
| 291 | + bottom: 0, | ||
| 292 | + child: TextButton( | ||
| 293 | + onPressed: _secondsRemaining == 0 | ||
| 294 | + ? _startResendCountdown | ||
| 295 | + : null, | ||
| 296 | + style: TextButton.styleFrom( | ||
| 297 | + foregroundColor: _securityEmailBrandColor, | ||
| 298 | + disabledForegroundColor: const Color(0xFFD9D9D9), | ||
| 299 | + padding: const EdgeInsets.symmetric(horizontal: 20), | ||
| 300 | + textStyle: const TextStyle( | ||
| 301 | + fontSize: 16, | ||
| 302 | + fontWeight: FontWeight.w500, | ||
| 303 | + ), | ||
| 304 | + ), | ||
| 305 | + child: Text( | ||
| 306 | + _secondsRemaining == 0 | ||
| 307 | + ? l10n.resend | ||
| 308 | + : l10n.resendWithSeconds(_secondsRemaining), | ||
| 309 | + ), | ||
| 310 | + ), | ||
| 311 | + ), | ||
| 312 | + ], | ||
| 313 | + ), | ||
| 314 | + ), | ||
| 315 | + const SizedBox(height: 8), | ||
| 316 | + Text( | ||
| 317 | + l10n.emailVerificationHelp, | ||
| 318 | + textAlign: TextAlign.center, | ||
| 319 | + style: const TextStyle( | ||
| 320 | + color: _securityEmailDescriptionColor, | ||
| 321 | + fontSize: 14, | ||
| 322 | + fontWeight: FontWeight.w400, | ||
| 323 | + height: 1.2, | ||
| 324 | + ), | ||
| 325 | + ), | ||
| 326 | + const SizedBox(height: 64), | ||
| 327 | + SizedBox( | ||
| 328 | + width: double.infinity, | ||
| 329 | + height: 48, | ||
| 330 | + child: ElevatedButton( | ||
| 331 | + onPressed: _canSubmit ? _submitCode : null, | ||
| 332 | + style: ElevatedButton.styleFrom( | ||
| 333 | + backgroundColor: _securityEmailBrandColor, | ||
| 334 | + disabledBackgroundColor: | ||
| 335 | + _securityEmailBrandColor.withValues(alpha: 0.4), | ||
| 336 | + foregroundColor: Colors.white, | ||
| 337 | + disabledForegroundColor: Colors.white, | ||
| 338 | + elevation: 0, | ||
| 339 | + shape: const StadiumBorder(), | ||
| 340 | + textStyle: const TextStyle( | ||
| 341 | + fontSize: 16, | ||
| 342 | + fontWeight: FontWeight.w600, | ||
| 343 | + ), | ||
| 344 | + ), | ||
| 345 | + child: Text(l10n.submit), | ||
| 346 | + ), | ||
| 347 | + ), | ||
| 348 | + ], | ||
| 349 | + ), | ||
| 350 | + ), | ||
| 351 | + ), | ||
| 352 | + ), | ||
| 353 | + ); | ||
| 354 | + } | ||
| 355 | +} | ||
| 356 | + | ||
| 357 | +TextSpan _verificationInstruction(String instruction, String email) { | ||
| 358 | + const textStyle = TextStyle( | ||
| 359 | + color: _securityEmailDescriptionColor, | ||
| 360 | + fontSize: 14, | ||
| 361 | + fontWeight: FontWeight.w400, | ||
| 362 | + height: 1.43, | ||
| 363 | + ); | ||
| 364 | + final emailStart = instruction.indexOf(email); | ||
| 365 | + if (emailStart < 0) return TextSpan(text: instruction, style: textStyle); | ||
| 366 | + final emailEnd = emailStart + email.length; | ||
| 367 | + | ||
| 368 | + return TextSpan( | ||
| 369 | + style: textStyle, | ||
| 370 | + children: [ | ||
| 371 | + TextSpan(text: instruction.substring(0, emailStart)), | ||
| 372 | + TextSpan( | ||
| 373 | + text: instruction.substring(emailStart, emailEnd), | ||
| 374 | + style: const TextStyle(color: _securityEmailTitleColor), | ||
| 375 | + ), | ||
| 376 | + TextSpan(text: instruction.substring(emailEnd)), | ||
| 377 | + ], | ||
| 378 | + ); | ||
| 379 | +} | ||
| 380 | + | ||
| 381 | +class _SecurityEmailAppBar extends StatelessWidget | ||
| 382 | + implements PreferredSizeWidget { | ||
| 383 | + const _SecurityEmailAppBar(); | ||
| 384 | + | ||
| 385 | + @override | ||
| 386 | + Size get preferredSize => const Size.fromHeight(44); | ||
| 387 | + | ||
| 388 | + @override | ||
| 389 | + Widget build(BuildContext context) { | ||
| 390 | + return AppBar( | ||
| 391 | + backgroundColor: _securityEmailPageColor, | ||
| 392 | + elevation: 0, | ||
| 393 | + scrolledUnderElevation: 0, | ||
| 394 | + toolbarHeight: 44, | ||
| 395 | + leadingWidth: 60, | ||
| 396 | + leading: IconButton( | ||
| 397 | + onPressed: Get.back, | ||
| 398 | + splashColor: Colors.transparent, | ||
| 399 | + highlightColor: Colors.transparent, | ||
| 400 | + icon: Image.asset( | ||
| 401 | + 'assets/images/common/ic_nav_back.webp', | ||
| 402 | + width: 28, | ||
| 403 | + height: 28, | ||
| 404 | + ), | ||
| 405 | + ), | ||
| 406 | + ); | ||
| 407 | + } | ||
| 408 | +} | ||
| 409 | + | ||
| 410 | +OutlineInputBorder _inputBorder({Color color = Colors.transparent}) { | ||
| 411 | + return OutlineInputBorder( | ||
| 412 | + borderRadius: BorderRadius.circular(27), | ||
| 413 | + borderSide: BorderSide(color: color), | ||
| 414 | + ); | ||
| 415 | +} |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | +import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | ||
| 3 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; | 4 | import 'package:doublefeel_flutter/core/theme/app_theme.dart'; |
| 4 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 5 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 5 | import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | 6 | import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; |
| @@ -150,7 +151,9 @@ class TodayHrvChartCard extends StatelessWidget { | @@ -150,7 +151,9 @@ class TodayHrvChartCard extends StatelessWidget { | ||
| 150 | Builder(builder: (context) { | 151 | Builder(builder: (context) { |
| 151 | final s = (sameDay | 152 | final s = (sameDay |
| 152 | ? context.l10n.todaySHrvTrend | 153 | ? context.l10n.todaySHrvTrend |
| 153 | - : context.l10n.hrvTrendForThatDay); | 154 | + : controller.environmentConfig.region.value != AppRegion.china |
| 155 | + ? context.l10n.todaySHrvTrend | ||
| 156 | + : context.l10n.hrvTrendForThatDay); | ||
| 154 | return Text( | 157 | return Text( |
| 155 | (controller.isFriend | 158 | (controller.isFriend |
| 156 | ? context.l10n.reportOtherPossessiveTitle(s) | 159 | ? context.l10n.reportOtherPossessiveTitle(s) |
| @@ -189,6 +189,11 @@ class LoginController extends GetxController { | @@ -189,6 +189,11 @@ class LoginController extends GetxController { | ||
| 189 | Get.toNamed(AppRoutes.debugEnvironment); | 189 | Get.toNamed(AppRoutes.debugEnvironment); |
| 190 | } | 190 | } |
| 191 | 191 | ||
| 192 | + Future<void> selectServiceRegion(AppRegion region) async { | ||
| 193 | + if (environmentConfig.region.value == region) return; | ||
| 194 | + await environmentConfig.setRegion(region); | ||
| 195 | + } | ||
| 196 | + | ||
| 192 | void openUserTerms() { | 197 | void openUserTerms() { |
| 193 | Get.toNamed( | 198 | Get.toNamed( |
| 194 | AppRoutes.webview, | 199 | AppRoutes.webview, |
| @@ -420,6 +425,7 @@ class LoginController extends GetxController { | @@ -420,6 +425,7 @@ class LoginController extends GetxController { | ||
| 420 | if (loginRes is AppSuccess<LoginResponse>) { | 425 | if (loginRes is AppSuccess<LoginResponse>) { |
| 421 | await _handleLoginSuccess(loginRes.data, appleModel: appleModel); | 426 | await _handleLoginSuccess(loginRes.data, appleModel: appleModel); |
| 422 | await Get.find<LocalStorage>().setLastLoginMethod('apple'); | 427 | await Get.find<LocalStorage>().setLastLoginMethod('apple'); |
| 428 | + await environmentConfig.updateHideChooseRegion(true); | ||
| 423 | } | 429 | } |
| 424 | }); | 430 | }); |
| 425 | } finally { | 431 | } finally { |
| 1 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 1 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| 2 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 2 | import 'package:flutter/material.dart'; | 3 | import 'package:flutter/material.dart'; |
| 3 | import 'package:flutter/services.dart'; | 4 | import 'package:flutter/services.dart'; |
| 4 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 5 | 6 | ||
| 6 | void showEmailLoginContactUsBottomSheet({ | 7 | void showEmailLoginContactUsBottomSheet({ |
| 7 | required String userId, | 8 | required String userId, |
| 8 | - VoidCallback? onSendEmail, | 9 | + String pageName = 'Setting page', |
| 9 | }) { | 10 | }) { |
| 10 | Get.bottomSheet( | 11 | Get.bottomSheet( |
| 11 | EmailLoginContactUsBottomSheet( | 12 | EmailLoginContactUsBottomSheet( |
| 12 | userId: userId, | 13 | userId: userId, |
| 13 | - onSendEmail: onSendEmail, | 14 | + pageName: pageName, |
| 14 | ), | 15 | ), |
| 15 | barrierColor: Colors.black.withValues(alpha: 0.7), | 16 | barrierColor: Colors.black.withValues(alpha: 0.7), |
| 16 | enableDrag: true, | 17 | enableDrag: true, |
| @@ -23,11 +24,11 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | @@ -23,11 +24,11 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | ||
| 23 | const EmailLoginContactUsBottomSheet({ | 24 | const EmailLoginContactUsBottomSheet({ |
| 24 | super.key, | 25 | super.key, |
| 25 | required this.userId, | 26 | required this.userId, |
| 26 | - this.onSendEmail, | 27 | + this.pageName = 'Setting page', |
| 27 | }); | 28 | }); |
| 28 | 29 | ||
| 29 | final String userId; | 30 | final String userId; |
| 30 | - final VoidCallback? onSendEmail; | 31 | + final String pageName; |
| 31 | 32 | ||
| 32 | @override | 33 | @override |
| 33 | Widget build(BuildContext context) { | 34 | Widget build(BuildContext context) { |
| @@ -51,22 +52,22 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | @@ -51,22 +52,22 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | ||
| 51 | ), | 52 | ), |
| 52 | ), | 53 | ), |
| 53 | const SizedBox(height: 17), | 54 | const SizedBox(height: 17), |
| 54 | - const Text( | ||
| 55 | - 'Contact us', | ||
| 56 | - style: TextStyle( | 55 | + Text( |
| 56 | + context.l10n.contactUs, | ||
| 57 | + style: const TextStyle( | ||
| 57 | color: Color(0xFF141414), | 58 | color: Color(0xFF141414), |
| 58 | fontSize: 16, | 59 | fontSize: 16, |
| 59 | fontWeight: FontWeight.w700, | 60 | fontWeight: FontWeight.w700, |
| 60 | ), | 61 | ), |
| 61 | ), | 62 | ), |
| 62 | const SizedBox(height: 22), | 63 | const SizedBox(height: 22), |
| 63 | - const Padding( | 64 | + Padding( |
| 64 | padding: EdgeInsets.symmetric(horizontal: 16), | 65 | padding: EdgeInsets.symmetric(horizontal: 16), |
| 65 | child: Align( | 66 | child: Align( |
| 66 | alignment: Alignment.centerLeft, | 67 | alignment: Alignment.centerLeft, |
| 67 | child: Text( | 68 | child: Text( |
| 68 | - 'Please describe the problem clearly and include screen ' | ||
| 69 | - 'recordings if possible.', | 69 | + context.l10n |
| 70 | + .pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible, | ||
| 70 | style: TextStyle( | 71 | style: TextStyle( |
| 71 | color: Color(0xFF141414), | 72 | color: Color(0xFF141414), |
| 72 | fontSize: 15, | 73 | fontSize: 15, |
| @@ -76,13 +77,13 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | @@ -76,13 +77,13 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | ||
| 76 | ), | 77 | ), |
| 77 | ), | 78 | ), |
| 78 | const SizedBox(height: 8), | 79 | const SizedBox(height: 8), |
| 79 | - const Padding( | 80 | + Padding( |
| 80 | padding: EdgeInsets.symmetric(horizontal: 16), | 81 | padding: EdgeInsets.symmetric(horizontal: 16), |
| 81 | child: Align( | 82 | child: Align( |
| 82 | alignment: Alignment.centerLeft, | 83 | alignment: Alignment.centerLeft, |
| 83 | child: Text( | 84 | child: Text( |
| 84 | - 'Send us your User ID as it will help us identify\n' | ||
| 85 | - 'the problem faster.', | 85 | + context.l10n |
| 86 | + .sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster, | ||
| 86 | style: TextStyle( | 87 | style: TextStyle( |
| 87 | color: Color(0xFF141414), | 88 | color: Color(0xFF141414), |
| 88 | fontSize: 15, | 89 | fontSize: 15, |
| @@ -98,7 +99,10 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | @@ -98,7 +99,10 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | ||
| 98 | _SendEmailButton( | 99 | _SendEmailButton( |
| 99 | onTap: () { | 100 | onTap: () { |
| 100 | Get.back(); | 101 | Get.back(); |
| 101 | - onSendEmail?.call(); | 102 | + sendFeedbackEmail( |
| 103 | + userId: userId, | ||
| 104 | + pageName: pageName, | ||
| 105 | + ); | ||
| 102 | }, | 106 | }, |
| 103 | ), | 107 | ), |
| 104 | const Spacer(), | 108 | const Spacer(), |
| @@ -109,6 +113,38 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | @@ -109,6 +113,38 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget { | ||
| 109 | } | 113 | } |
| 110 | } | 114 | } |
| 111 | 115 | ||
| 116 | +Future<void> sendFeedbackEmail({ | ||
| 117 | + required String userId, | ||
| 118 | + String pageName = 'Setting page', | ||
| 119 | +}) async { | ||
| 120 | + final String cleanUserId = userId.trim().isEmpty ? 'N/A' : userId.trim(); | ||
| 121 | + final String body = | ||
| 122 | + 'This issue was reported from $pageName by User ID: $cleanUserId.'; | ||
| 123 | + | ||
| 124 | + final Uri emailUri = Uri( | ||
| 125 | + scheme: 'mailto', | ||
| 126 | + path: 'support@doublefeel.cn', | ||
| 127 | + query: _encodeQueryParameters({ | ||
| 128 | + 'subject': 'DoubleFeel Feedback', | ||
| 129 | + 'body': body, | ||
| 130 | + }), | ||
| 131 | + ); | ||
| 132 | + | ||
| 133 | + // try { | ||
| 134 | + // await launchUrl( | ||
| 135 | + // emailUri, | ||
| 136 | + // mode: LaunchMode.externalApplication, | ||
| 137 | + // ); | ||
| 138 | + // } catch (_) {} | ||
| 139 | +} | ||
| 140 | + | ||
| 141 | +String _encodeQueryParameters(Map<String, String> params) { | ||
| 142 | + return params.entries | ||
| 143 | + .map((e) => | ||
| 144 | + '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') | ||
| 145 | + .join('&'); | ||
| 146 | +} | ||
| 147 | + | ||
| 112 | class _UserIdField extends StatelessWidget { | 148 | class _UserIdField extends StatelessWidget { |
| 113 | const _UserIdField({required this.userId}); | 149 | const _UserIdField({required this.userId}); |
| 114 | 150 | ||
| @@ -155,7 +191,7 @@ class _UserIdField extends StatelessWidget { | @@ -155,7 +191,7 @@ class _UserIdField extends StatelessWidget { | ||
| 155 | height: 48, | 191 | height: 48, |
| 156 | child: Center( | 192 | child: Center( |
| 157 | child: Image.asset( | 193 | child: Image.asset( |
| 158 | - 'assets/images/common/ic_contact_us_copy.png', | 194 | + 'assets/images/common/ic_contact_us_copy_2.png', |
| 159 | width: 24, | 195 | width: 24, |
| 160 | height: 24, | 196 | height: 24, |
| 161 | ), | 197 | ), |
| @@ -191,7 +227,7 @@ class _SendEmailButton extends StatelessWidget { | @@ -191,7 +227,7 @@ class _SendEmailButton extends StatelessWidget { | ||
| 191 | children: [ | 227 | children: [ |
| 192 | Image( | 228 | Image( |
| 193 | image: AssetImage( | 229 | image: AssetImage( |
| 194 | - 'assets/images/common/ic_contact_us_lightning.png', | 230 | + 'assets/images/common/ic_contact_us.png', |
| 195 | ), | 231 | ), |
| 196 | width: 24, | 232 | width: 24, |
| 197 | height: 24, | 233 | height: 24, |
| @@ -353,8 +353,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> { | @@ -353,8 +353,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> { | ||
| 353 | child: GestureDetector( | 353 | child: GestureDetector( |
| 354 | onTap: () => showEmailLoginContactUsBottomSheet( | 354 | onTap: () => showEmailLoginContactUsBottomSheet( |
| 355 | userId: controller.emailInput.value.trim(), | 355 | userId: controller.emailInput.value.trim(), |
| 356 | - onSendEmail: () => | ||
| 357 | - Get.toNamed(Routes.SUBMIT_FEEDBACK), | 356 | + pageName: 'Reset password page', |
| 358 | ), | 357 | ), |
| 359 | child: Text( | 358 | child: Text( |
| 360 | 'Having trouble? Contact us', | 359 | 'Having trouble? Contact us', |
| @@ -41,124 +41,172 @@ class LoginView extends GetView<LoginController> { | @@ -41,124 +41,172 @@ class LoginView extends GetView<LoginController> { | ||
| 41 | final rightOffset = (width - buttonWidth) / 2; | 41 | final rightOffset = (width - buttonWidth) / 2; |
| 42 | double bottom(double value) => bottomInset + value; | 42 | double bottom(double value) => bottomInset + value; |
| 43 | final l10n = context.l10n; | 43 | final l10n = context.l10n; |
| 44 | - return Stack( | ||
| 45 | - fit: StackFit.expand, | ||
| 46 | - children: [ | ||
| 47 | - Column( | ||
| 48 | - mainAxisSize: MainAxisSize.max, | ||
| 49 | - children: [ | ||
| 50 | - Image.asset( | ||
| 51 | - 'assets/images/user_onboarding/bg_login.png', | ||
| 52 | - fit: BoxFit.fitWidth, | ||
| 53 | - ), | ||
| 54 | - Expanded( | ||
| 55 | - child: Container( | ||
| 56 | - decoration: const BoxDecoration( | ||
| 57 | - gradient: LinearGradient( | ||
| 58 | - begin: Alignment(0.50, -0.00), | ||
| 59 | - end: Alignment(0.50, 1.00), | ||
| 60 | - colors: [ | ||
| 61 | - const Color(0xFFEEE6FF), | ||
| 62 | - const Color(0xFFF8F6FF) | ||
| 63 | - ]), | ||
| 64 | - ), | ||
| 65 | - )), | ||
| 66 | - ], | ||
| 67 | - ), | ||
| 68 | - // Welcome texts | ||
| 69 | - Positioned( | ||
| 70 | - bottom: bottom(controller.environmentConfig.region.value == | ||
| 71 | - AppRegion.china | ||
| 72 | - ? 253 | ||
| 73 | - : 273), | ||
| 74 | - left: 0, | ||
| 75 | - right: 0, | ||
| 76 | - child: Column( | ||
| 77 | - mainAxisSize: MainAxisSize.min, | ||
| 78 | - crossAxisAlignment: CrossAxisAlignment.center, | 44 | + return Obx(() { |
| 45 | + final region = controller.environmentConfig.region.value; | ||
| 46 | + return Stack( | ||
| 47 | + fit: StackFit.expand, | ||
| 48 | + children: [ | ||
| 49 | + Column( | ||
| 50 | + mainAxisSize: MainAxisSize.max, | ||
| 79 | children: [ | 51 | children: [ |
| 80 | Image.asset( | 52 | Image.asset( |
| 81 | - 'assets/images/common/ic_double_feel_text.png', | ||
| 82 | - width: 160, | ||
| 83 | - height: 42, | 53 | + 'assets/images/user_onboarding/bg_login.png', |
| 54 | + fit: BoxFit.fitWidth, | ||
| 84 | ), | 55 | ), |
| 85 | - SizedBox(height: 8), | ||
| 86 | - Padding( | ||
| 87 | - padding: EdgeInsets.symmetric( | ||
| 88 | - horizontal: | ||
| 89 | - controller.environmentConfig.region.value == | ||
| 90 | - AppRegion.china | ||
| 91 | - ? 0 | ||
| 92 | - : 40), | ||
| 93 | - child: Text( | ||
| 94 | - l10n.loginSlogan, | ||
| 95 | - textAlign: TextAlign.center, | ||
| 96 | - style: TextStyle( | ||
| 97 | - color: context.colors.textSecondary, | ||
| 98 | - fontSize: 12, | ||
| 99 | - fontWeight: FontWeight.w400, | ||
| 100 | - height: 1.50, | ||
| 101 | - letterSpacing: | ||
| 102 | - controller.environmentConfig.region.value == | ||
| 103 | - AppRegion.china | ||
| 104 | - ? 6 | ||
| 105 | - : 1, | ||
| 106 | - ), | 56 | + Expanded( |
| 57 | + child: Container( | ||
| 58 | + decoration: const BoxDecoration( | ||
| 59 | + gradient: LinearGradient( | ||
| 60 | + begin: Alignment(0.50, -0.00), | ||
| 61 | + end: Alignment(0.50, 1.00), | ||
| 62 | + colors: [ | ||
| 63 | + const Color(0xFFEEE6FF), | ||
| 64 | + const Color(0xFFF8F6FF) | ||
| 65 | + ]), | ||
| 107 | ), | 66 | ), |
| 108 | - ), | 67 | + )), |
| 109 | ], | 68 | ], |
| 110 | ), | 69 | ), |
| 111 | - ), | ||
| 112 | - | ||
| 113 | - // Login buttons – add new methods here as Column children | ||
| 114 | - Positioned( | ||
| 115 | - bottom: bottom(85), | ||
| 116 | - left: 0, | ||
| 117 | - right: 0, | ||
| 118 | - child: Center( | 70 | + // Welcome texts |
| 71 | + if (!controller.environmentConfig.hideChooseRegion) | ||
| 72 | + Positioned( | ||
| 73 | + right: 20, | ||
| 74 | + top: MediaQuery.paddingOf(context).top, | ||
| 75 | + child: GestureDetector( | ||
| 76 | + onTap: () => Get.bottomSheet( | ||
| 77 | + const _ServiceRegionBottomSheet(), | ||
| 78 | + backgroundColor: Colors.transparent, | ||
| 79 | + barrierColor: Colors.black.withValues(alpha: 0.5), | ||
| 80 | + enableDrag: true, | ||
| 81 | + isScrollControlled: true, | ||
| 82 | + ), | ||
| 83 | + child: Text( | ||
| 84 | + '🌏', | ||
| 85 | + textAlign: TextAlign.right, | ||
| 86 | + style: TextStyle( | ||
| 87 | + color: Colors.black, | ||
| 88 | + fontSize: 23, | ||
| 89 | + fontWeight: FontWeight.w400, | ||
| 90 | + ), | ||
| 91 | + ), | ||
| 92 | + )), | ||
| 93 | + Positioned( | ||
| 94 | + bottom: bottom(region == AppRegion.china ? 253 : 273), | ||
| 95 | + left: 0, | ||
| 96 | + right: 0, | ||
| 119 | child: Column( | 97 | child: Column( |
| 120 | mainAxisSize: MainAxisSize.min, | 98 | mainAxisSize: MainAxisSize.min, |
| 99 | + crossAxisAlignment: CrossAxisAlignment.center, | ||
| 121 | children: [ | 100 | children: [ |
| 122 | - if (controller.environmentConfig.region.value == | ||
| 123 | - AppRegion.china) ...[ | ||
| 124 | - _LoginMethodButton( | ||
| 125 | - width: buttonWidth, | ||
| 126 | - label: l10n.loginWithPhone, | ||
| 127 | - onPressed: controller.onPhoneLoginPressed, | ||
| 128 | - isLastUsed: | ||
| 129 | - Get.find<LocalStorage>().lastLoginMethod == | ||
| 130 | - 'phone', | ||
| 131 | - lastUsedLabel: l10n.loginLastUsed, | ||
| 132 | - ), | ||
| 133 | - const SizedBox(height: 12) | ||
| 134 | - ], | ||
| 135 | - if (controller.environmentConfig.region.value == | ||
| 136 | - AppRegion.global) ...[ | ||
| 137 | - _LoginMethodButton( | ||
| 138 | - width: buttonWidth, | ||
| 139 | - label: l10n.continueWithEmail, | ||
| 140 | - onPressed: controller.onEmailLoginPressed, | ||
| 141 | - isLastUsed: | ||
| 142 | - Get.find<LocalStorage>().lastLoginMethod == | ||
| 143 | - 'email', | ||
| 144 | - lastUsedLabel: l10n.loginLastUsed, | 101 | + Image.asset( |
| 102 | + 'assets/images/common/ic_double_feel_text.png', | ||
| 103 | + width: 160, | ||
| 104 | + height: 42, | ||
| 105 | + ), | ||
| 106 | + SizedBox(height: 8), | ||
| 107 | + Padding( | ||
| 108 | + padding: EdgeInsets.symmetric( | ||
| 109 | + horizontal: region == AppRegion.china ? 0 : 40), | ||
| 110 | + child: Text( | ||
| 111 | + l10n.loginSlogan, | ||
| 112 | + textAlign: TextAlign.center, | ||
| 113 | + style: TextStyle( | ||
| 114 | + color: context.colors.textSecondary, | ||
| 115 | + fontSize: 12, | ||
| 116 | + fontWeight: FontWeight.w400, | ||
| 117 | + height: 1.50, | ||
| 118 | + letterSpacing: region == AppRegion.china ? 6 : 1, | ||
| 119 | + ), | ||
| 145 | ), | 120 | ), |
| 146 | - const SizedBox(height: 12) | ||
| 147 | - ], | ||
| 148 | - if (controller.environmentConfig.region.value == | ||
| 149 | - AppRegion.global) ...[ | 121 | + ), |
| 122 | + ], | ||
| 123 | + ), | ||
| 124 | + ), | ||
| 125 | + | ||
| 126 | + // Login buttons – add new methods here as Column children | ||
| 127 | + Positioned( | ||
| 128 | + bottom: bottom(85), | ||
| 129 | + left: 0, | ||
| 130 | + right: 0, | ||
| 131 | + child: Center( | ||
| 132 | + child: Column( | ||
| 133 | + mainAxisSize: MainAxisSize.min, | ||
| 134 | + children: [ | ||
| 135 | + if (region == AppRegion.china) ...[ | ||
| 136 | + //GetPlatform.isOhos | ||
| 137 | + _LoginMethodButton( | ||
| 138 | + width: buttonWidth, | ||
| 139 | + label: l10n.loginWithPhone, | ||
| 140 | + onPressed: controller.onPhoneLoginPressed, | ||
| 141 | + isLastUsed: | ||
| 142 | + Get.find<LocalStorage>().lastLoginMethod == | ||
| 143 | + 'phone', | ||
| 144 | + lastUsedLabel: l10n.loginLastUsed, | ||
| 145 | + ), | ||
| 146 | + const SizedBox(height: 12) | ||
| 147 | + ], | ||
| 148 | + if (region == AppRegion.global) ...[ | ||
| 149 | + _LoginMethodButton( | ||
| 150 | + width: buttonWidth, | ||
| 151 | + label: l10n.continueWithEmail, | ||
| 152 | + onPressed: controller.onEmailLoginPressed, | ||
| 153 | + isLastUsed: | ||
| 154 | + Get.find<LocalStorage>().lastLoginMethod == | ||
| 155 | + 'email', | ||
| 156 | + lastUsedLabel: l10n.loginLastUsed, | ||
| 157 | + ), | ||
| 158 | + const SizedBox(height: 12) | ||
| 159 | + ], | ||
| 160 | + if (region == AppRegion.global) ...[ | ||
| 161 | + _LoginMethodButton( | ||
| 162 | + width: buttonWidth, | ||
| 163 | + label: l10n.continueWithGoogle, | ||
| 164 | + onPressed: controller.onGoogleLoginPressed, | ||
| 165 | + isLastUsed: | ||
| 166 | + Get.find<LocalStorage>().lastLoginMethod == | ||
| 167 | + 'google', | ||
| 168 | + lastUsedLabel: l10n.loginLastUsed, | ||
| 169 | + icon: Image.asset( | ||
| 170 | + 'assets/images/common/ic_google.png', | ||
| 171 | + width: 20, | ||
| 172 | + height: 20, | ||
| 173 | + ), | ||
| 174 | + buttonStyle: ElevatedButton.styleFrom( | ||
| 175 | + backgroundColor: Colors.white, | ||
| 176 | + foregroundColor: context.colors.textPrimary, | ||
| 177 | + disabledBackgroundColor: context | ||
| 178 | + .colors.textPrimary | ||
| 179 | + .withValues(alpha: 0.5), | ||
| 180 | + disabledForegroundColor: | ||
| 181 | + context.colors.textPrimary, | ||
| 182 | + elevation: 0, | ||
| 183 | + shadowColor: Colors.transparent, | ||
| 184 | + padding: | ||
| 185 | + const EdgeInsets.symmetric(horizontal: 12), | ||
| 186 | + shape: RoundedRectangleBorder( | ||
| 187 | + borderRadius: BorderRadius.circular(24), | ||
| 188 | + ), | ||
| 189 | + textStyle: const TextStyle( | ||
| 190 | + fontSize: 16, | ||
| 191 | + fontWeight: FontWeight.w700, | ||
| 192 | + ), | ||
| 193 | + ), | ||
| 194 | + ), | ||
| 195 | + const SizedBox(height: 12) | ||
| 196 | + ], | ||
| 150 | _LoginMethodButton( | 197 | _LoginMethodButton( |
| 151 | width: buttonWidth, | 198 | width: buttonWidth, |
| 152 | - label: l10n.continueWithGoogle, | ||
| 153 | - onPressed: controller.onGoogleLoginPressed, | 199 | + label: l10n.loginWithApple, |
| 200 | + onPressed: controller.onAppleLoginPressed, | ||
| 154 | isLastUsed: | 201 | isLastUsed: |
| 155 | Get.find<LocalStorage>().lastLoginMethod == | 202 | Get.find<LocalStorage>().lastLoginMethod == |
| 156 | - 'google', | 203 | + 'apple', |
| 157 | lastUsedLabel: l10n.loginLastUsed, | 204 | lastUsedLabel: l10n.loginLastUsed, |
| 158 | icon: Image.asset( | 205 | icon: Image.asset( |
| 159 | - 'assets/images/common/ic_google.png', | ||
| 160 | - width: 20, | ||
| 161 | - height: 20, | 206 | + 'assets/images/common/ic_apple.png', |
| 207 | + width: 24, | ||
| 208 | + height: 24, | ||
| 209 | + color: context.colors.textPrimary, | ||
| 162 | ), | 210 | ), |
| 163 | buttonStyle: ElevatedButton.styleFrom( | 211 | buttonStyle: ElevatedButton.styleFrom( |
| 164 | backgroundColor: Colors.white, | 212 | backgroundColor: Colors.white, |
| @@ -181,79 +229,292 @@ class LoginView extends GetView<LoginController> { | @@ -181,79 +229,292 @@ class LoginView extends GetView<LoginController> { | ||
| 181 | ), | 229 | ), |
| 182 | ), | 230 | ), |
| 183 | ), | 231 | ), |
| 184 | - const SizedBox(height: 12) | ||
| 185 | ], | 232 | ], |
| 186 | - _LoginMethodButton( | ||
| 187 | - width: buttonWidth, | ||
| 188 | - label: l10n.loginWithApple, | ||
| 189 | - onPressed: controller.onAppleLoginPressed, | ||
| 190 | - isLastUsed: | ||
| 191 | - Get.find<LocalStorage>().lastLoginMethod == | ||
| 192 | - 'apple', | ||
| 193 | - lastUsedLabel: l10n.loginLastUsed, | ||
| 194 | - icon: Image.asset( | ||
| 195 | - 'assets/images/common/ic_apple.png', | ||
| 196 | - width: 24, | ||
| 197 | - height: 24, | ||
| 198 | - color: context.colors.textPrimary, | ||
| 199 | - ), | ||
| 200 | - buttonStyle: ElevatedButton.styleFrom( | ||
| 201 | - backgroundColor: Colors.white, | ||
| 202 | - foregroundColor: context.colors.textPrimary, | ||
| 203 | - disabledBackgroundColor: context.colors.textPrimary | ||
| 204 | - .withValues(alpha: 0.5), | ||
| 205 | - disabledForegroundColor: context.colors.textPrimary, | ||
| 206 | - elevation: 0, | ||
| 207 | - shadowColor: Colors.transparent, | ||
| 208 | - padding: const EdgeInsets.symmetric(horizontal: 12), | ||
| 209 | - shape: RoundedRectangleBorder( | ||
| 210 | - borderRadius: BorderRadius.circular(24), | ||
| 211 | - ), | 233 | + ), |
| 234 | + ), | ||
| 235 | + ), | ||
| 236 | + // Terms agreement text + checkbox | ||
| 237 | + Positioned( | ||
| 238 | + bottom: bottom(0), | ||
| 239 | + left: 0, | ||
| 240 | + right: 0, | ||
| 241 | + child: const _AgreementText(), | ||
| 242 | + ), | ||
| 243 | + | ||
| 244 | + if (controller.environmentConfig.isDebug) | ||
| 245 | + Positioned( | ||
| 246 | + left: 0, | ||
| 247 | + right: 0, | ||
| 248 | + bottom: bottom(0), | ||
| 249 | + child: Center( | ||
| 250 | + child: TextButton( | ||
| 251 | + onPressed: controller.onDebugPressed, | ||
| 252 | + style: TextButton.styleFrom( | ||
| 253 | + foregroundColor: context.colors.textSecondary | ||
| 254 | + .withValues(alpha: 0.55), | ||
| 212 | textStyle: const TextStyle( | 255 | textStyle: const TextStyle( |
| 213 | - fontSize: 16, | ||
| 214 | - fontWeight: FontWeight.w700, | 256 | + fontSize: 12, |
| 257 | + fontWeight: FontWeight.w600, | ||
| 215 | ), | 258 | ), |
| 216 | ), | 259 | ), |
| 260 | + child: Builder(builder: (context) { | ||
| 261 | + final environmentConfig = | ||
| 262 | + Get.find<AppEnvironmentConfig>(); | ||
| 263 | + return Text( | ||
| 264 | + environmentConfig.environment.value.name); | ||
| 265 | + }), | ||
| 217 | ), | 266 | ), |
| 218 | - ], | 267 | + ), |
| 268 | + ), | ||
| 269 | + ], | ||
| 270 | + ); | ||
| 271 | + }); | ||
| 272 | + }, | ||
| 273 | + ), | ||
| 274 | + ), | ||
| 275 | + ); | ||
| 276 | + } | ||
| 277 | +} | ||
| 278 | + | ||
| 279 | +class _ServiceRegionBottomSheet extends StatefulWidget { | ||
| 280 | + const _ServiceRegionBottomSheet(); | ||
| 281 | + | ||
| 282 | + @override | ||
| 283 | + State<_ServiceRegionBottomSheet> createState() => | ||
| 284 | + _ServiceRegionBottomSheetState(); | ||
| 285 | +} | ||
| 286 | + | ||
| 287 | +class _ServiceRegionBottomSheetState extends State<_ServiceRegionBottomSheet> { | ||
| 288 | + static const _brandColor = Color(0xFF845EEE); | ||
| 289 | + static const _backgroundColor = Color(0xFFF5F2FF); | ||
| 290 | + | ||
| 291 | + final _controller = Get.find<LoginController>(); | ||
| 292 | + late AppRegion _selectedRegion; | ||
| 293 | + var _isSaving = false; | ||
| 294 | + | ||
| 295 | + @override | ||
| 296 | + void initState() { | ||
| 297 | + super.initState(); | ||
| 298 | + _selectedRegion = _controller.environmentConfig.region.value; | ||
| 299 | + } | ||
| 300 | + | ||
| 301 | + Future<void> _save() async { | ||
| 302 | + if (_isSaving) return; | ||
| 303 | + | ||
| 304 | + setState(() => _isSaving = true); | ||
| 305 | + await _controller.selectServiceRegion(_selectedRegion); | ||
| 306 | + if (mounted) Get.back(); | ||
| 307 | + } | ||
| 308 | + | ||
| 309 | + @override | ||
| 310 | + Widget build(BuildContext context) { | ||
| 311 | + final l10n = context.l10n; | ||
| 312 | + | ||
| 313 | + final isChinaSelected = _selectedRegion == AppRegion.china; | ||
| 314 | + return SizedBox( | ||
| 315 | + height: 446, | ||
| 316 | + child: DecoratedBox( | ||
| 317 | + decoration: const BoxDecoration( | ||
| 318 | + color: _backgroundColor, | ||
| 319 | + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), | ||
| 320 | + ), | ||
| 321 | + child: Padding( | ||
| 322 | + padding: EdgeInsets.only( | ||
| 323 | + top: 8, | ||
| 324 | + bottom: MediaQuery.paddingOf(context).bottom, | ||
| 325 | + ), | ||
| 326 | + child: Column( | ||
| 327 | + children: [ | ||
| 328 | + Align( | ||
| 329 | + alignment: Alignment.centerLeft, | ||
| 330 | + child: GestureDetector( | ||
| 331 | + behavior: HitTestBehavior.opaque, | ||
| 332 | + onTap: _isSaving ? null : Get.back, | ||
| 333 | + child: SizedBox( | ||
| 334 | + width: 84, | ||
| 335 | + height: 56, | ||
| 336 | + child: Center( | ||
| 337 | + child: Image.asset( | ||
| 338 | + 'assets/images/common/ic_close.png', | ||
| 339 | + width: 16, | ||
| 340 | + height: 16, | ||
| 341 | + color: _brandColor, | ||
| 342 | + ), | ||
| 219 | ), | 343 | ), |
| 220 | ), | 344 | ), |
| 221 | ), | 345 | ), |
| 222 | - // Terms agreement text + checkbox | ||
| 223 | - Positioned( | ||
| 224 | - bottom: bottom(0), | ||
| 225 | - left: 0, | ||
| 226 | - right: 0, | ||
| 227 | - child: const _AgreementText(), | 346 | + ), |
| 347 | + const SizedBox(height: 6), | ||
| 348 | + Padding( | ||
| 349 | + padding: const EdgeInsets.symmetric(horizontal: 16), | ||
| 350 | + child: Column( | ||
| 351 | + children: [ | ||
| 352 | + _ServiceRegionOption( | ||
| 353 | + title: l10n.mainlandChinaServices, | ||
| 354 | + description: l10n.mainlandChinaServicesDescription, | ||
| 355 | + cannotChangeLabel: l10n.serviceRegionCannotBeChanged, | ||
| 356 | + isSelected: isChinaSelected, | ||
| 357 | + minHeight: isChinaSelected ? 142 : 56, | ||
| 358 | + onTap: _isSaving | ||
| 359 | + ? null | ||
| 360 | + : () => setState( | ||
| 361 | + () => _selectedRegion = AppRegion.china, | ||
| 362 | + ), | ||
| 363 | + ), | ||
| 364 | + const SizedBox(height: 12), | ||
| 365 | + _ServiceRegionOption( | ||
| 366 | + title: l10n.internationalServices, | ||
| 367 | + description: l10n.internationalServicesDescription, | ||
| 368 | + cannotChangeLabel: l10n.serviceRegionCannotBeChanged, | ||
| 369 | + isSelected: !isChinaSelected, | ||
| 370 | + minHeight: isChinaSelected ? 56 : 182, | ||
| 371 | + onTap: _isSaving | ||
| 372 | + ? null | ||
| 373 | + : () => setState( | ||
| 374 | + () => _selectedRegion = AppRegion.global, | ||
| 375 | + ), | ||
| 376 | + ), | ||
| 377 | + ], | ||
| 228 | ), | 378 | ), |
| 379 | + ), | ||
| 380 | + const Spacer(), | ||
| 381 | + Padding( | ||
| 382 | + padding: const EdgeInsets.symmetric(horizontal: 28), | ||
| 383 | + child: SizedBox( | ||
| 384 | + width: double.infinity, | ||
| 385 | + height: 48, | ||
| 386 | + child: ElevatedButton( | ||
| 387 | + onPressed: _isSaving ? null : _save, | ||
| 388 | + style: ElevatedButton.styleFrom( | ||
| 389 | + backgroundColor: _brandColor, | ||
| 390 | + disabledBackgroundColor: _brandColor.withValues( | ||
| 391 | + alpha: 0.4, | ||
| 392 | + ), | ||
| 393 | + foregroundColor: Colors.white, | ||
| 394 | + disabledForegroundColor: Colors.white, | ||
| 395 | + elevation: 0, | ||
| 396 | + shape: const StadiumBorder(), | ||
| 397 | + textStyle: const TextStyle( | ||
| 398 | + fontSize: 16, | ||
| 399 | + fontWeight: FontWeight.w600, | ||
| 400 | + ), | ||
| 401 | + ), | ||
| 402 | + child: Text(l10n.friendsSave), | ||
| 403 | + ), | ||
| 404 | + ), | ||
| 405 | + ), | ||
| 406 | + ], | ||
| 407 | + ), | ||
| 408 | + ), | ||
| 409 | + ), | ||
| 410 | + ); | ||
| 411 | + } | ||
| 412 | +} | ||
| 229 | 413 | ||
| 230 | - if (controller.environmentConfig.isDebug) | ||
| 231 | - Positioned( | ||
| 232 | - left: 0, | ||
| 233 | - right: 0, | ||
| 234 | - bottom: bottom(0), | ||
| 235 | - child: Center( | ||
| 236 | - child: TextButton( | ||
| 237 | - onPressed: controller.onDebugPressed, | ||
| 238 | - style: TextButton.styleFrom( | ||
| 239 | - foregroundColor: context.colors.textSecondary | ||
| 240 | - .withValues(alpha: 0.55), | ||
| 241 | - textStyle: const TextStyle( | ||
| 242 | - fontSize: 12, | ||
| 243 | - fontWeight: FontWeight.w600, | 414 | +class _ServiceRegionOption extends StatelessWidget { |
| 415 | + const _ServiceRegionOption({ | ||
| 416 | + required this.title, | ||
| 417 | + required this.description, | ||
| 418 | + required this.cannotChangeLabel, | ||
| 419 | + required this.isSelected, | ||
| 420 | + required this.minHeight, | ||
| 421 | + required this.onTap, | ||
| 422 | + }); | ||
| 423 | + | ||
| 424 | + final String title; | ||
| 425 | + final String description; | ||
| 426 | + final String cannotChangeLabel; | ||
| 427 | + final bool isSelected; | ||
| 428 | + final double minHeight; | ||
| 429 | + final VoidCallback? onTap; | ||
| 430 | + | ||
| 431 | + @override | ||
| 432 | + Widget build(BuildContext context) { | ||
| 433 | + return Semantics( | ||
| 434 | + button: true, | ||
| 435 | + selected: isSelected, | ||
| 436 | + label: title, | ||
| 437 | + child: GestureDetector( | ||
| 438 | + behavior: HitTestBehavior.opaque, | ||
| 439 | + onTap: onTap, | ||
| 440 | + child: Container( | ||
| 441 | + constraints: BoxConstraints(minHeight: minHeight), | ||
| 442 | + width: double.infinity, | ||
| 443 | + decoration: BoxDecoration( | ||
| 444 | + color: Colors.white, | ||
| 445 | + borderRadius: BorderRadius.circular(16), | ||
| 446 | + border: isSelected | ||
| 447 | + ? Border.all( | ||
| 448 | + color: Color(0xFF845EEE), | ||
| 449 | + ) | ||
| 450 | + : null, | ||
| 451 | + ), | ||
| 452 | + child: isSelected | ||
| 453 | + ? Stack( | ||
| 454 | + children: [ | ||
| 455 | + Padding( | ||
| 456 | + padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), | ||
| 457 | + child: Column( | ||
| 458 | + crossAxisAlignment: CrossAxisAlignment.start, | ||
| 459 | + children: [ | ||
| 460 | + Text( | ||
| 461 | + title, | ||
| 462 | + style: const TextStyle( | ||
| 463 | + color: Color(0xFF0F0F11), | ||
| 464 | + fontSize: 16, | ||
| 465 | + fontWeight: FontWeight.w500, | ||
| 466 | + height: 1.25, | ||
| 467 | + ), | ||
| 244 | ), | 468 | ), |
| 245 | - ), | ||
| 246 | - child: Builder(builder: (context) { | ||
| 247 | - final environmentConfig = | ||
| 248 | - Get.find<AppEnvironmentConfig>(); | ||
| 249 | - return Text(environmentConfig.environment.value.name); | ||
| 250 | - }), | 469 | + const SizedBox(height: 14), |
| 470 | + Text( | ||
| 471 | + description, | ||
| 472 | + style: const TextStyle( | ||
| 473 | + color: Color(0xFF78787D), | ||
| 474 | + fontSize: 14, | ||
| 475 | + fontWeight: FontWeight.w400, | ||
| 476 | + height: 1.43, | ||
| 477 | + ), | ||
| 478 | + ), | ||
| 479 | + const SizedBox(height: 12), | ||
| 480 | + Text( | ||
| 481 | + cannotChangeLabel, | ||
| 482 | + style: const TextStyle( | ||
| 483 | + color: Color(0xFF78787D), | ||
| 484 | + fontSize: 14, | ||
| 485 | + fontWeight: FontWeight.w400, | ||
| 486 | + height: 1.43, | ||
| 487 | + ), | ||
| 488 | + ), | ||
| 489 | + ], | ||
| 490 | + ), | ||
| 491 | + ), | ||
| 492 | + const Positioned( | ||
| 493 | + top: 20, | ||
| 494 | + right: 20, | ||
| 495 | + child: Icon( | ||
| 496 | + Icons.check_circle_rounded, | ||
| 497 | + color: Color(0xFF845EEE), | ||
| 498 | + size: 20, | ||
| 499 | + ), | ||
| 500 | + ), | ||
| 501 | + ], | ||
| 502 | + ) | ||
| 503 | + : Padding( | ||
| 504 | + padding: const EdgeInsets.symmetric(horizontal: 24), | ||
| 505 | + child: Align( | ||
| 506 | + alignment: Alignment.centerLeft, | ||
| 507 | + child: Text( | ||
| 508 | + title, | ||
| 509 | + style: const TextStyle( | ||
| 510 | + color: Color(0xFF0F0F11), | ||
| 511 | + fontSize: 16, | ||
| 512 | + fontWeight: FontWeight.w500, | ||
| 513 | + height: 1.25, | ||
| 251 | ), | 514 | ), |
| 252 | ), | 515 | ), |
| 253 | ), | 516 | ), |
| 254 | - ], | ||
| 255 | - ); | ||
| 256 | - }, | 517 | + ), |
| 257 | ), | 518 | ), |
| 258 | ), | 519 | ), |
| 259 | ); | 520 | ); |
| @@ -71,6 +71,12 @@ class AppEnvironmentConfig { | @@ -71,6 +71,12 @@ class AppEnvironmentConfig { | ||
| 71 | Get.find<DioClient>().refreshBaseUrl(); | 71 | Get.find<DioClient>().refreshBaseUrl(); |
| 72 | } | 72 | } |
| 73 | 73 | ||
| 74 | + get hideChooseRegion => _storage.hideChooseRegion; | ||
| 75 | + | ||
| 76 | + Future<void> updateHideChooseRegion(bool value) async { | ||
| 77 | + await _storage.writeHideChooseRegion(value); | ||
| 78 | + } | ||
| 79 | + | ||
| 74 | String get serverBaseUrl { | 80 | String get serverBaseUrl { |
| 75 | final regionUrl = switch (region.value) { | 81 | final regionUrl = switch (region.value) { |
| 76 | AppRegion.china => AppConst.serverBaseUrl, | 82 | AppRegion.china => AppConst.serverBaseUrl, |
| @@ -4,6 +4,7 @@ abstract final class StorageConst { | @@ -4,6 +4,7 @@ abstract final class StorageConst { | ||
| 4 | 'double_feel_environment_config'; | 4 | 'double_feel_environment_config'; |
| 5 | static const String appEnvironmentKey = 'environment'; | 5 | static const String appEnvironmentKey = 'environment'; |
| 6 | static const String appRegionKey = 'region'; | 6 | static const String appRegionKey = 'region'; |
| 7 | + static const String hideChooseRegionKey = 'hide_choose_region'; | ||
| 7 | static const String appSettingsPrefsName = 'app_settings'; | 8 | static const String appSettingsPrefsName = 'app_settings'; |
| 8 | static const String termsAgreedKey = 'terms_agreed'; | 9 | static const String termsAgreedKey = 'terms_agreed'; |
| 9 | static const String lastLoginMethodKey = 'last_login_method'; | 10 | static const String lastLoginMethodKey = 'last_login_method'; |
| 1 | +import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart'; | ||
| 1 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 2 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| 3 | +import 'package:doublefeel_flutter/app/utils/dialog_utils.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | ||
| 2 | import 'package:get/get.dart'; | 5 | import 'package:get/get.dart'; |
| 3 | 6 | ||
| 4 | import '../constants/network_const.dart'; | 7 | import '../constants/network_const.dart'; |
| @@ -52,6 +55,13 @@ class AppErrorHandler { | @@ -52,6 +55,13 @@ class AppErrorHandler { | ||
| 52 | Get.find<UserStateService>().onLogout(); | 55 | Get.find<UserStateService>().onLogout(); |
| 53 | } | 56 | } |
| 54 | Get.offAllNamed(AppRoutes.login); | 57 | Get.offAllNamed(AppRoutes.login); |
| 58 | + DialogUtils.showCommonDialog(DialogMetaData( | ||
| 59 | + title: l10n.logOut, | ||
| 60 | + message: l10n | ||
| 61 | + .yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue, | ||
| 62 | + confirmText: l10n.watchThemeOk, | ||
| 63 | + iconAsset: 'assets/images/common/ic_warning.png', | ||
| 64 | + )); | ||
| 55 | } | 65 | } |
| 56 | 66 | ||
| 57 | void _showMessage(String? message) { | 67 | void _showMessage(String? message) { |
| @@ -44,6 +44,13 @@ class LocalStorage { | @@ -44,6 +44,13 @@ class LocalStorage { | ||
| 44 | await sharedPreferences.setInt(StorageConst.appRegionKey, region.index); | 44 | await sharedPreferences.setInt(StorageConst.appRegionKey, region.index); |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | + bool get hideChooseRegion => | ||
| 48 | + sharedPreferences.getBool(StorageConst.hideChooseRegionKey) ?? false; | ||
| 49 | + | ||
| 50 | + Future<void> writeHideChooseRegion(bool value) async { | ||
| 51 | + await sharedPreferences.setBool(StorageConst.hideChooseRegionKey, value); | ||
| 52 | + } | ||
| 53 | + | ||
| 47 | // ─── App Settings Storage ────────────────────────────────────────────────── | 54 | // ─── App Settings Storage ────────────────────────────────────────────────── |
| 48 | 55 | ||
| 49 | bool get termsAgreed => | 56 | bool get termsAgreed => |
| @@ -206,7 +206,9 @@ class UserInfoResponse { | @@ -206,7 +206,9 @@ class UserInfoResponse { | ||
| 206 | this.uniqueCode, | 206 | this.uniqueCode, |
| 207 | this.enableAddWithUCode, | 207 | this.enableAddWithUCode, |
| 208 | this.isPassNoviceGuide, | 208 | this.isPassNoviceGuide, |
| 209 | - this.thirdAccountId}); | 209 | + this.thirdAccountId, |
| 210 | + this.thirdAccountType, | ||
| 211 | + this.securityEmail}); | ||
| 210 | 212 | ||
| 211 | final int? id; | 213 | final int? id; |
| 212 | final String? pairCode; | 214 | final String? pairCode; |
| @@ -223,6 +225,8 @@ class UserInfoResponse { | @@ -223,6 +225,8 @@ class UserInfoResponse { | ||
| 223 | final int? enableAddWithUCode; | 225 | final int? enableAddWithUCode; |
| 224 | final int? isPassNoviceGuide; | 226 | final int? isPassNoviceGuide; |
| 225 | final String? thirdAccountId; | 227 | final String? thirdAccountId; |
| 228 | + final String? thirdAccountType; | ||
| 229 | + final String? securityEmail; | ||
| 226 | 230 | ||
| 227 | factory UserInfoResponse.fromJson(Map<String, dynamic> json) { | 231 | factory UserInfoResponse.fromJson(Map<String, dynamic> json) { |
| 228 | return UserInfoResponse( | 232 | return UserInfoResponse( |
| @@ -241,6 +245,14 @@ class UserInfoResponse { | @@ -241,6 +245,14 @@ class UserInfoResponse { | ||
| 241 | enableAddWithUCode: json['enable_add_with_ucode'] as int?, | 245 | enableAddWithUCode: json['enable_add_with_ucode'] as int?, |
| 242 | isPassNoviceGuide: json['is_pass_novice_guide'] as int?, | 246 | isPassNoviceGuide: json['is_pass_novice_guide'] as int?, |
| 243 | thirdAccountId: json['third_account_id'] as String?, | 247 | thirdAccountId: json['third_account_id'] as String?, |
| 248 | + thirdAccountType: _asString( | ||
| 249 | + json['third_account_type'] ?? | ||
| 250 | + json['third_account_provider'] ?? | ||
| 251 | + json['login_type'], | ||
| 252 | + ), | ||
| 253 | + securityEmail: _asString( | ||
| 254 | + json['security_email'] ?? json['safe_email'] ?? json['email'], | ||
| 255 | + ), | ||
| 244 | ); | 256 | ); |
| 245 | } | 257 | } |
| 246 | 258 | ||
| @@ -265,10 +277,19 @@ class UserInfoResponse { | @@ -265,10 +277,19 @@ class UserInfoResponse { | ||
| 265 | val['is_pass_novice_guide'] = isPassNoviceGuide; | 277 | val['is_pass_novice_guide'] = isPassNoviceGuide; |
| 266 | } | 278 | } |
| 267 | if (thirdAccountId != null) val['third_account_id'] = thirdAccountId; | 279 | if (thirdAccountId != null) val['third_account_id'] = thirdAccountId; |
| 280 | + if (thirdAccountType != null) { | ||
| 281 | + val['third_account_type'] = thirdAccountType; | ||
| 282 | + } | ||
| 283 | + if (securityEmail != null) val['security_email'] = securityEmail; | ||
| 268 | return val; | 284 | return val; |
| 269 | } | 285 | } |
| 270 | } | 286 | } |
| 271 | 287 | ||
| 288 | +String? _asString(Object? value) { | ||
| 289 | + final normalized = value?.toString().trim(); | ||
| 290 | + return normalized == null || normalized.isEmpty ? null : normalized; | ||
| 291 | +} | ||
| 292 | + | ||
| 272 | class BoundUserInfoResponse { | 293 | class BoundUserInfoResponse { |
| 273 | const BoundUserInfoResponse({this.userInfo, this.partnerUserInfo}); | 294 | const BoundUserInfoResponse({this.userInfo, this.partnerUserInfo}); |
| 274 | 295 |
| @@ -19,6 +19,11 @@ | @@ -19,6 +19,11 @@ | ||
| 19 | "tabFriends": "Friends", | 19 | "tabFriends": "Friends", |
| 20 | "tabMy": "Me", | 20 | "tabMy": "Me", |
| 21 | "switchLanguage": "Switch Language", | 21 | "switchLanguage": "Switch Language", |
| 22 | + "mainlandChinaServices": "Mainland China Services", | ||
| 23 | + "internationalServices": "International Services", | ||
| 24 | + "mainlandChinaServicesDescription": "For users mainly in Mainland China. Health, account, and friend data is stored in Mainland China.", | ||
| 25 | + "internationalServicesDescription": "For users mainly outside Mainland China. Health, account, and friend data is stored internationally.", | ||
| 26 | + "serviceRegionCannotBeChanged": "Service region cannot be changed after account creation.", | ||
| 22 | "settings": "Settings", | 27 | "settings": "Settings", |
| 23 | "onboardingIntroTitle": "DoubleFeel is a health companion app built for Apple Watch", | 28 | "onboardingIntroTitle": "DoubleFeel is a health companion app built for Apple Watch", |
| 24 | "onboardingIntroBody": "<em>Understand yourself better</em>, and let people who care about you <em>notice when you need support.</em>", | 29 | "onboardingIntroBody": "<em>Understand yourself better</em>, and let people who care about you <em>notice when you need support.</em>", |
| @@ -228,9 +233,9 @@ | @@ -228,9 +233,9 @@ | ||
| 228 | "redeemOffer": "Claim Offer", | 233 | "redeemOffer": "Claim Offer", |
| 229 | "allPlans": "All Plans", | 234 | "allPlans": "All Plans", |
| 230 | "clickToAddTheHrvThemedWatchFace": "Add HRV Watch Face", | 235 | "clickToAddTheHrvThemedWatchFace": "Add HRV Watch Face", |
| 231 | - "stayOnTopOfYourHealthFluctuations": "Stay on top of your health", | ||
| 232 | - "addACloseContact": "Add a Close Contact", | ||
| 233 | - "oneMorePersonLookingOutForYourHealth": "Have someone care about your health", | 236 | + "stayOnTopOfYourHealthFluctuations": "Track your body changes", |
| 237 | + "addACloseContact": "Add a Loved One", | ||
| 238 | + "oneMorePersonLookingOutForYourHealth": "Follow your health", | ||
| 234 | "addAFriend": "Add", | 239 | "addAFriend": "Add", |
| 235 | "averageHrvForTheDay": "Avg. Hrv", | 240 | "averageHrvForTheDay": "Avg. Hrv", |
| 236 | "restingHeartRate": "RHR", | 241 | "restingHeartRate": "RHR", |
| @@ -255,6 +260,32 @@ | @@ -255,6 +260,32 @@ | ||
| 255 | "frequentlyAskedQuestions": "Frequently Asked Questions", | 260 | "frequentlyAskedQuestions": "Frequently Asked Questions", |
| 256 | "areYouSureYouWantToDeleteYourAccount": "Are you sure you want to delete your account?", | 261 | "areYouSureYouWantToDeleteYourAccount": "Are you sure you want to delete your account?", |
| 257 | "accountSettings": "Account Settings", | 262 | "accountSettings": "Account Settings", |
| 263 | + "appleAccount": "Apple Account", | ||
| 264 | + "googleAccount": "Google Account", | ||
| 265 | + "securityEmail": "Email", | ||
| 266 | + "addSecurityEmail": "Add security email", | ||
| 267 | + "securityEmailDescription": "Use a security email to help protect your account.", | ||
| 268 | + "securityEmailHint": "Email address", | ||
| 269 | + "sendVerificationEmail": "Send verification email", | ||
| 270 | + "confirmYourEmail": "Confirm your email", | ||
| 271 | + "enterCodeSentTo": "Enter the code sent to {email}", | ||
| 272 | + "@enterCodeSentTo": { | ||
| 273 | + "placeholders": { | ||
| 274 | + "email": {} | ||
| 275 | + } | ||
| 276 | + }, | ||
| 277 | + "emailVerificationHelp": "If you don't see the email, check other places it might be, like your junk, spam, social, or other folders", | ||
| 278 | + "verificationCode": "Verification code", | ||
| 279 | + "resend": "Re-send", | ||
| 280 | + "resendWithSeconds": "Re-send({seconds})", | ||
| 281 | + "@resendWithSeconds": { | ||
| 282 | + "placeholders": { | ||
| 283 | + "seconds": { | ||
| 284 | + "type": "int" | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + }, | ||
| 288 | + "invalidEmailFormat": "Invalid email format", | ||
| 258 | "deleteAccount": "Delete Account", | 289 | "deleteAccount": "Delete Account", |
| 259 | "mobilePhoneNumber": "Mobile phone number", | 290 | "mobilePhoneNumber": "Mobile phone number", |
| 260 | "logOut": "Log Out", | 291 | "logOut": "Log Out", |
| @@ -312,7 +343,7 @@ | @@ -312,7 +343,7 @@ | ||
| 312 | "reportWeekdaySaturday": "Sat", | 343 | "reportWeekdaySaturday": "Sat", |
| 313 | "reportWeekdaySunday": "Sun", | 344 | "reportWeekdaySunday": "Sun", |
| 314 | "reportDateWithWeekday": "{date}, {weekday}", | 345 | "reportDateWithWeekday": "{date}, {weekday}", |
| 315 | - "reportOtherPossessiveTitle": "{title}", | 346 | + "reportOtherPossessiveTitle": "Their {title}", |
| 316 | "reportOtherTitle": "{title}", | 347 | "reportOtherTitle": "{title}", |
| 317 | "reportTrend": "Trends", | 348 | "reportTrend": "Trends", |
| 318 | "reportExampleTitle": "{title} (Example)", | 349 | "reportExampleTitle": "{title} (Example)", |
| @@ -498,7 +529,7 @@ | @@ -498,7 +529,7 @@ | ||
| 498 | "purchaseNoteRestore": "If your purchase does not take effect, tap Restore Purchases.", | 529 | "purchaseNoteRestore": "If your purchase does not take effect, tap Restore Purchases.", |
| 499 | "purchaseNoteContact": "If you have any other questions, ", | 530 | "purchaseNoteContact": "If you have any other questions, ", |
| 500 | "purchaseLinkContactUs": "Contact Us", | 531 | "purchaseLinkContactUs": "Contact Us", |
| 501 | - "reportBottomSlogan": "FEEL MORE, STRESS LESS", | 532 | + "reportBottomSlogan": "·Double the Awareness, Half the Stress·", |
| 502 | "refundExplanationTitle": "Refund Information", | 533 | "refundExplanationTitle": "Refund Information", |
| 503 | "refundAppStoreReviewTitle": "Refunds are reviewed by the App Store", | 534 | "refundAppStoreReviewTitle": "Refunds are reviewed by the App Store", |
| 504 | "refundAppStoreReviewDescription": "All subscriptions and virtual products are purchased through the official App Store payment system. DoubleFeel cannot directly process payments or refunds.", | 535 | "refundAppStoreReviewDescription": "All subscriptions and virtual products are purchased through the official App Store payment system. DoubleFeel cannot directly process payments or refunds.", |
| @@ -540,23 +571,24 @@ | @@ -540,23 +571,24 @@ | ||
| 540 | "appReviewFeedbackSendAction": "Send Feedback", | 571 | "appReviewFeedbackSendAction": "Send Feedback", |
| 541 | "appReviewFeedbackLaterAction": "Maybe Later", | 572 | "appReviewFeedbackLaterAction": "Maybe Later", |
| 542 | "appReviewIllustrationPlaceholder": "Illustration Placeholder", | 573 | "appReviewIllustrationPlaceholder": "Illustration Placeholder", |
| 543 | - "overallStressLevelToday": "Overall Stress Level Today", | ||
| 544 | - "stressLevelsOnThatDay": "Stress Levels on That Day", | 574 | + "overallStressLevelToday": "here's your overall stress status", |
| 575 | + "stressLevelsOnThatDay": "Their Stress Levels on That Day", | ||
| 576 | + "stressLevelsToday": "Their Stress Status Today", | ||
| 545 | "noPressureDataAvailableAtThisTime": "No pressure data available at this time", | 577 | "noPressureDataAvailableAtThisTime": "No pressure data available at this time", |
| 546 | "membersCanViewTheCompleteData": "Unlock Pro to view", | 578 | "membersCanViewTheCompleteData": "Unlock Pro to view", |
| 547 | "unlockNow": "Unlock", | 579 | "unlockNow": "Unlock", |
| 548 | - "pressureOverload": "Pressure Overload", | ||
| 549 | - "beMindfulOfStress": "Be Aware of Stress", | ||
| 550 | - "statusNormal": "Status: Normal", | ||
| 551 | - "inExcellentCondition": "In excellent condition", | 580 | + "pressureOverload": "Overload", |
| 581 | + "beMindfulOfStress": "Stressful", | ||
| 582 | + "statusNormal": "Normal", | ||
| 583 | + "inExcellentCondition": "Excellent", | ||
| 552 | "waitingForData": "Waiting for data", | 584 | "waitingForData": "Waiting for data", |
| 553 | "pressure": "Pressure", | 585 | "pressure": "Pressure", |
| 554 | "mostRecent": "Latest Reading", | 586 | "mostRecent": "Latest Reading", |
| 555 | "theDayBeforeYesterday": "The day before yesterday", | 587 | "theDayBeforeYesterday": "The day before yesterday", |
| 556 | "uploadPhotos": "Upload Photos", | 588 | "uploadPhotos": "Upload Photos", |
| 557 | "filming": "Filming", | 589 | "filming": "Filming", |
| 558 | - "unlockTheProVersion": "Unlock the Pro Version", | ||
| 559 | - "embarkOnAJourneyOfStressAwarenessAndWellnessSupport": "Embark on a Journey of Stress Awareness and Wellness Support", | 590 | + "unlockTheProVersion": "Unlock Pro", |
| 591 | + "embarkOnAJourneyOfStressAwarenessAndWellnessSupport": "Begin Your Stress Alerts & Health Journey", | ||
| 560 | "sharePartnerCodeTemplate": "My friend ID: {inviteCode}. Hey❤! Come and use DoubleFeel with me! It helps us care for each other—track stress and sleep, view HRV and body status, and stay updated on each other's health in real-time. Come join me 👉 https://apps.apple.com/cn/app/doublefeel-%E5%8F%8C%E4%BA%BA%E6%83%85%E7%BB%AA%E5%85%B1%E4%BA%ABhrv%E5%8E%8B%E5%8A%9B%E6%B0%B4%E5%B9%B3%E8%87%AA%E6%B5%8B%E7%9D%A1%E7%9C%A0%E8%AE%B0%E5%BD%95/id6747254434", | 592 | "sharePartnerCodeTemplate": "My friend ID: {inviteCode}. Hey❤! Come and use DoubleFeel with me! It helps us care for each other—track stress and sleep, view HRV and body status, and stay updated on each other's health in real-time. Come join me 👉 https://apps.apple.com/cn/app/doublefeel-%E5%8F%8C%E4%BA%BA%E6%83%85%E7%BB%AA%E5%85%B1%E4%BA%ABhrv%E5%8E%8B%E5%8A%9B%E6%B0%B4%E5%B9%B3%E8%87%AA%E6%B5%8B%E7%9D%A1%E7%9C%A0%E8%AE%B0%E5%BD%95/id6747254434", |
| 561 | "@sharePartnerCodeTemplate": { | 593 | "@sharePartnerCodeTemplate": { |
| 562 | "description": "Share partner code template", | 594 | "description": "Share partner code template", |
| @@ -582,10 +614,10 @@ | @@ -582,10 +614,10 @@ | ||
| 582 | } | 614 | } |
| 583 | } | 615 | } |
| 584 | }, | 616 | }, |
| 585 | - "annualMemberDiscounts": "Special Offer", | 617 | + "annualMemberDiscounts": "Annual Membership Offer", |
| 586 | "specialOffers": "OFF", | 618 | "specialOffers": "OFF", |
| 587 | "currentPrice": "Now", | 619 | "currentPrice": "Now", |
| 588 | - "originalPrice": "Was {price}", | 620 | + "originalPrice": "{price}/y", |
| 589 | "@originalPrice": { | 621 | "@originalPrice": { |
| 590 | "description": "Original price label", | 622 | "description": "Original price label", |
| 591 | "placeholders": { | 623 | "placeholders": { |
| @@ -788,5 +820,9 @@ | @@ -788,5 +820,9 @@ | ||
| 788 | "continueWithEmail": "Continue with email", | 820 | "continueWithEmail": "Continue with email", |
| 789 | "continueWithGoogle": "Continue with Google", | 821 | "continueWithGoogle": "Continue with Google", |
| 790 | "emailLoginEmailHint": "Your email", | 822 | "emailLoginEmailHint": "Your email", |
| 791 | - "emailLoginYourPassword": "Your password" | ||
| 792 | -} | 823 | + "emailLoginYourPassword": "Your password", |
| 824 | + "yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue": "Your account was signed out due to another device login or token expiration. Please log in again to continue.", | ||
| 825 | + "contactUs": "Contact us", | ||
| 826 | + "pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible": "Please describe the problem clearly and include screen recordings if possible.", | ||
| 827 | + "sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "Send us your User ID as it will help us identify the problem faster." | ||
| 828 | +} |
| @@ -22,6 +22,11 @@ | @@ -22,6 +22,11 @@ | ||
| 22 | "tabFriends": "好友", | 22 | "tabFriends": "好友", |
| 23 | "tabMy": "我的", | 23 | "tabMy": "我的", |
| 24 | "switchLanguage": "切换语言", | 24 | "switchLanguage": "切换语言", |
| 25 | + "mainlandChinaServices": "中国大陆服务", | ||
| 26 | + "internationalServices": "国际服务", | ||
| 27 | + "mainlandChinaServicesDescription": "适用于主要使用中国大陆服务的用户,健康、账号、好友数据将存储于中国大陆。", | ||
| 28 | + "internationalServicesDescription": "适用于主要使用中国大陆以外服务的用户,健康、账号、好友数据将存储于国际。", | ||
| 29 | + "serviceRegionCannotBeChanged": "账号创建后,服务区域不能直接更改。", | ||
| 25 | "settings": "设置", | 30 | "settings": "设置", |
| 26 | "onboardingIntroTitle": "DoubleFeel 是专为 Apple Watch 打造的健康陪伴app", | 31 | "onboardingIntroTitle": "DoubleFeel 是专为 Apple Watch 打造的健康陪伴app", |
| 27 | "onboardingIntroBody": "我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>", | 32 | "onboardingIntroBody": "我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>", |
| @@ -338,6 +343,32 @@ | @@ -338,6 +343,32 @@ | ||
| 338 | "frequentlyAskedQuestions": "常见问题", | 343 | "frequentlyAskedQuestions": "常见问题", |
| 339 | "areYouSureYouWantToDeleteYourAccount": "确认注销账号吗?", | 344 | "areYouSureYouWantToDeleteYourAccount": "确认注销账号吗?", |
| 340 | "accountSettings": "账号设置", | 345 | "accountSettings": "账号设置", |
| 346 | + "appleAccount": "Apple Account", | ||
| 347 | + "googleAccount": "Google Account", | ||
| 348 | + "securityEmail": "安全邮箱", | ||
| 349 | + "addSecurityEmail": "添加安全邮箱", | ||
| 350 | + "securityEmailDescription": "添加安全邮箱,帮助保护你的账号。", | ||
| 351 | + "securityEmailHint": "请输入邮箱地址", | ||
| 352 | + "sendVerificationEmail": "发送验证邮件", | ||
| 353 | + "confirmYourEmail": "验证你的邮箱", | ||
| 354 | + "enterCodeSentTo": "请输入发送至 {email} 的验证码", | ||
| 355 | + "@enterCodeSentTo": { | ||
| 356 | + "placeholders": { | ||
| 357 | + "email": {} | ||
| 358 | + } | ||
| 359 | + }, | ||
| 360 | + "emailVerificationHelp": "如果没有收到邮件,请检查垃圾邮件、推广邮件或其他文件夹。", | ||
| 361 | + "verificationCode": "验证码", | ||
| 362 | + "resend": "重新发送", | ||
| 363 | + "resendWithSeconds": "重新发送({seconds})", | ||
| 364 | + "@resendWithSeconds": { | ||
| 365 | + "placeholders": { | ||
| 366 | + "seconds": { | ||
| 367 | + "type": "int" | ||
| 368 | + } | ||
| 369 | + } | ||
| 370 | + }, | ||
| 371 | + "invalidEmailFormat": "邮箱格式错误", | ||
| 341 | "deleteAccount": "注销账号", | 372 | "deleteAccount": "注销账号", |
| 342 | "mobilePhoneNumber": "手机号", | 373 | "mobilePhoneNumber": "手机号", |
| 343 | "logOut": "退出登录", | 374 | "logOut": "退出登录", |
| @@ -935,6 +966,7 @@ | @@ -935,6 +966,7 @@ | ||
| 935 | "appReviewIllustrationPlaceholder": "插图占位", | 966 | "appReviewIllustrationPlaceholder": "插图占位", |
| 936 | "overallStressLevelToday": "今日的综合压力状态", | 967 | "overallStressLevelToday": "今日的综合压力状态", |
| 937 | "stressLevelsOnThatDay": "该日压力状态", | 968 | "stressLevelsOnThatDay": "该日压力状态", |
| 969 | + "stressLevelsToday": "今日压力状态", | ||
| 938 | "noPressureDataAvailableAtThisTime": "暂无压力数据", | 970 | "noPressureDataAvailableAtThisTime": "暂无压力数据", |
| 939 | "membersCanViewTheCompleteData": "会员可查看完整数据", | 971 | "membersCanViewTheCompleteData": "会员可查看完整数据", |
| 940 | "unlockNow": "立即解锁", | 972 | "unlockNow": "立即解锁", |
| @@ -1181,5 +1213,9 @@ | @@ -1181,5 +1213,9 @@ | ||
| 1181 | "continueWithEmail": "使用电子邮件", | 1213 | "continueWithEmail": "使用电子邮件", |
| 1182 | "continueWithGoogle": "使用 Google", | 1214 | "continueWithGoogle": "使用 Google", |
| 1183 | "emailLoginEmailHint": "请输入邮箱", | 1215 | "emailLoginEmailHint": "请输入邮箱", |
| 1184 | - "emailLoginYourPassword": "请输入密码" | ||
| 1185 | -} | 1216 | + "emailLoginYourPassword": "请输入密码", |
| 1217 | + "yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue": "由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。", | ||
| 1218 | + "contactUs": "联系我们", | ||
| 1219 | + "pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible": "请清楚地描述问题,并尽可能附上屏幕录像。", | ||
| 1220 | + "sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。" | ||
| 1221 | +} |
| @@ -212,6 +212,36 @@ abstract class AppLocalizations { | @@ -212,6 +212,36 @@ abstract class AppLocalizations { | ||
| 212 | /// **'切换语言'** | 212 | /// **'切换语言'** |
| 213 | String get switchLanguage; | 213 | String get switchLanguage; |
| 214 | 214 | ||
| 215 | + /// No description provided for @mainlandChinaServices. | ||
| 216 | + /// | ||
| 217 | + /// In zh, this message translates to: | ||
| 218 | + /// **'中国大陆服务'** | ||
| 219 | + String get mainlandChinaServices; | ||
| 220 | + | ||
| 221 | + /// No description provided for @internationalServices. | ||
| 222 | + /// | ||
| 223 | + /// In zh, this message translates to: | ||
| 224 | + /// **'国际服务'** | ||
| 225 | + String get internationalServices; | ||
| 226 | + | ||
| 227 | + /// No description provided for @mainlandChinaServicesDescription. | ||
| 228 | + /// | ||
| 229 | + /// In zh, this message translates to: | ||
| 230 | + /// **'适用于主要使用中国大陆服务的用户,健康、账号、好友数据将存储于中国大陆。'** | ||
| 231 | + String get mainlandChinaServicesDescription; | ||
| 232 | + | ||
| 233 | + /// No description provided for @internationalServicesDescription. | ||
| 234 | + /// | ||
| 235 | + /// In zh, this message translates to: | ||
| 236 | + /// **'适用于主要使用中国大陆以外服务的用户,健康、账号、好友数据将存储于国际。'** | ||
| 237 | + String get internationalServicesDescription; | ||
| 238 | + | ||
| 239 | + /// No description provided for @serviceRegionCannotBeChanged. | ||
| 240 | + /// | ||
| 241 | + /// In zh, this message translates to: | ||
| 242 | + /// **'账号创建后,服务区域不能直接更改。'** | ||
| 243 | + String get serviceRegionCannotBeChanged; | ||
| 244 | + | ||
| 215 | /// No description provided for @settings. | 245 | /// No description provided for @settings. |
| 216 | /// | 246 | /// |
| 217 | /// In zh, this message translates to: | 247 | /// In zh, this message translates to: |
| @@ -1629,6 +1659,90 @@ abstract class AppLocalizations { | @@ -1629,6 +1659,90 @@ abstract class AppLocalizations { | ||
| 1629 | /// **'账号设置'** | 1659 | /// **'账号设置'** |
| 1630 | String get accountSettings; | 1660 | String get accountSettings; |
| 1631 | 1661 | ||
| 1662 | + /// No description provided for @appleAccount. | ||
| 1663 | + /// | ||
| 1664 | + /// In zh, this message translates to: | ||
| 1665 | + /// **'Apple Account'** | ||
| 1666 | + String get appleAccount; | ||
| 1667 | + | ||
| 1668 | + /// No description provided for @googleAccount. | ||
| 1669 | + /// | ||
| 1670 | + /// In zh, this message translates to: | ||
| 1671 | + /// **'Google Account'** | ||
| 1672 | + String get googleAccount; | ||
| 1673 | + | ||
| 1674 | + /// No description provided for @securityEmail. | ||
| 1675 | + /// | ||
| 1676 | + /// In zh, this message translates to: | ||
| 1677 | + /// **'安全邮箱'** | ||
| 1678 | + String get securityEmail; | ||
| 1679 | + | ||
| 1680 | + /// No description provided for @addSecurityEmail. | ||
| 1681 | + /// | ||
| 1682 | + /// In zh, this message translates to: | ||
| 1683 | + /// **'添加安全邮箱'** | ||
| 1684 | + String get addSecurityEmail; | ||
| 1685 | + | ||
| 1686 | + /// No description provided for @securityEmailDescription. | ||
| 1687 | + /// | ||
| 1688 | + /// In zh, this message translates to: | ||
| 1689 | + /// **'添加安全邮箱,帮助保护你的账号。'** | ||
| 1690 | + String get securityEmailDescription; | ||
| 1691 | + | ||
| 1692 | + /// No description provided for @securityEmailHint. | ||
| 1693 | + /// | ||
| 1694 | + /// In zh, this message translates to: | ||
| 1695 | + /// **'请输入邮箱地址'** | ||
| 1696 | + String get securityEmailHint; | ||
| 1697 | + | ||
| 1698 | + /// No description provided for @sendVerificationEmail. | ||
| 1699 | + /// | ||
| 1700 | + /// In zh, this message translates to: | ||
| 1701 | + /// **'发送验证邮件'** | ||
| 1702 | + String get sendVerificationEmail; | ||
| 1703 | + | ||
| 1704 | + /// No description provided for @confirmYourEmail. | ||
| 1705 | + /// | ||
| 1706 | + /// In zh, this message translates to: | ||
| 1707 | + /// **'验证你的邮箱'** | ||
| 1708 | + String get confirmYourEmail; | ||
| 1709 | + | ||
| 1710 | + /// No description provided for @enterCodeSentTo. | ||
| 1711 | + /// | ||
| 1712 | + /// In zh, this message translates to: | ||
| 1713 | + /// **'请输入发送至 {email} 的验证码'** | ||
| 1714 | + String enterCodeSentTo(Object email); | ||
| 1715 | + | ||
| 1716 | + /// No description provided for @emailVerificationHelp. | ||
| 1717 | + /// | ||
| 1718 | + /// In zh, this message translates to: | ||
| 1719 | + /// **'如果没有收到邮件,请检查垃圾邮件、推广邮件或其他文件夹。'** | ||
| 1720 | + String get emailVerificationHelp; | ||
| 1721 | + | ||
| 1722 | + /// No description provided for @verificationCode. | ||
| 1723 | + /// | ||
| 1724 | + /// In zh, this message translates to: | ||
| 1725 | + /// **'验证码'** | ||
| 1726 | + String get verificationCode; | ||
| 1727 | + | ||
| 1728 | + /// No description provided for @resend. | ||
| 1729 | + /// | ||
| 1730 | + /// In zh, this message translates to: | ||
| 1731 | + /// **'重新发送'** | ||
| 1732 | + String get resend; | ||
| 1733 | + | ||
| 1734 | + /// No description provided for @resendWithSeconds. | ||
| 1735 | + /// | ||
| 1736 | + /// In zh, this message translates to: | ||
| 1737 | + /// **'重新发送({seconds})'** | ||
| 1738 | + String resendWithSeconds(int seconds); | ||
| 1739 | + | ||
| 1740 | + /// No description provided for @invalidEmailFormat. | ||
| 1741 | + /// | ||
| 1742 | + /// In zh, this message translates to: | ||
| 1743 | + /// **'邮箱格式错误'** | ||
| 1744 | + String get invalidEmailFormat; | ||
| 1745 | + | ||
| 1632 | /// No description provided for @deleteAccount. | 1746 | /// No description provided for @deleteAccount. |
| 1633 | /// | 1747 | /// |
| 1634 | /// In zh, this message translates to: | 1748 | /// In zh, this message translates to: |
| @@ -3351,6 +3465,12 @@ abstract class AppLocalizations { | @@ -3351,6 +3465,12 @@ abstract class AppLocalizations { | ||
| 3351 | /// **'该日压力状态'** | 3465 | /// **'该日压力状态'** |
| 3352 | String get stressLevelsOnThatDay; | 3466 | String get stressLevelsOnThatDay; |
| 3353 | 3467 | ||
| 3468 | + /// No description provided for @stressLevelsToday. | ||
| 3469 | + /// | ||
| 3470 | + /// In zh, this message translates to: | ||
| 3471 | + /// **'今日压力状态'** | ||
| 3472 | + String get stressLevelsToday; | ||
| 3473 | + | ||
| 3354 | /// No description provided for @noPressureDataAvailableAtThisTime. | 3474 | /// No description provided for @noPressureDataAvailableAtThisTime. |
| 3355 | /// | 3475 | /// |
| 3356 | /// In zh, this message translates to: | 3476 | /// In zh, this message translates to: |
| @@ -4203,6 +4323,32 @@ abstract class AppLocalizations { | @@ -4203,6 +4323,32 @@ abstract class AppLocalizations { | ||
| 4203 | /// In zh, this message translates to: | 4323 | /// In zh, this message translates to: |
| 4204 | /// **'请输入密码'** | 4324 | /// **'请输入密码'** |
| 4205 | String get emailLoginYourPassword; | 4325 | String get emailLoginYourPassword; |
| 4326 | + | ||
| 4327 | + /// No description provided for @yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue. | ||
| 4328 | + /// | ||
| 4329 | + /// In zh, this message translates to: | ||
| 4330 | + /// **'由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。'** | ||
| 4331 | + String | ||
| 4332 | + get yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue; | ||
| 4333 | + | ||
| 4334 | + /// No description provided for @contactUs. | ||
| 4335 | + /// | ||
| 4336 | + /// In zh, this message translates to: | ||
| 4337 | + /// **'联系我们'** | ||
| 4338 | + String get contactUs; | ||
| 4339 | + | ||
| 4340 | + /// No description provided for @pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible. | ||
| 4341 | + /// | ||
| 4342 | + /// In zh, this message translates to: | ||
| 4343 | + /// **'请清楚地描述问题,并尽可能附上屏幕录像。'** | ||
| 4344 | + String | ||
| 4345 | + get pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible; | ||
| 4346 | + | ||
| 4347 | + /// No description provided for @sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster. | ||
| 4348 | + /// | ||
| 4349 | + /// In zh, this message translates to: | ||
| 4350 | + /// **'请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。'** | ||
| 4351 | + String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster; | ||
| 4206 | } | 4352 | } |
| 4207 | 4353 | ||
| 4208 | class _AppLocalizationsDelegate | 4354 | class _AppLocalizationsDelegate |
| @@ -66,6 +66,24 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -66,6 +66,24 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 66 | String get switchLanguage => 'Switch Language'; | 66 | String get switchLanguage => 'Switch Language'; |
| 67 | 67 | ||
| 68 | @override | 68 | @override |
| 69 | + String get mainlandChinaServices => 'Mainland China Services'; | ||
| 70 | + | ||
| 71 | + @override | ||
| 72 | + String get internationalServices => 'International Services'; | ||
| 73 | + | ||
| 74 | + @override | ||
| 75 | + String get mainlandChinaServicesDescription => | ||
| 76 | + 'For users mainly in Mainland China. Health, account, and friend data is stored in Mainland China.'; | ||
| 77 | + | ||
| 78 | + @override | ||
| 79 | + String get internationalServicesDescription => | ||
| 80 | + 'For users mainly outside Mainland China. Health, account, and friend data is stored internationally.'; | ||
| 81 | + | ||
| 82 | + @override | ||
| 83 | + String get serviceRegionCannotBeChanged => | ||
| 84 | + 'Service region cannot be changed after account creation.'; | ||
| 85 | + | ||
| 86 | + @override | ||
| 69 | String get settings => 'Settings'; | 87 | String get settings => 'Settings'; |
| 70 | 88 | ||
| 71 | @override | 89 | @override |
| @@ -800,14 +818,13 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -800,14 +818,13 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 800 | String get clickToAddTheHrvThemedWatchFace => 'Add HRV Watch Face'; | 818 | String get clickToAddTheHrvThemedWatchFace => 'Add HRV Watch Face'; |
| 801 | 819 | ||
| 802 | @override | 820 | @override |
| 803 | - String get stayOnTopOfYourHealthFluctuations => 'Stay on top of your health'; | 821 | + String get stayOnTopOfYourHealthFluctuations => 'Track your body changes'; |
| 804 | 822 | ||
| 805 | @override | 823 | @override |
| 806 | - String get addACloseContact => 'Add a Close Contact'; | 824 | + String get addACloseContact => 'Add a Loved One'; |
| 807 | 825 | ||
| 808 | @override | 826 | @override |
| 809 | - String get oneMorePersonLookingOutForYourHealth => | ||
| 810 | - 'Have someone care about your health'; | 827 | + String get oneMorePersonLookingOutForYourHealth => 'Follow your health'; |
| 811 | 828 | ||
| 812 | @override | 829 | @override |
| 813 | String get addAFriend => 'Add'; | 830 | String get addAFriend => 'Add'; |
| @@ -885,6 +902,54 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -885,6 +902,54 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 885 | String get accountSettings => 'Account Settings'; | 902 | String get accountSettings => 'Account Settings'; |
| 886 | 903 | ||
| 887 | @override | 904 | @override |
| 905 | + String get appleAccount => 'Apple Account'; | ||
| 906 | + | ||
| 907 | + @override | ||
| 908 | + String get googleAccount => 'Google Account'; | ||
| 909 | + | ||
| 910 | + @override | ||
| 911 | + String get securityEmail => 'Email'; | ||
| 912 | + | ||
| 913 | + @override | ||
| 914 | + String get addSecurityEmail => 'Add security email'; | ||
| 915 | + | ||
| 916 | + @override | ||
| 917 | + String get securityEmailDescription => | ||
| 918 | + 'Use a security email to help protect your account.'; | ||
| 919 | + | ||
| 920 | + @override | ||
| 921 | + String get securityEmailHint => 'Email address'; | ||
| 922 | + | ||
| 923 | + @override | ||
| 924 | + String get sendVerificationEmail => 'Send verification email'; | ||
| 925 | + | ||
| 926 | + @override | ||
| 927 | + String get confirmYourEmail => 'Confirm your email'; | ||
| 928 | + | ||
| 929 | + @override | ||
| 930 | + String enterCodeSentTo(Object email) { | ||
| 931 | + return 'Enter the code sent to $email'; | ||
| 932 | + } | ||
| 933 | + | ||
| 934 | + @override | ||
| 935 | + String get emailVerificationHelp => | ||
| 936 | + 'If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders'; | ||
| 937 | + | ||
| 938 | + @override | ||
| 939 | + String get verificationCode => 'Verification code'; | ||
| 940 | + | ||
| 941 | + @override | ||
| 942 | + String get resend => 'Re-send'; | ||
| 943 | + | ||
| 944 | + @override | ||
| 945 | + String resendWithSeconds(int seconds) { | ||
| 946 | + return 'Re-send($seconds)'; | ||
| 947 | + } | ||
| 948 | + | ||
| 949 | + @override | ||
| 950 | + String get invalidEmailFormat => 'Invalid email format'; | ||
| 951 | + | ||
| 952 | + @override | ||
| 888 | String get deleteAccount => 'Delete Account'; | 953 | String get deleteAccount => 'Delete Account'; |
| 889 | 954 | ||
| 890 | @override | 955 | @override |
| @@ -1077,7 +1142,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1077,7 +1142,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1077 | 1142 | ||
| 1078 | @override | 1143 | @override |
| 1079 | String reportOtherPossessiveTitle(String title) { | 1144 | String reportOtherPossessiveTitle(String title) { |
| 1080 | - return '$title'; | 1145 | + return 'Their $title'; |
| 1081 | } | 1146 | } |
| 1082 | 1147 | ||
| 1083 | @override | 1148 | @override |
| @@ -1716,7 +1781,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1716,7 +1781,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1716 | String get purchaseLinkContactUs => 'Contact Us'; | 1781 | String get purchaseLinkContactUs => 'Contact Us'; |
| 1717 | 1782 | ||
| 1718 | @override | 1783 | @override |
| 1719 | - String get reportBottomSlogan => 'FEEL MORE, STRESS LESS'; | 1784 | + String get reportBottomSlogan => '·Double the Awareness, Half the Stress·'; |
| 1720 | 1785 | ||
| 1721 | @override | 1786 | @override |
| 1722 | String get refundExplanationTitle => 'Refund Information'; | 1787 | String get refundExplanationTitle => 'Refund Information'; |
| @@ -1868,10 +1933,13 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1868,10 +1933,13 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1868 | String get appReviewIllustrationPlaceholder => 'Illustration Placeholder'; | 1933 | String get appReviewIllustrationPlaceholder => 'Illustration Placeholder'; |
| 1869 | 1934 | ||
| 1870 | @override | 1935 | @override |
| 1871 | - String get overallStressLevelToday => 'Overall Stress Level Today'; | 1936 | + String get overallStressLevelToday => 'here\'s your overall stress status'; |
| 1937 | + | ||
| 1938 | + @override | ||
| 1939 | + String get stressLevelsOnThatDay => 'Their Stress Levels on That Day'; | ||
| 1872 | 1940 | ||
| 1873 | @override | 1941 | @override |
| 1874 | - String get stressLevelsOnThatDay => 'Stress Levels on That Day'; | 1942 | + String get stressLevelsToday => 'Their Stress Status Today'; |
| 1875 | 1943 | ||
| 1876 | @override | 1944 | @override |
| 1877 | String get noPressureDataAvailableAtThisTime => | 1945 | String get noPressureDataAvailableAtThisTime => |
| @@ -1884,16 +1952,16 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1884,16 +1952,16 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1884 | String get unlockNow => 'Unlock'; | 1952 | String get unlockNow => 'Unlock'; |
| 1885 | 1953 | ||
| 1886 | @override | 1954 | @override |
| 1887 | - String get pressureOverload => 'Pressure Overload'; | 1955 | + String get pressureOverload => 'Overload'; |
| 1888 | 1956 | ||
| 1889 | @override | 1957 | @override |
| 1890 | - String get beMindfulOfStress => 'Be Aware of Stress'; | 1958 | + String get beMindfulOfStress => 'Stressful'; |
| 1891 | 1959 | ||
| 1892 | @override | 1960 | @override |
| 1893 | - String get statusNormal => 'Status: Normal'; | 1961 | + String get statusNormal => 'Normal'; |
| 1894 | 1962 | ||
| 1895 | @override | 1963 | @override |
| 1896 | - String get inExcellentCondition => 'In excellent condition'; | 1964 | + String get inExcellentCondition => 'Excellent'; |
| 1897 | 1965 | ||
| 1898 | @override | 1966 | @override |
| 1899 | String get waitingForData => 'Waiting for data'; | 1967 | String get waitingForData => 'Waiting for data'; |
| @@ -1914,11 +1982,11 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1914,11 +1982,11 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1914 | String get filming => 'Filming'; | 1982 | String get filming => 'Filming'; |
| 1915 | 1983 | ||
| 1916 | @override | 1984 | @override |
| 1917 | - String get unlockTheProVersion => 'Unlock the Pro Version'; | 1985 | + String get unlockTheProVersion => 'Unlock Pro'; |
| 1918 | 1986 | ||
| 1919 | @override | 1987 | @override |
| 1920 | String get embarkOnAJourneyOfStressAwarenessAndWellnessSupport => | 1988 | String get embarkOnAJourneyOfStressAwarenessAndWellnessSupport => |
| 1921 | - 'Embark on a Journey of Stress Awareness and Wellness Support'; | 1989 | + 'Begin Your Stress Alerts & Health Journey'; |
| 1922 | 1990 | ||
| 1923 | @override | 1991 | @override |
| 1924 | String sharePartnerCodeTemplate(String inviteCode) { | 1992 | String sharePartnerCodeTemplate(String inviteCode) { |
| @@ -1954,7 +2022,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1954,7 +2022,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1954 | } | 2022 | } |
| 1955 | 2023 | ||
| 1956 | @override | 2024 | @override |
| 1957 | - String get annualMemberDiscounts => 'Special Offer'; | 2025 | + String get annualMemberDiscounts => 'Annual Membership Offer'; |
| 1958 | 2026 | ||
| 1959 | @override | 2027 | @override |
| 1960 | String get specialOffers => 'OFF'; | 2028 | String get specialOffers => 'OFF'; |
| @@ -1964,7 +2032,7 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -1964,7 +2032,7 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 1964 | 2032 | ||
| 1965 | @override | 2033 | @override |
| 1966 | String originalPrice(String price) { | 2034 | String originalPrice(String price) { |
| 1967 | - return 'Was $price'; | 2035 | + return '$price/y'; |
| 1968 | } | 2036 | } |
| 1969 | 2037 | ||
| 1970 | @override | 2038 | @override |
| @@ -2360,4 +2428,19 @@ class AppLocalizationsEn extends AppLocalizations { | @@ -2360,4 +2428,19 @@ class AppLocalizationsEn extends AppLocalizations { | ||
| 2360 | 2428 | ||
| 2361 | @override | 2429 | @override |
| 2362 | String get emailLoginYourPassword => 'Your password'; | 2430 | String get emailLoginYourPassword => 'Your password'; |
| 2431 | + | ||
| 2432 | + @override | ||
| 2433 | + String get yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue => | ||
| 2434 | + 'Your account was signed out due to another device login or token expiration. Please log in again to continue.'; | ||
| 2435 | + | ||
| 2436 | + @override | ||
| 2437 | + String get contactUs => 'Contact us'; | ||
| 2438 | + | ||
| 2439 | + @override | ||
| 2440 | + String get pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible => | ||
| 2441 | + 'Please describe the problem clearly and include screen recordings if possible.'; | ||
| 2442 | + | ||
| 2443 | + @override | ||
| 2444 | + String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster => | ||
| 2445 | + 'Send us your User ID as it will help us identify the problem faster.'; | ||
| 2363 | } | 2446 | } |
| @@ -66,6 +66,23 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -66,6 +66,23 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 66 | String get switchLanguage => '切换语言'; | 66 | String get switchLanguage => '切换语言'; |
| 67 | 67 | ||
| 68 | @override | 68 | @override |
| 69 | + String get mainlandChinaServices => '中国大陆服务'; | ||
| 70 | + | ||
| 71 | + @override | ||
| 72 | + String get internationalServices => '国际服务'; | ||
| 73 | + | ||
| 74 | + @override | ||
| 75 | + String get mainlandChinaServicesDescription => | ||
| 76 | + '适用于主要使用中国大陆服务的用户,健康、账号、好友数据将存储于中国大陆。'; | ||
| 77 | + | ||
| 78 | + @override | ||
| 79 | + String get internationalServicesDescription => | ||
| 80 | + '适用于主要使用中国大陆以外服务的用户,健康、账号、好友数据将存储于国际。'; | ||
| 81 | + | ||
| 82 | + @override | ||
| 83 | + String get serviceRegionCannotBeChanged => '账号创建后,服务区域不能直接更改。'; | ||
| 84 | + | ||
| 85 | + @override | ||
| 69 | String get settings => '设置'; | 86 | String get settings => '设置'; |
| 70 | 87 | ||
| 71 | @override | 88 | @override |
| @@ -835,6 +852,52 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -835,6 +852,52 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 835 | String get accountSettings => '账号设置'; | 852 | String get accountSettings => '账号设置'; |
| 836 | 853 | ||
| 837 | @override | 854 | @override |
| 855 | + String get appleAccount => 'Apple Account'; | ||
| 856 | + | ||
| 857 | + @override | ||
| 858 | + String get googleAccount => 'Google Account'; | ||
| 859 | + | ||
| 860 | + @override | ||
| 861 | + String get securityEmail => '安全邮箱'; | ||
| 862 | + | ||
| 863 | + @override | ||
| 864 | + String get addSecurityEmail => '添加安全邮箱'; | ||
| 865 | + | ||
| 866 | + @override | ||
| 867 | + String get securityEmailDescription => '添加安全邮箱,帮助保护你的账号。'; | ||
| 868 | + | ||
| 869 | + @override | ||
| 870 | + String get securityEmailHint => '请输入邮箱地址'; | ||
| 871 | + | ||
| 872 | + @override | ||
| 873 | + String get sendVerificationEmail => '发送验证邮件'; | ||
| 874 | + | ||
| 875 | + @override | ||
| 876 | + String get confirmYourEmail => '验证你的邮箱'; | ||
| 877 | + | ||
| 878 | + @override | ||
| 879 | + String enterCodeSentTo(Object email) { | ||
| 880 | + return '请输入发送至 $email 的验证码'; | ||
| 881 | + } | ||
| 882 | + | ||
| 883 | + @override | ||
| 884 | + String get emailVerificationHelp => '如果没有收到邮件,请检查垃圾邮件、推广邮件或其他文件夹。'; | ||
| 885 | + | ||
| 886 | + @override | ||
| 887 | + String get verificationCode => '验证码'; | ||
| 888 | + | ||
| 889 | + @override | ||
| 890 | + String get resend => '重新发送'; | ||
| 891 | + | ||
| 892 | + @override | ||
| 893 | + String resendWithSeconds(int seconds) { | ||
| 894 | + return '重新发送($seconds)'; | ||
| 895 | + } | ||
| 896 | + | ||
| 897 | + @override | ||
| 898 | + String get invalidEmailFormat => '邮箱格式错误'; | ||
| 899 | + | ||
| 900 | + @override | ||
| 838 | String get deleteAccount => '注销账号'; | 901 | String get deleteAccount => '注销账号'; |
| 839 | 902 | ||
| 840 | @override | 903 | @override |
| @@ -1792,6 +1855,9 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -1792,6 +1855,9 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 1792 | String get stressLevelsOnThatDay => '该日压力状态'; | 1855 | String get stressLevelsOnThatDay => '该日压力状态'; |
| 1793 | 1856 | ||
| 1794 | @override | 1857 | @override |
| 1858 | + String get stressLevelsToday => '今日压力状态'; | ||
| 1859 | + | ||
| 1860 | + @override | ||
| 1795 | String get noPressureDataAvailableAtThisTime => '暂无压力数据'; | 1861 | String get noPressureDataAvailableAtThisTime => '暂无压力数据'; |
| 1796 | 1862 | ||
| 1797 | @override | 1863 | @override |
| @@ -2261,4 +2327,21 @@ class AppLocalizationsZh extends AppLocalizations { | @@ -2261,4 +2327,21 @@ class AppLocalizationsZh extends AppLocalizations { | ||
| 2261 | 2327 | ||
| 2262 | @override | 2328 | @override |
| 2263 | String get emailLoginYourPassword => '请输入密码'; | 2329 | String get emailLoginYourPassword => '请输入密码'; |
| 2330 | + | ||
| 2331 | + @override | ||
| 2332 | + String | ||
| 2333 | + get yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue => | ||
| 2334 | + '由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。'; | ||
| 2335 | + | ||
| 2336 | + @override | ||
| 2337 | + String get contactUs => '联系我们'; | ||
| 2338 | + | ||
| 2339 | + @override | ||
| 2340 | + String | ||
| 2341 | + get pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible => | ||
| 2342 | + '请清楚地描述问题,并尽可能附上屏幕录像。'; | ||
| 2343 | + | ||
| 2344 | + @override | ||
| 2345 | + String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster => | ||
| 2346 | + '请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。'; | ||
| 2264 | } | 2347 | } |
-
Please register or login to post a comment