Commit c17ab416797fa99d3df64c1ff48897c9eae004f1

Authored by 常守达
1 parent 035f1ece

feat(login): 登陆国际化

@@ -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 }