Commit c17ab416797fa99d3df64c1ff48897c9eae004f1

Authored by 常守达
1 parent 035f1ece

feat(login): 登陆国际化

... ... @@ -7,6 +7,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_
import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
... ... @@ -120,6 +121,8 @@ class TodayController extends GetMaterialController {
showPartnerAdBanner.value = false;
}
final environmentConfig = Get.find<AppEnvironmentConfig>();
// ── HRV 趋势图 ────────────────────────────
final hrvChartData = <V2HrvTrendItem>[].obs;
final stressChartData = <V2RealtimeStressItem>[].obs;
... ... @@ -516,10 +519,14 @@ class TodayController extends GetMaterialController {
Future<void> _refreshHealthDataForDate(DateTime date) async {
_clearRealTimeData();
final intDate = int.parse(DateFormat('yyyyMMdd').format(date));
final isToday = DateUtils.isSameDay(date, lastSelectableDay.value);
stressSubtitle.value = (isFriend ? 'Ta' : '') +
(isToday ? l10n.overallStressLevelToday : l10n.stressLevelsOnThatDay);
stressSubtitle.value = environmentConfig.region.value == AppRegion.china
? 'Hi, ${isFriend ? 'Ta' : ''}${isToday ? l10n.overallStressLevelToday : l10n.stressLevelsOnThatDay}'
: isFriend
? isToday
? l10n.stressLevelsToday
: l10n.stressLevelsOnThatDay
: 'Hi, ${l10n.overallStressLevelToday}';
switch (await _healthApi.getV2HealthData(friendUserId, intDate)) {
case AppSuccess(:final data):
v2HealthData.value = data;
... ...
... ... @@ -2,7 +2,9 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/input_dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_cn_view.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/my/account_setting_view.dart';
import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/assets_helper.dart';
... ... @@ -59,12 +61,25 @@ class MyTab extends GetView<MyController> {
const SizedBox(height: 12),
_SettingsRow(
title: context.l10n.accountInformation,
onTap: () => Get.to(() => const AccountSettingView()),
onTap: () => Get.to(
() => environmentConfig.region.value == AppRegion.china
? const AccountSettingCNView()
: const AccountSettingView(),
),
),
const SizedBox(height: 12),
_SettingsRow(
title: context.l10n.help,
onTap: () => Get.toNamed(Routes.HELP),
onTap: (() {
if (environmentConfig.region.value == AppRegion.china) {
Get.toNamed(Routes.HELP);
} else {
showEmailLoginContactUsBottomSheet(
userId: (me?.id ?? 0).toString(),
pageName: 'Setting page',
);
}
}),
),
if (environmentConfig.isDebug) ...[
const SizedBox(height: 12),
... ...
... ... @@ -4,6 +4,7 @@ import 'package:doublefeel_flutter/app/modules/home/widgets/today/hrv_measuremen
import 'package:doublefeel_flutter/app/modules/report_common/models/report_period.dart';
import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_bottom_slogan.dart';
import 'package:doublefeel_flutter/app/modules/report_common/widgets/report_date_picker_sheet.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/app_config_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
... ... @@ -159,7 +160,7 @@ class TodayTabBody extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Hi,${controller.stressSubtitle.value}',
controller.stressSubtitle.value,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFF78787D),
... ... @@ -422,7 +423,8 @@ class TodayTabBody extends StatelessWidget {
)
: null,
child: Text(
isSameDay(day, controller.lastSelectableDay.value)
isSameDay(day, controller.lastSelectableDay.value) &&
controller.environmentConfig.region.value == AppRegion.china
? l10n.todayOnWeeklyCalendar
: _weekdayText(context, day),
style: TextStyle(
... ...
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/loading_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/core/theme/app_colors.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class AccountSettingCNView extends GetView<MyController> {
const AccountSettingCNView({super.key});
@override
Widget build(BuildContext context) {
final userPrefs = Get.find<UserPreferencesStorage>();
return Scaffold(
appBar: AppBar(
title: Text(
context.l10n.accountSettings,
),
leading: IconButton(
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: Get.back,
icon: Image.asset(
'assets/images/common/ic_nav_back.webp',
width: 24,
height: 24,
),
),
),
body: Obx(() {
final telephone =
userPrefs.preferences.value.meUserInfo?.telephone ?? '';
final thirdAccountId =
userPrefs.preferences.value.meUserInfo?.thirdAccountId ?? '';
return Column(
children: [
SizedBox(height: 16),
if (thirdAccountId.isNotEmpty)
_buildAccountCard(
context: context,
title: 'Apple ID',
content: thirdAccountId,
)
else
_buildAccountCard(
context: context,
title: context.l10n.mobilePhoneNumber,
content: telephone,
),
SizedBox(height: 20),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Get.bottomSheet(
_DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount),
barrierColor: Colors.black.withValues(alpha: 0.7),
enableDrag: true,
isScrollControlled: true,
persistent: false,
);
},
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 24,
vertical: 8,
),
child: Text(
context.l10n.deleteAccount,
style: TextStyle(
color: AppColors.warning,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
),
],
);
}),
);
}
Widget _buildAccountCard({
required BuildContext context,
required String title,
required String content,
}) {
return Container(
height: 110,
margin: EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
SizedBox(
height: 54,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(
title,
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
SizedBox(width: 16),
Expanded(
child: Text(
content,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
],
),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 21),
child: const Divider(
height: 1,
thickness: 0.5,
color: Color(0xFFF3F3F3),
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _logout,
child: Center(
child: Text(
context.l10n.logOut,
style: TextStyle(
color: AppColors.primary,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
),
),
),
),
),
],
),
);
}
Future<void> _logout() async {
final confirmResult = await DialogUtils.showCommonDialog(DialogMetaData(
title: l10n.logOut,
message: l10n.confirmLogoutPrompt,
confirmText: l10n.confirmLogout,
iconAsset: null));
if (confirmResult.action != DialogAction.confirm) return;
await LoadingService.instance.run(() async {
await Get.find<UserStateService>().onLogout();
});
AppToast.show(l10n.loggedOutSuccessfully);
Get.offAllNamed(AppRoutes.login);
}
Future<void> _deleteAccount() async {
await LoadingService.instance.run(() async {
final deleteResult = await Get.find<UserApi>().deleteAccount();
if (deleteResult is! AppSuccess<void>) return;
await Get.find<UserStateService>().onLogout(callServerLogout: false);
});
AppToast.show(l10n.accountDeletedSuccessfully);
Get.offAllNamed(AppRoutes.login);
}
}
class _DeleteAccountBottomSheet extends StatelessWidget {
const _DeleteAccountBottomSheet({required this.onDeleteAccount});
final VoidCallback onDeleteAccount;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: context.colors.backgroundPage,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 8),
SizedBox(
height: 56,
child: Stack(
alignment: Alignment.center,
children: [
Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 72),
child: Text(
AppLocalizations.of(context)!
.areYouSureYouWantToDeleteYourAccount,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
),
),
Positioned(
left: 16,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: Get.back,
child: SizedBox(
width: 44,
height: 44,
child: Center(
child: Image.asset(
'assets/images/common/ic_close.png',
width: 20,
height: 20,
color: context.colors.chartPurple,
),
),
),
),
),
],
),
),
Container(
margin: EdgeInsets.symmetric(horizontal: 16),
padding: EdgeInsets.fromLTRB(20, 20, 20, 18),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.deleteAccountWarningTitle,
textAlign: TextAlign.center,
style: TextStyle(
color: context.colors.warning,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
SizedBox(
height: 12,
),
Text(
context.l10n.deleteAccountWarningPrompt,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
SizedBox(
height: 12,
),
Text(
context.l10n.deleteAccountWarningNote1,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
SizedBox(
height: 12,
),
Text(
context.l10n.deleteAccountWarningNote2,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
)
],
),
),
SizedBox(
height: 45,
),
GestureDetector(
onTap: onDeleteAccount,
child: Text(
AppLocalizations.of(context)!.confirmDeletion,
style: TextStyle(
color: context.colors.warning,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
width: 280,
height: 48,
margin: EdgeInsets.only(top: 17, bottom: 20),
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
decoration: ShapeDecoration(
color: context.colors.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 24,
children: [
Text(
context.l10n.iLlThinkAboutItSomeMore,
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
)
],
),
),
);
}
}
... ...
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/my_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/my/security_email_views.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
... ... @@ -13,6 +14,7 @@ import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/user/user_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
... ... @@ -26,150 +28,96 @@ class AccountSettingView extends GetView<MyController> {
final userPrefs = Get.find<UserPreferencesStorage>();
return Scaffold(
backgroundColor: context.colors.backgroundPage,
appBar: AppBar(
title: Text(
context.l10n.accountSettings,
),
leadingWidth: 60,
leading: IconButton(
highlightColor: Colors.transparent,
padding: EdgeInsets.zero,
onPressed: Get.back,
icon: Image.asset(
'assets/images/common/ic_nav_back.webp',
width: 24,
height: 24,
width: 28,
height: 28,
),
),
),
body: Obx(() {
final telephone =
userPrefs.preferences.value.meUserInfo?.telephone ?? '';
final thirdAccountId =
userPrefs.preferences.value.meUserInfo?.thirdAccountId ?? '';
final user = userPrefs.preferences.value.meUserInfo;
final linkedAccount = _linkedAccount(context, user);
final telephone = user?.telephone?.trim() ?? '';
final securityEmail = user?.securityEmail?.trim() ?? '';
return Column(
children: [
SizedBox(height: 16),
if (thirdAccountId.isNotEmpty)
_buildAccountCard(
context: context,
title: 'Apple ID',
content: thirdAccountId,
)
else
_buildAccountCard(
context: context,
const SizedBox(height: 16),
if (linkedAccount != null) ...[
_AccountInfoRow(
title: linkedAccount.title,
content: linkedAccount.content,
),
const SizedBox(height: 12),
] else if (telephone.isNotEmpty) ...[
_AccountInfoRow(
title: context.l10n.mobilePhoneNumber,
content: telephone,
),
SizedBox(height: 20),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Get.bottomSheet(
_DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount),
barrierColor: Colors.black.withValues(alpha: 0.7),
enableDrag: true,
isScrollControlled: true,
persistent: false,
);
},
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: 24,
vertical: 8,
),
child: Text(
context.l10n.deleteAccount,
style: TextStyle(
color: AppColors.warning,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
const SizedBox(height: 12),
],
_SecurityEmailRow(
email: securityEmail,
onTap: () => Get.to(() => const AddSecurityEmailView()),
),
const Spacer(),
_AccountActionButton(
label: context.l10n.logOut,
color: context.colors.primary,
backgroundColor: Colors.white,
onTap: _logout,
),
const SizedBox(height: 12),
_AccountActionButton(
label: context.l10n.deleteAccount,
color: AppColors.warning,
onTap: _showDeleteAccountSheet,
),
SizedBox(height: MediaQuery.paddingOf(context).bottom + 40),
],
);
}),
);
}
Widget _buildAccountCard({
required BuildContext context,
required String title,
required String content,
}) {
return Container(
height: 110,
margin: EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
SizedBox(
height: 54,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Text(
title,
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
SizedBox(width: 16),
Expanded(
child: Text(
content,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
],
),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 21),
child: const Divider(
height: 1,
thickness: 0.5,
color: Color(0xFFF3F3F3),
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _logout,
child: Center(
child: Text(
context.l10n.logOut,
style: TextStyle(
color: AppColors.primary,
fontSize: 14,
fontWeight: FontWeight.w500,
height: 1.4,
),
),
),
),
),
],
),
_LinkedAccount? _linkedAccount(
BuildContext context,
UserInfoResponse? user,
) {
final accountId = user?.thirdAccountId?.trim() ?? '';
if (accountId.isEmpty) return null;
if (accountId.toLowerCase().endsWith('@privaterelay.appleid.com')) {
return _LinkedAccount(title: 'Apple ID', content: accountId);
}
final provider = user?.thirdAccountType?.toLowerCase();
return _LinkedAccount(
title: provider?.contains('google') == true
? context.l10n.googleAccount
: context.l10n.appleAccount,
content: accountId,
);
}
void _showDeleteAccountSheet() {
Get.bottomSheet(
_DeleteAccountBottomSheet(onDeleteAccount: _deleteAccount),
barrierColor: Colors.black.withValues(alpha: 0.7),
enableDrag: true,
isScrollControlled: true,
persistent: false,
);
}
... ... @@ -201,6 +149,164 @@ class AccountSettingView extends GetView<MyController> {
}
}
class _LinkedAccount {
const _LinkedAccount({required this.title, required this.content});
final String title;
final String content;
}
class _AccountInfoRow extends StatelessWidget {
const _AccountInfoRow({required this.title, required this.content});
final String title;
final String content;
@override
Widget build(BuildContext context) {
return Container(
height: 74,
margin: const EdgeInsets.symmetric(horizontal: 15),
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Text(
title,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
content,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
],
),
);
}
}
class _SecurityEmailRow extends StatelessWidget {
const _SecurityEmailRow({required this.email, required this.onTap});
final String email;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
label: context.l10n.securityEmail,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
height: 56,
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
Text(
context.l10n.securityEmail,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
email,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
),
),
),
const SizedBox(width: 8),
Image.asset(
'assets/images/common/ic_more.png',
width: 16,
height: 16,
),
],
),
),
),
);
}
}
class _AccountActionButton extends StatelessWidget {
const _AccountActionButton({
required this.label,
required this.color,
required this.onTap,
this.backgroundColor = Colors.transparent,
});
final String label;
final Color color;
final Color backgroundColor;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
width: 280,
height: 48,
child: Material(
color: backgroundColor,
borderRadius: BorderRadius.circular(24),
child: InkWell(
borderRadius: BorderRadius.circular(24),
onTap: onTap,
child: Center(
child: Text(
label,
style: TextStyle(
color: color,
fontSize: 16,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
),
),
),
);
}
}
class _DeleteAccountBottomSheet extends StatelessWidget {
const _DeleteAccountBottomSheet({required this.onDeleteAccount});
... ...
import 'dart:async';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
const _securityEmailPageColor = Color(0xFFF5F2FF);
const _securityEmailTitleColor = Color(0xFF0F0F11);
const _securityEmailDescriptionColor = Color(0xFF78787D);
const _securityEmailBrandColor = Color(0xFF845EEE);
class AddSecurityEmailView extends StatefulWidget {
const AddSecurityEmailView({super.key});
@override
State<AddSecurityEmailView> createState() => _AddSecurityEmailViewState();
}
class _AddSecurityEmailViewState extends State<AddSecurityEmailView> {
static final _emailPattern = RegExp(
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$',
);
final _emailController = TextEditingController();
bool _hasSubmitted = false;
bool get _isEmailValid =>
_emailPattern.hasMatch(_emailController.text.trim());
@override
void dispose() {
_emailController.dispose();
super.dispose();
}
void _sendVerificationEmail() {
setState(() => _hasSubmitted = true);
if (!_isEmailValid) return;
Get.to(
() => SecurityEmailVerificationView(
email: _emailController.text.trim(),
),
);
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final showError = _hasSubmitted && !_isEmailValid;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: _securityEmailPageColor,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: _securityEmailPageColor,
appBar: const _SecurityEmailAppBar(),
body: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
children: [
const SizedBox(height: 16),
Text(
l10n.addSecurityEmail,
style: const TextStyle(
color: _securityEmailTitleColor,
fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
const SizedBox(height: 8),
Text(
l10n.securityEmailDescription,
textAlign: TextAlign.center,
style: const TextStyle(
color: _securityEmailDescriptionColor,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
),
),
const SizedBox(height: 32),
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (_) => setState(() {}),
onSubmitted: (_) => _sendVerificationEmail(),
style: const TextStyle(
color: _securityEmailTitleColor,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.securityEmailHint,
hintStyle: const TextStyle(
color: Color(0xFFD9D9D9),
fontSize: 16,
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
errorText: showError ? l10n.invalidEmailFormat : null,
errorStyle: const TextStyle(fontSize: 12),
border: _inputBorder(),
enabledBorder: _inputBorder(),
focusedBorder: _inputBorder(
color: _securityEmailBrandColor,
),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isEmailValid ? _sendVerificationEmail : null,
style: ElevatedButton.styleFrom(
backgroundColor: _securityEmailBrandColor,
disabledBackgroundColor:
_securityEmailBrandColor.withValues(alpha: 0.4),
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white,
elevation: 0,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: Text(l10n.sendVerificationEmail),
),
),
],
),
),
),
),
);
}
}
class SecurityEmailVerificationView extends StatefulWidget {
const SecurityEmailVerificationView({
super.key,
required this.email,
});
final String email;
@override
State<SecurityEmailVerificationView> createState() =>
_SecurityEmailVerificationViewState();
}
class _SecurityEmailVerificationViewState
extends State<SecurityEmailVerificationView> {
static const _resendSeconds = 60;
final _codeController = TextEditingController();
Timer? _resendTimer;
int _secondsRemaining = _resendSeconds;
bool get _canSubmit => _codeController.text.trim().isNotEmpty;
@override
void initState() {
super.initState();
_startResendCountdown();
}
@override
void dispose() {
_resendTimer?.cancel();
_codeController.dispose();
super.dispose();
}
void _startResendCountdown() {
_resendTimer?.cancel();
setState(() => _secondsRemaining = _resendSeconds);
_resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_secondsRemaining <= 1) {
timer.cancel();
setState(() => _secondsRemaining = 0);
return;
}
setState(() => _secondsRemaining -= 1);
});
}
void _submitCode() {
if (!_canSubmit) return;
FocusScope.of(context).unfocus();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: _securityEmailPageColor,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: _securityEmailPageColor,
appBar: const _SecurityEmailAppBar(),
body: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
children: [
const SizedBox(height: 16),
Text(
l10n.confirmYourEmail,
style: const TextStyle(
color: _securityEmailTitleColor,
fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
const SizedBox(height: 7),
Text.rich(
_verificationInstruction(
l10n.enterCodeSentTo(widget.email),
widget.email,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 47),
Container(
height: 52,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(27),
),
child: Stack(
children: [
TextField(
controller: _codeController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
maxLength: 6,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
onChanged: (_) => setState(() {}),
onSubmitted: (_) => _submitCode(),
style: const TextStyle(
color: _securityEmailTitleColor,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.verificationCode,
hintStyle: const TextStyle(
color: Color(0xFFD9D9D9),
fontSize: 16,
),
counterText: '',
contentPadding: const EdgeInsets.only(
left: 20,
right: 112,
),
border: InputBorder.none,
),
),
Positioned(
top: 0,
right: 0,
bottom: 0,
child: TextButton(
onPressed: _secondsRemaining == 0
? _startResendCountdown
: null,
style: TextButton.styleFrom(
foregroundColor: _securityEmailBrandColor,
disabledForegroundColor: const Color(0xFFD9D9D9),
padding: const EdgeInsets.symmetric(horizontal: 20),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
child: Text(
_secondsRemaining == 0
? l10n.resend
: l10n.resendWithSeconds(_secondsRemaining),
),
),
),
],
),
),
const SizedBox(height: 8),
Text(
l10n.emailVerificationHelp,
textAlign: TextAlign.center,
style: const TextStyle(
color: _securityEmailDescriptionColor,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.2,
),
),
const SizedBox(height: 64),
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _canSubmit ? _submitCode : null,
style: ElevatedButton.styleFrom(
backgroundColor: _securityEmailBrandColor,
disabledBackgroundColor:
_securityEmailBrandColor.withValues(alpha: 0.4),
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white,
elevation: 0,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: Text(l10n.submit),
),
),
],
),
),
),
),
);
}
}
TextSpan _verificationInstruction(String instruction, String email) {
const textStyle = TextStyle(
color: _securityEmailDescriptionColor,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
);
final emailStart = instruction.indexOf(email);
if (emailStart < 0) return TextSpan(text: instruction, style: textStyle);
final emailEnd = emailStart + email.length;
return TextSpan(
style: textStyle,
children: [
TextSpan(text: instruction.substring(0, emailStart)),
TextSpan(
text: instruction.substring(emailStart, emailEnd),
style: const TextStyle(color: _securityEmailTitleColor),
),
TextSpan(text: instruction.substring(emailEnd)),
],
);
}
class _SecurityEmailAppBar extends StatelessWidget
implements PreferredSizeWidget {
const _SecurityEmailAppBar();
@override
Size get preferredSize => const Size.fromHeight(44);
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: _securityEmailPageColor,
elevation: 0,
scrolledUnderElevation: 0,
toolbarHeight: 44,
leadingWidth: 60,
leading: IconButton(
onPressed: Get.back,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
icon: Image.asset(
'assets/images/common/ic_nav_back.webp',
width: 28,
height: 28,
),
),
);
}
}
OutlineInputBorder _inputBorder({Color color = Colors.transparent}) {
return OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(color: color),
);
}
... ...
import 'dart:async';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
... ... @@ -150,7 +151,9 @@ class TodayHrvChartCard extends StatelessWidget {
Builder(builder: (context) {
final s = (sameDay
? context.l10n.todaySHrvTrend
: context.l10n.hrvTrendForThatDay);
: controller.environmentConfig.region.value != AppRegion.china
? context.l10n.todaySHrvTrend
: context.l10n.hrvTrendForThatDay);
return Text(
(controller.isFriend
? context.l10n.reportOtherPossessiveTitle(s)
... ...
... ... @@ -189,6 +189,11 @@ class LoginController extends GetxController {
Get.toNamed(AppRoutes.debugEnvironment);
}
Future<void> selectServiceRegion(AppRegion region) async {
if (environmentConfig.region.value == region) return;
await environmentConfig.setRegion(region);
}
void openUserTerms() {
Get.toNamed(
AppRoutes.webview,
... ... @@ -420,6 +425,7 @@ class LoginController extends GetxController {
if (loginRes is AppSuccess<LoginResponse>) {
await _handleLoginSuccess(loginRes.data, appleModel: appleModel);
await Get.find<LocalStorage>().setLastLoginMethod('apple');
await environmentConfig.updateHideChooseRegion(true);
}
});
} finally {
... ...
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
void showEmailLoginContactUsBottomSheet({
required String userId,
VoidCallback? onSendEmail,
String pageName = 'Setting page',
}) {
Get.bottomSheet(
EmailLoginContactUsBottomSheet(
userId: userId,
onSendEmail: onSendEmail,
pageName: pageName,
),
barrierColor: Colors.black.withValues(alpha: 0.7),
enableDrag: true,
... ... @@ -23,11 +24,11 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget {
const EmailLoginContactUsBottomSheet({
super.key,
required this.userId,
this.onSendEmail,
this.pageName = 'Setting page',
});
final String userId;
final VoidCallback? onSendEmail;
final String pageName;
@override
Widget build(BuildContext context) {
... ... @@ -51,22 +52,22 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget {
),
),
const SizedBox(height: 17),
const Text(
'Contact us',
style: TextStyle(
Text(
context.l10n.contactUs,
style: const TextStyle(
color: Color(0xFF141414),
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 22),
const Padding(
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Please describe the problem clearly and include screen '
'recordings if possible.',
context.l10n
.pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible,
style: TextStyle(
color: Color(0xFF141414),
fontSize: 15,
... ... @@ -76,13 +77,13 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget {
),
),
const SizedBox(height: 8),
const Padding(
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'Send us your User ID as it will help us identify\n'
'the problem faster.',
context.l10n
.sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster,
style: TextStyle(
color: Color(0xFF141414),
fontSize: 15,
... ... @@ -98,7 +99,10 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget {
_SendEmailButton(
onTap: () {
Get.back();
onSendEmail?.call();
sendFeedbackEmail(
userId: userId,
pageName: pageName,
);
},
),
const Spacer(),
... ... @@ -109,6 +113,38 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget {
}
}
Future<void> sendFeedbackEmail({
required String userId,
String pageName = 'Setting page',
}) async {
final String cleanUserId = userId.trim().isEmpty ? 'N/A' : userId.trim();
final String body =
'This issue was reported from $pageName by User ID: $cleanUserId.';
final Uri emailUri = Uri(
scheme: 'mailto',
path: 'support@doublefeel.cn',
query: _encodeQueryParameters({
'subject': 'DoubleFeel Feedback',
'body': body,
}),
);
// try {
// await launchUrl(
// emailUri,
// mode: LaunchMode.externalApplication,
// );
// } catch (_) {}
}
String _encodeQueryParameters(Map<String, String> params) {
return params.entries
.map((e) =>
'${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
.join('&');
}
class _UserIdField extends StatelessWidget {
const _UserIdField({required this.userId});
... ... @@ -155,7 +191,7 @@ class _UserIdField extends StatelessWidget {
height: 48,
child: Center(
child: Image.asset(
'assets/images/common/ic_contact_us_copy.png',
'assets/images/common/ic_contact_us_copy_2.png',
width: 24,
height: 24,
),
... ... @@ -191,7 +227,7 @@ class _SendEmailButton extends StatelessWidget {
children: [
Image(
image: AssetImage(
'assets/images/common/ic_contact_us_lightning.png',
'assets/images/common/ic_contact_us.png',
),
width: 24,
height: 24,
... ...
... ... @@ -353,8 +353,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
child: GestureDetector(
onTap: () => showEmailLoginContactUsBottomSheet(
userId: controller.emailInput.value.trim(),
onSendEmail: () =>
Get.toNamed(Routes.SUBMIT_FEEDBACK),
pageName: 'Reset password page',
),
child: Text(
'Having trouble? Contact us',
... ...
... ... @@ -41,124 +41,172 @@ class LoginView extends GetView<LoginController> {
final rightOffset = (width - buttonWidth) / 2;
double bottom(double value) => bottomInset + value;
final l10n = context.l10n;
return Stack(
fit: StackFit.expand,
children: [
Column(
mainAxisSize: MainAxisSize.max,
children: [
Image.asset(
'assets/images/user_onboarding/bg_login.png',
fit: BoxFit.fitWidth,
),
Expanded(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.50, -0.00),
end: Alignment(0.50, 1.00),
colors: [
const Color(0xFFEEE6FF),
const Color(0xFFF8F6FF)
]),
),
)),
],
),
// Welcome texts
Positioned(
bottom: bottom(controller.environmentConfig.region.value ==
AppRegion.china
? 253
: 273),
left: 0,
right: 0,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
return Obx(() {
final region = controller.environmentConfig.region.value;
return Stack(
fit: StackFit.expand,
children: [
Column(
mainAxisSize: MainAxisSize.max,
children: [
Image.asset(
'assets/images/common/ic_double_feel_text.png',
width: 160,
height: 42,
'assets/images/user_onboarding/bg_login.png',
fit: BoxFit.fitWidth,
),
SizedBox(height: 8),
Padding(
padding: EdgeInsets.symmetric(
horizontal:
controller.environmentConfig.region.value ==
AppRegion.china
? 0
: 40),
child: Text(
l10n.loginSlogan,
textAlign: TextAlign.center,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.50,
letterSpacing:
controller.environmentConfig.region.value ==
AppRegion.china
? 6
: 1,
),
Expanded(
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment(0.50, -0.00),
end: Alignment(0.50, 1.00),
colors: [
const Color(0xFFEEE6FF),
const Color(0xFFF8F6FF)
]),
),
),
)),
],
),
),
// Login buttons – add new methods here as Column children
Positioned(
bottom: bottom(85),
left: 0,
right: 0,
child: Center(
// Welcome texts
if (!controller.environmentConfig.hideChooseRegion)
Positioned(
right: 20,
top: MediaQuery.paddingOf(context).top,
child: GestureDetector(
onTap: () => Get.bottomSheet(
const _ServiceRegionBottomSheet(),
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withValues(alpha: 0.5),
enableDrag: true,
isScrollControlled: true,
),
child: Text(
'🌏',
textAlign: TextAlign.right,
style: TextStyle(
color: Colors.black,
fontSize: 23,
fontWeight: FontWeight.w400,
),
),
)),
Positioned(
bottom: bottom(region == AppRegion.china ? 253 : 273),
left: 0,
right: 0,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (controller.environmentConfig.region.value ==
AppRegion.china) ...[
_LoginMethodButton(
width: buttonWidth,
label: l10n.loginWithPhone,
onPressed: controller.onPhoneLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'phone',
lastUsedLabel: l10n.loginLastUsed,
),
const SizedBox(height: 12)
],
if (controller.environmentConfig.region.value ==
AppRegion.global) ...[
_LoginMethodButton(
width: buttonWidth,
label: l10n.continueWithEmail,
onPressed: controller.onEmailLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'email',
lastUsedLabel: l10n.loginLastUsed,
Image.asset(
'assets/images/common/ic_double_feel_text.png',
width: 160,
height: 42,
),
SizedBox(height: 8),
Padding(
padding: EdgeInsets.symmetric(
horizontal: region == AppRegion.china ? 0 : 40),
child: Text(
l10n.loginSlogan,
textAlign: TextAlign.center,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.50,
letterSpacing: region == AppRegion.china ? 6 : 1,
),
),
const SizedBox(height: 12)
],
if (controller.environmentConfig.region.value ==
AppRegion.global) ...[
),
],
),
),
// Login buttons – add new methods here as Column children
Positioned(
bottom: bottom(85),
left: 0,
right: 0,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (region == AppRegion.china) ...[
//GetPlatform.isOhos
_LoginMethodButton(
width: buttonWidth,
label: l10n.loginWithPhone,
onPressed: controller.onPhoneLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'phone',
lastUsedLabel: l10n.loginLastUsed,
),
const SizedBox(height: 12)
],
if (region == AppRegion.global) ...[
_LoginMethodButton(
width: buttonWidth,
label: l10n.continueWithEmail,
onPressed: controller.onEmailLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'email',
lastUsedLabel: l10n.loginLastUsed,
),
const SizedBox(height: 12)
],
if (region == AppRegion.global) ...[
_LoginMethodButton(
width: buttonWidth,
label: l10n.continueWithGoogle,
onPressed: controller.onGoogleLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'google',
lastUsedLabel: l10n.loginLastUsed,
icon: Image.asset(
'assets/images/common/ic_google.png',
width: 20,
height: 20,
),
buttonStyle: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: context.colors.textPrimary,
disabledBackgroundColor: context
.colors.textPrimary
.withValues(alpha: 0.5),
disabledForegroundColor:
context.colors.textPrimary,
elevation: 0,
shadowColor: Colors.transparent,
padding:
const EdgeInsets.symmetric(horizontal: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(height: 12)
],
_LoginMethodButton(
width: buttonWidth,
label: l10n.continueWithGoogle,
onPressed: controller.onGoogleLoginPressed,
label: l10n.loginWithApple,
onPressed: controller.onAppleLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'google',
'apple',
lastUsedLabel: l10n.loginLastUsed,
icon: Image.asset(
'assets/images/common/ic_google.png',
width: 20,
height: 20,
'assets/images/common/ic_apple.png',
width: 24,
height: 24,
color: context.colors.textPrimary,
),
buttonStyle: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
... ... @@ -181,79 +229,292 @@ class LoginView extends GetView<LoginController> {
),
),
),
const SizedBox(height: 12)
],
_LoginMethodButton(
width: buttonWidth,
label: l10n.loginWithApple,
onPressed: controller.onAppleLoginPressed,
isLastUsed:
Get.find<LocalStorage>().lastLoginMethod ==
'apple',
lastUsedLabel: l10n.loginLastUsed,
icon: Image.asset(
'assets/images/common/ic_apple.png',
width: 24,
height: 24,
color: context.colors.textPrimary,
),
buttonStyle: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: context.colors.textPrimary,
disabledBackgroundColor: context.colors.textPrimary
.withValues(alpha: 0.5),
disabledForegroundColor: context.colors.textPrimary,
elevation: 0,
shadowColor: Colors.transparent,
padding: const EdgeInsets.symmetric(horizontal: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
),
),
// Terms agreement text + checkbox
Positioned(
bottom: bottom(0),
left: 0,
right: 0,
child: const _AgreementText(),
),
if (controller.environmentConfig.isDebug)
Positioned(
left: 0,
right: 0,
bottom: bottom(0),
child: Center(
child: TextButton(
onPressed: controller.onDebugPressed,
style: TextButton.styleFrom(
foregroundColor: context.colors.textSecondary
.withValues(alpha: 0.55),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
child: Builder(builder: (context) {
final environmentConfig =
Get.find<AppEnvironmentConfig>();
return Text(
environmentConfig.environment.value.name);
}),
),
],
),
),
],
);
});
},
),
),
);
}
}
class _ServiceRegionBottomSheet extends StatefulWidget {
const _ServiceRegionBottomSheet();
@override
State<_ServiceRegionBottomSheet> createState() =>
_ServiceRegionBottomSheetState();
}
class _ServiceRegionBottomSheetState extends State<_ServiceRegionBottomSheet> {
static const _brandColor = Color(0xFF845EEE);
static const _backgroundColor = Color(0xFFF5F2FF);
final _controller = Get.find<LoginController>();
late AppRegion _selectedRegion;
var _isSaving = false;
@override
void initState() {
super.initState();
_selectedRegion = _controller.environmentConfig.region.value;
}
Future<void> _save() async {
if (_isSaving) return;
setState(() => _isSaving = true);
await _controller.selectServiceRegion(_selectedRegion);
if (mounted) Get.back();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final isChinaSelected = _selectedRegion == AppRegion.china;
return SizedBox(
height: 446,
child: DecoratedBox(
decoration: const BoxDecoration(
color: _backgroundColor,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: Padding(
padding: EdgeInsets.only(
top: 8,
bottom: MediaQuery.paddingOf(context).bottom,
),
child: Column(
children: [
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _isSaving ? null : Get.back,
child: SizedBox(
width: 84,
height: 56,
child: Center(
child: Image.asset(
'assets/images/common/ic_close.png',
width: 16,
height: 16,
color: _brandColor,
),
),
),
),
// Terms agreement text + checkbox
Positioned(
bottom: bottom(0),
left: 0,
right: 0,
child: const _AgreementText(),
),
const SizedBox(height: 6),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
_ServiceRegionOption(
title: l10n.mainlandChinaServices,
description: l10n.mainlandChinaServicesDescription,
cannotChangeLabel: l10n.serviceRegionCannotBeChanged,
isSelected: isChinaSelected,
minHeight: isChinaSelected ? 142 : 56,
onTap: _isSaving
? null
: () => setState(
() => _selectedRegion = AppRegion.china,
),
),
const SizedBox(height: 12),
_ServiceRegionOption(
title: l10n.internationalServices,
description: l10n.internationalServicesDescription,
cannotChangeLabel: l10n.serviceRegionCannotBeChanged,
isSelected: !isChinaSelected,
minHeight: isChinaSelected ? 56 : 182,
onTap: _isSaving
? null
: () => setState(
() => _selectedRegion = AppRegion.global,
),
),
],
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _isSaving ? null : _save,
style: ElevatedButton.styleFrom(
backgroundColor: _brandColor,
disabledBackgroundColor: _brandColor.withValues(
alpha: 0.4,
),
foregroundColor: Colors.white,
disabledForegroundColor: Colors.white,
elevation: 0,
shape: const StadiumBorder(),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: Text(l10n.friendsSave),
),
),
),
],
),
),
),
);
}
}
if (controller.environmentConfig.isDebug)
Positioned(
left: 0,
right: 0,
bottom: bottom(0),
child: Center(
child: TextButton(
onPressed: controller.onDebugPressed,
style: TextButton.styleFrom(
foregroundColor: context.colors.textSecondary
.withValues(alpha: 0.55),
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
class _ServiceRegionOption extends StatelessWidget {
const _ServiceRegionOption({
required this.title,
required this.description,
required this.cannotChangeLabel,
required this.isSelected,
required this.minHeight,
required this.onTap,
});
final String title;
final String description;
final String cannotChangeLabel;
final bool isSelected;
final double minHeight;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
selected: isSelected,
label: title,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
constraints: BoxConstraints(minHeight: minHeight),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: isSelected
? Border.all(
color: Color(0xFF845EEE),
)
: null,
),
child: isSelected
? Stack(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.25,
),
),
),
child: Builder(builder: (context) {
final environmentConfig =
Get.find<AppEnvironmentConfig>();
return Text(environmentConfig.environment.value.name);
}),
const SizedBox(height: 14),
Text(
description,
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
),
),
const SizedBox(height: 12),
Text(
cannotChangeLabel,
style: const TextStyle(
color: Color(0xFF78787D),
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
),
),
],
),
),
const Positioned(
top: 20,
right: 20,
child: Icon(
Icons.check_circle_rounded,
color: Color(0xFF845EEE),
size: 20,
),
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
title,
style: const TextStyle(
color: Color(0xFF0F0F11),
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.25,
),
),
),
],
);
},
),
),
),
);
... ...
... ... @@ -71,6 +71,12 @@ class AppEnvironmentConfig {
Get.find<DioClient>().refreshBaseUrl();
}
get hideChooseRegion => _storage.hideChooseRegion;
Future<void> updateHideChooseRegion(bool value) async {
await _storage.writeHideChooseRegion(value);
}
String get serverBaseUrl {
final regionUrl = switch (region.value) {
AppRegion.china => AppConst.serverBaseUrl,
... ...
... ... @@ -4,6 +4,7 @@ abstract final class StorageConst {
'double_feel_environment_config';
static const String appEnvironmentKey = 'environment';
static const String appRegionKey = 'region';
static const String hideChooseRegionKey = 'hide_choose_region';
static const String appSettingsPrefsName = 'app_settings';
static const String termsAgreedKey = 'terms_agreed';
static const String lastLoginMethodKey = 'last_login_method';
... ...
import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:get/get.dart';
import '../constants/network_const.dart';
... ... @@ -52,6 +55,13 @@ class AppErrorHandler {
Get.find<UserStateService>().onLogout();
}
Get.offAllNamed(AppRoutes.login);
DialogUtils.showCommonDialog(DialogMetaData(
title: l10n.logOut,
message: l10n
.yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue,
confirmText: l10n.watchThemeOk,
iconAsset: 'assets/images/common/ic_warning.png',
));
}
void _showMessage(String? message) {
... ...
... ... @@ -44,6 +44,13 @@ class LocalStorage {
await sharedPreferences.setInt(StorageConst.appRegionKey, region.index);
}
bool get hideChooseRegion =>
sharedPreferences.getBool(StorageConst.hideChooseRegionKey) ?? false;
Future<void> writeHideChooseRegion(bool value) async {
await sharedPreferences.setBool(StorageConst.hideChooseRegionKey, value);
}
// ─── App Settings Storage ──────────────────────────────────────────────────
bool get termsAgreed =>
... ...
... ... @@ -206,7 +206,9 @@ class UserInfoResponse {
this.uniqueCode,
this.enableAddWithUCode,
this.isPassNoviceGuide,
this.thirdAccountId});
this.thirdAccountId,
this.thirdAccountType,
this.securityEmail});
final int? id;
final String? pairCode;
... ... @@ -223,6 +225,8 @@ class UserInfoResponse {
final int? enableAddWithUCode;
final int? isPassNoviceGuide;
final String? thirdAccountId;
final String? thirdAccountType;
final String? securityEmail;
factory UserInfoResponse.fromJson(Map<String, dynamic> json) {
return UserInfoResponse(
... ... @@ -241,6 +245,14 @@ class UserInfoResponse {
enableAddWithUCode: json['enable_add_with_ucode'] as int?,
isPassNoviceGuide: json['is_pass_novice_guide'] as int?,
thirdAccountId: json['third_account_id'] as String?,
thirdAccountType: _asString(
json['third_account_type'] ??
json['third_account_provider'] ??
json['login_type'],
),
securityEmail: _asString(
json['security_email'] ?? json['safe_email'] ?? json['email'],
),
);
}
... ... @@ -265,10 +277,19 @@ class UserInfoResponse {
val['is_pass_novice_guide'] = isPassNoviceGuide;
}
if (thirdAccountId != null) val['third_account_id'] = thirdAccountId;
if (thirdAccountType != null) {
val['third_account_type'] = thirdAccountType;
}
if (securityEmail != null) val['security_email'] = securityEmail;
return val;
}
}
String? _asString(Object? value) {
final normalized = value?.toString().trim();
return normalized == null || normalized.isEmpty ? null : normalized;
}
class BoundUserInfoResponse {
const BoundUserInfoResponse({this.userInfo, this.partnerUserInfo});
... ...
... ... @@ -19,6 +19,11 @@
"tabFriends": "Friends",
"tabMy": "Me",
"switchLanguage": "Switch Language",
"mainlandChinaServices": "Mainland China Services",
"internationalServices": "International Services",
"mainlandChinaServicesDescription": "For users mainly in Mainland China. Health, account, and friend data is stored in Mainland China.",
"internationalServicesDescription": "For users mainly outside Mainland China. Health, account, and friend data is stored internationally.",
"serviceRegionCannotBeChanged": "Service region cannot be changed after account creation.",
"settings": "Settings",
"onboardingIntroTitle": "DoubleFeel is a health companion app built for Apple Watch",
"onboardingIntroBody": "<em>Understand yourself better</em>, and let people who care about you <em>notice when you need support.</em>",
... ... @@ -228,9 +233,9 @@
"redeemOffer": "Claim Offer",
"allPlans": "All Plans",
"clickToAddTheHrvThemedWatchFace": "Add HRV Watch Face",
"stayOnTopOfYourHealthFluctuations": "Stay on top of your health",
"addACloseContact": "Add a Close Contact",
"oneMorePersonLookingOutForYourHealth": "Have someone care about your health",
"stayOnTopOfYourHealthFluctuations": "Track your body changes",
"addACloseContact": "Add a Loved One",
"oneMorePersonLookingOutForYourHealth": "Follow your health",
"addAFriend": "Add",
"averageHrvForTheDay": "Avg. Hrv",
"restingHeartRate": "RHR",
... ... @@ -255,6 +260,32 @@
"frequentlyAskedQuestions": "Frequently Asked Questions",
"areYouSureYouWantToDeleteYourAccount": "Are you sure you want to delete your account?",
"accountSettings": "Account Settings",
"appleAccount": "Apple Account",
"googleAccount": "Google Account",
"securityEmail": "Email",
"addSecurityEmail": "Add security email",
"securityEmailDescription": "Use a security email to help protect your account.",
"securityEmailHint": "Email address",
"sendVerificationEmail": "Send verification email",
"confirmYourEmail": "Confirm your email",
"enterCodeSentTo": "Enter the code sent to {email}",
"@enterCodeSentTo": {
"placeholders": {
"email": {}
}
},
"emailVerificationHelp": "If you don't see the email, check other places it might be, like your junk, spam, social, or other folders",
"verificationCode": "Verification code",
"resend": "Re-send",
"resendWithSeconds": "Re-send({seconds})",
"@resendWithSeconds": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"invalidEmailFormat": "Invalid email format",
"deleteAccount": "Delete Account",
"mobilePhoneNumber": "Mobile phone number",
"logOut": "Log Out",
... ... @@ -312,7 +343,7 @@
"reportWeekdaySaturday": "Sat",
"reportWeekdaySunday": "Sun",
"reportDateWithWeekday": "{date}, {weekday}",
"reportOtherPossessiveTitle": "{title}",
"reportOtherPossessiveTitle": "Their {title}",
"reportOtherTitle": "{title}",
"reportTrend": "Trends",
"reportExampleTitle": "{title} (Example)",
... ... @@ -498,7 +529,7 @@
"purchaseNoteRestore": "If your purchase does not take effect, tap Restore Purchases.",
"purchaseNoteContact": "If you have any other questions, ",
"purchaseLinkContactUs": "Contact Us",
"reportBottomSlogan": "FEEL MORE, STRESS LESS",
"reportBottomSlogan": "·Double the Awareness, Half the Stress·",
"refundExplanationTitle": "Refund Information",
"refundAppStoreReviewTitle": "Refunds are reviewed by the App Store",
"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 @@
"appReviewFeedbackSendAction": "Send Feedback",
"appReviewFeedbackLaterAction": "Maybe Later",
"appReviewIllustrationPlaceholder": "Illustration Placeholder",
"overallStressLevelToday": "Overall Stress Level Today",
"stressLevelsOnThatDay": "Stress Levels on That Day",
"overallStressLevelToday": "here's your overall stress status",
"stressLevelsOnThatDay": "Their Stress Levels on That Day",
"stressLevelsToday": "Their Stress Status Today",
"noPressureDataAvailableAtThisTime": "No pressure data available at this time",
"membersCanViewTheCompleteData": "Unlock Pro to view",
"unlockNow": "Unlock",
"pressureOverload": "Pressure Overload",
"beMindfulOfStress": "Be Aware of Stress",
"statusNormal": "Status: Normal",
"inExcellentCondition": "In excellent condition",
"pressureOverload": "Overload",
"beMindfulOfStress": "Stressful",
"statusNormal": "Normal",
"inExcellentCondition": "Excellent",
"waitingForData": "Waiting for data",
"pressure": "Pressure",
"mostRecent": "Latest Reading",
"theDayBeforeYesterday": "The day before yesterday",
"uploadPhotos": "Upload Photos",
"filming": "Filming",
"unlockTheProVersion": "Unlock the Pro Version",
"embarkOnAJourneyOfStressAwarenessAndWellnessSupport": "Embark on a Journey of Stress Awareness and Wellness Support",
"unlockTheProVersion": "Unlock Pro",
"embarkOnAJourneyOfStressAwarenessAndWellnessSupport": "Begin Your Stress Alerts & Health Journey",
"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",
"@sharePartnerCodeTemplate": {
"description": "Share partner code template",
... ... @@ -582,10 +614,10 @@
}
}
},
"annualMemberDiscounts": "Special Offer",
"annualMemberDiscounts": "Annual Membership Offer",
"specialOffers": "OFF",
"currentPrice": "Now",
"originalPrice": "Was {price}",
"originalPrice": "{price}/y",
"@originalPrice": {
"description": "Original price label",
"placeholders": {
... ... @@ -788,5 +820,9 @@
"continueWithEmail": "Continue with email",
"continueWithGoogle": "Continue with Google",
"emailLoginEmailHint": "Your email",
"emailLoginYourPassword": "Your password"
}
"emailLoginYourPassword": "Your password",
"yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue": "Your account was signed out due to another device login or token expiration. Please log in again to continue.",
"contactUs": "Contact us",
"pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible": "Please describe the problem clearly and include screen recordings if possible.",
"sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "Send us your User ID as it will help us identify the problem faster."
}
\ No newline at end of file
... ...
... ... @@ -22,6 +22,11 @@
"tabFriends": "好友",
"tabMy": "我的",
"switchLanguage": "切换语言",
"mainlandChinaServices": "中国大陆服务",
"internationalServices": "国际服务",
"mainlandChinaServicesDescription": "适用于主要使用中国大陆服务的用户,健康、账号、好友数据将存储于中国大陆。",
"internationalServicesDescription": "适用于主要使用中国大陆以外服务的用户,健康、账号、好友数据将存储于国际。",
"serviceRegionCannotBeChanged": "账号创建后,服务区域不能直接更改。",
"settings": "设置",
"onboardingIntroTitle": "DoubleFeel 是专为 Apple Watch 打造的健康陪伴app",
"onboardingIntroBody": "我们希望可以帮助你\n<em>关注自己的身心变化,也让爱你的人</em>及时发现你的<em>疲惫与需要</em>",
... ... @@ -338,6 +343,32 @@
"frequentlyAskedQuestions": "常见问题",
"areYouSureYouWantToDeleteYourAccount": "确认注销账号吗?",
"accountSettings": "账号设置",
"appleAccount": "Apple Account",
"googleAccount": "Google Account",
"securityEmail": "安全邮箱",
"addSecurityEmail": "添加安全邮箱",
"securityEmailDescription": "添加安全邮箱,帮助保护你的账号。",
"securityEmailHint": "请输入邮箱地址",
"sendVerificationEmail": "发送验证邮件",
"confirmYourEmail": "验证你的邮箱",
"enterCodeSentTo": "请输入发送至 {email} 的验证码",
"@enterCodeSentTo": {
"placeholders": {
"email": {}
}
},
"emailVerificationHelp": "如果没有收到邮件,请检查垃圾邮件、推广邮件或其他文件夹。",
"verificationCode": "验证码",
"resend": "重新发送",
"resendWithSeconds": "重新发送({seconds})",
"@resendWithSeconds": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"invalidEmailFormat": "邮箱格式错误",
"deleteAccount": "注销账号",
"mobilePhoneNumber": "手机号",
"logOut": "退出登录",
... ... @@ -935,6 +966,7 @@
"appReviewIllustrationPlaceholder": "插图占位",
"overallStressLevelToday": "今日的综合压力状态",
"stressLevelsOnThatDay": "该日压力状态",
"stressLevelsToday": "今日压力状态",
"noPressureDataAvailableAtThisTime": "暂无压力数据",
"membersCanViewTheCompleteData": "会员可查看完整数据",
"unlockNow": "立即解锁",
... ... @@ -1181,5 +1213,9 @@
"continueWithEmail": "使用电子邮件",
"continueWithGoogle": "使用 Google",
"emailLoginEmailHint": "请输入邮箱",
"emailLoginYourPassword": "请输入密码"
}
"emailLoginYourPassword": "请输入密码",
"yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue": "由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。",
"contactUs": "联系我们",
"pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible": "请清楚地描述问题,并尽可能附上屏幕录像。",
"sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。"
}
\ No newline at end of file
... ...
... ... @@ -212,6 +212,36 @@ abstract class AppLocalizations {
/// **'切换语言'**
String get switchLanguage;
/// No description provided for @mainlandChinaServices.
///
/// In zh, this message translates to:
/// **'中国大陆服务'**
String get mainlandChinaServices;
/// No description provided for @internationalServices.
///
/// In zh, this message translates to:
/// **'国际服务'**
String get internationalServices;
/// No description provided for @mainlandChinaServicesDescription.
///
/// In zh, this message translates to:
/// **'适用于主要使用中国大陆服务的用户,健康、账号、好友数据将存储于中国大陆。'**
String get mainlandChinaServicesDescription;
/// No description provided for @internationalServicesDescription.
///
/// In zh, this message translates to:
/// **'适用于主要使用中国大陆以外服务的用户,健康、账号、好友数据将存储于国际。'**
String get internationalServicesDescription;
/// No description provided for @serviceRegionCannotBeChanged.
///
/// In zh, this message translates to:
/// **'账号创建后,服务区域不能直接更改。'**
String get serviceRegionCannotBeChanged;
/// No description provided for @settings.
///
/// In zh, this message translates to:
... ... @@ -1629,6 +1659,90 @@ abstract class AppLocalizations {
/// **'账号设置'**
String get accountSettings;
/// No description provided for @appleAccount.
///
/// In zh, this message translates to:
/// **'Apple Account'**
String get appleAccount;
/// No description provided for @googleAccount.
///
/// In zh, this message translates to:
/// **'Google Account'**
String get googleAccount;
/// No description provided for @securityEmail.
///
/// In zh, this message translates to:
/// **'安全邮箱'**
String get securityEmail;
/// No description provided for @addSecurityEmail.
///
/// In zh, this message translates to:
/// **'添加安全邮箱'**
String get addSecurityEmail;
/// No description provided for @securityEmailDescription.
///
/// In zh, this message translates to:
/// **'添加安全邮箱,帮助保护你的账号。'**
String get securityEmailDescription;
/// No description provided for @securityEmailHint.
///
/// In zh, this message translates to:
/// **'请输入邮箱地址'**
String get securityEmailHint;
/// No description provided for @sendVerificationEmail.
///
/// In zh, this message translates to:
/// **'发送验证邮件'**
String get sendVerificationEmail;
/// No description provided for @confirmYourEmail.
///
/// In zh, this message translates to:
/// **'验证你的邮箱'**
String get confirmYourEmail;
/// No description provided for @enterCodeSentTo.
///
/// In zh, this message translates to:
/// **'请输入发送至 {email} 的验证码'**
String enterCodeSentTo(Object email);
/// No description provided for @emailVerificationHelp.
///
/// In zh, this message translates to:
/// **'如果没有收到邮件,请检查垃圾邮件、推广邮件或其他文件夹。'**
String get emailVerificationHelp;
/// No description provided for @verificationCode.
///
/// In zh, this message translates to:
/// **'验证码'**
String get verificationCode;
/// No description provided for @resend.
///
/// In zh, this message translates to:
/// **'重新发送'**
String get resend;
/// No description provided for @resendWithSeconds.
///
/// In zh, this message translates to:
/// **'重新发送({seconds})'**
String resendWithSeconds(int seconds);
/// No description provided for @invalidEmailFormat.
///
/// In zh, this message translates to:
/// **'邮箱格式错误'**
String get invalidEmailFormat;
/// No description provided for @deleteAccount.
///
/// In zh, this message translates to:
... ... @@ -3351,6 +3465,12 @@ abstract class AppLocalizations {
/// **'该日压力状态'**
String get stressLevelsOnThatDay;
/// No description provided for @stressLevelsToday.
///
/// In zh, this message translates to:
/// **'今日压力状态'**
String get stressLevelsToday;
/// No description provided for @noPressureDataAvailableAtThisTime.
///
/// In zh, this message translates to:
... ... @@ -4203,6 +4323,32 @@ abstract class AppLocalizations {
/// In zh, this message translates to:
/// **'请输入密码'**
String get emailLoginYourPassword;
/// No description provided for @yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue.
///
/// In zh, this message translates to:
/// **'由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。'**
String
get yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue;
/// No description provided for @contactUs.
///
/// In zh, this message translates to:
/// **'联系我们'**
String get contactUs;
/// No description provided for @pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible.
///
/// In zh, this message translates to:
/// **'请清楚地描述问题,并尽可能附上屏幕录像。'**
String
get pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible;
/// No description provided for @sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster.
///
/// In zh, this message translates to:
/// **'请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。'**
String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster;
}
class _AppLocalizationsDelegate
... ...
... ... @@ -66,6 +66,24 @@ class AppLocalizationsEn extends AppLocalizations {
String get switchLanguage => 'Switch Language';
@override
String get mainlandChinaServices => 'Mainland China Services';
@override
String get internationalServices => 'International Services';
@override
String get mainlandChinaServicesDescription =>
'For users mainly in Mainland China. Health, account, and friend data is stored in Mainland China.';
@override
String get internationalServicesDescription =>
'For users mainly outside Mainland China. Health, account, and friend data is stored internationally.';
@override
String get serviceRegionCannotBeChanged =>
'Service region cannot be changed after account creation.';
@override
String get settings => 'Settings';
@override
... ... @@ -800,14 +818,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get clickToAddTheHrvThemedWatchFace => 'Add HRV Watch Face';
@override
String get stayOnTopOfYourHealthFluctuations => 'Stay on top of your health';
String get stayOnTopOfYourHealthFluctuations => 'Track your body changes';
@override
String get addACloseContact => 'Add a Close Contact';
String get addACloseContact => 'Add a Loved One';
@override
String get oneMorePersonLookingOutForYourHealth =>
'Have someone care about your health';
String get oneMorePersonLookingOutForYourHealth => 'Follow your health';
@override
String get addAFriend => 'Add';
... ... @@ -885,6 +902,54 @@ class AppLocalizationsEn extends AppLocalizations {
String get accountSettings => 'Account Settings';
@override
String get appleAccount => 'Apple Account';
@override
String get googleAccount => 'Google Account';
@override
String get securityEmail => 'Email';
@override
String get addSecurityEmail => 'Add security email';
@override
String get securityEmailDescription =>
'Use a security email to help protect your account.';
@override
String get securityEmailHint => 'Email address';
@override
String get sendVerificationEmail => 'Send verification email';
@override
String get confirmYourEmail => 'Confirm your email';
@override
String enterCodeSentTo(Object email) {
return 'Enter the code sent to $email';
}
@override
String get emailVerificationHelp =>
'If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders';
@override
String get verificationCode => 'Verification code';
@override
String get resend => 'Re-send';
@override
String resendWithSeconds(int seconds) {
return 'Re-send($seconds)';
}
@override
String get invalidEmailFormat => 'Invalid email format';
@override
String get deleteAccount => 'Delete Account';
@override
... ... @@ -1077,7 +1142,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String reportOtherPossessiveTitle(String title) {
return '$title';
return 'Their $title';
}
@override
... ... @@ -1716,7 +1781,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get purchaseLinkContactUs => 'Contact Us';
@override
String get reportBottomSlogan => 'FEEL MORE, STRESS LESS';
String get reportBottomSlogan => '·Double the Awareness, Half the Stress·';
@override
String get refundExplanationTitle => 'Refund Information';
... ... @@ -1868,10 +1933,13 @@ class AppLocalizationsEn extends AppLocalizations {
String get appReviewIllustrationPlaceholder => 'Illustration Placeholder';
@override
String get overallStressLevelToday => 'Overall Stress Level Today';
String get overallStressLevelToday => 'here\'s your overall stress status';
@override
String get stressLevelsOnThatDay => 'Their Stress Levels on That Day';
@override
String get stressLevelsOnThatDay => 'Stress Levels on That Day';
String get stressLevelsToday => 'Their Stress Status Today';
@override
String get noPressureDataAvailableAtThisTime =>
... ... @@ -1884,16 +1952,16 @@ class AppLocalizationsEn extends AppLocalizations {
String get unlockNow => 'Unlock';
@override
String get pressureOverload => 'Pressure Overload';
String get pressureOverload => 'Overload';
@override
String get beMindfulOfStress => 'Be Aware of Stress';
String get beMindfulOfStress => 'Stressful';
@override
String get statusNormal => 'Status: Normal';
String get statusNormal => 'Normal';
@override
String get inExcellentCondition => 'In excellent condition';
String get inExcellentCondition => 'Excellent';
@override
String get waitingForData => 'Waiting for data';
... ... @@ -1914,11 +1982,11 @@ class AppLocalizationsEn extends AppLocalizations {
String get filming => 'Filming';
@override
String get unlockTheProVersion => 'Unlock the Pro Version';
String get unlockTheProVersion => 'Unlock Pro';
@override
String get embarkOnAJourneyOfStressAwarenessAndWellnessSupport =>
'Embark on a Journey of Stress Awareness and Wellness Support';
'Begin Your Stress Alerts & Health Journey';
@override
String sharePartnerCodeTemplate(String inviteCode) {
... ... @@ -1954,7 +2022,7 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get annualMemberDiscounts => 'Special Offer';
String get annualMemberDiscounts => 'Annual Membership Offer';
@override
String get specialOffers => 'OFF';
... ... @@ -1964,7 +2032,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String originalPrice(String price) {
return 'Was $price';
return '$price/y';
}
@override
... ... @@ -2360,4 +2428,19 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get emailLoginYourPassword => 'Your password';
@override
String get yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue =>
'Your account was signed out due to another device login or token expiration. Please log in again to continue.';
@override
String get contactUs => 'Contact us';
@override
String get pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible =>
'Please describe the problem clearly and include screen recordings if possible.';
@override
String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster =>
'Send us your User ID as it will help us identify the problem faster.';
}
... ...
... ... @@ -66,6 +66,23 @@ class AppLocalizationsZh extends AppLocalizations {
String get switchLanguage => '切换语言';
@override
String get mainlandChinaServices => '中国大陆服务';
@override
String get internationalServices => '国际服务';
@override
String get mainlandChinaServicesDescription =>
'适用于主要使用中国大陆服务的用户,健康、账号、好友数据将存储于中国大陆。';
@override
String get internationalServicesDescription =>
'适用于主要使用中国大陆以外服务的用户,健康、账号、好友数据将存储于国际。';
@override
String get serviceRegionCannotBeChanged => '账号创建后,服务区域不能直接更改。';
@override
String get settings => '设置';
@override
... ... @@ -835,6 +852,52 @@ class AppLocalizationsZh extends AppLocalizations {
String get accountSettings => '账号设置';
@override
String get appleAccount => 'Apple Account';
@override
String get googleAccount => 'Google Account';
@override
String get securityEmail => '安全邮箱';
@override
String get addSecurityEmail => '添加安全邮箱';
@override
String get securityEmailDescription => '添加安全邮箱,帮助保护你的账号。';
@override
String get securityEmailHint => '请输入邮箱地址';
@override
String get sendVerificationEmail => '发送验证邮件';
@override
String get confirmYourEmail => '验证你的邮箱';
@override
String enterCodeSentTo(Object email) {
return '请输入发送至 $email 的验证码';
}
@override
String get emailVerificationHelp => '如果没有收到邮件,请检查垃圾邮件、推广邮件或其他文件夹。';
@override
String get verificationCode => '验证码';
@override
String get resend => '重新发送';
@override
String resendWithSeconds(int seconds) {
return '重新发送($seconds)';
}
@override
String get invalidEmailFormat => '邮箱格式错误';
@override
String get deleteAccount => '注销账号';
@override
... ... @@ -1792,6 +1855,9 @@ class AppLocalizationsZh extends AppLocalizations {
String get stressLevelsOnThatDay => '该日压力状态';
@override
String get stressLevelsToday => '今日压力状态';
@override
String get noPressureDataAvailableAtThisTime => '暂无压力数据';
@override
... ... @@ -2261,4 +2327,21 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get emailLoginYourPassword => '请输入密码';
@override
String
get yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue =>
'由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。';
@override
String get contactUs => '联系我们';
@override
String
get pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible =>
'请清楚地描述问题,并尽可能附上屏幕录像。';
@override
String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster =>
'请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。';
}
... ...