Commit df1e3d3b2370b0cf31c2b87eb429da8a73df6f8f

Authored by 常守达
1 parent 30b195eb

feat(login): 换绑UI

... ... @@ -203,7 +203,7 @@ class BindPartnerView extends GetView<BindPartnerController> {
Clipboard.setData(
ClipboardData(text: controller.myInviteCode.value),
);
AppToast.show('复制成功');
AppToast.show(context.l10n.copiedSuccessfully);
ta.track('click_doublefeel_add_friend_page', properties: {
'channel_type': controller.channel,
'click_content': '复制共享码'
... ...
... ... @@ -103,7 +103,6 @@ class MyController extends GetxController {
await loadWatchThemes();
}
Future<void> toPremiumPage() async {
await Get.toNamed(Routes.PURCHASE, arguments: {
IntentKeys.channelType: '我的页面会员入口',
... ... @@ -116,4 +115,6 @@ class MyController extends GetxController {
} on Exception catch (e) {}
}
}
void refreshEmail() {}
}
... ...
... ... @@ -5,6 +5,7 @@ import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.
import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/friend_select_bottom_sheet.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/today/no_health_data_page.dart';
import 'package:doublefeel_flutter/app/modules/login/views/email_login_contact_us_bottom_sheet.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';
... ... @@ -394,7 +395,20 @@ class TodayController extends GetMaterialController {
});
},
onHelp: () {
Get.toNamed(Routes.HELP);
if (environmentConfig.region.value == AppRegion.china) {
Get.toNamed(Routes.HELP);
} else {
final userId = Get.find<UserPreferencesStorage>()
.preferences
.value
.meUserInfo
?.id ??
0;
sendFeedbackEmail(
userId: userId.toString(),
pageName: 'No heart rate data available page',
);
}
},
));
checkHealthDataAuthCardVisible();
... ...
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';
... ... @@ -69,7 +68,10 @@ class AccountSettingView extends GetView<MyController> {
],
_SecurityEmailRow(
email: securityEmail,
onTap: () => Get.to(() => const AddSecurityEmailView()),
onTap: () async {
await Get.toNamed(Routes.ADD_SECURITY_EMAIL);
controller.refreshEmail();
},
),
const Spacer(),
_AccountActionButton(
... ... @@ -81,7 +83,7 @@ class AccountSettingView extends GetView<MyController> {
const SizedBox(height: 12),
_AccountActionButton(
label: context.l10n.deleteAccount,
color: AppColors.warning,
color: context.colors.warning,
onTap: _showDeleteAccountSheet,
),
SizedBox(height: MediaQuery.paddingOf(context).bottom + 40),
... ... @@ -176,8 +178,8 @@ class _AccountInfoRow extends StatelessWidget {
children: [
Text(
title,
style: const TextStyle(
color: AppColors.textPrimary,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
... ... @@ -190,8 +192,8 @@ class _AccountInfoRow extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.textSecondary,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
... ... @@ -230,8 +232,8 @@ class _SecurityEmailRow extends StatelessWidget {
children: [
Text(
context.l10n.securityEmail,
style: const TextStyle(
color: AppColors.textPrimary,
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
... ... @@ -244,8 +246,8 @@ class _SecurityEmailRow extends StatelessWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: const TextStyle(
color: AppColors.textSecondary,
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.4,
... ...
import 'dart:async';
import 'package:doublefeel_flutter/app/modules/home/widgets/my/security_email_views.dart';
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';
class AddSecurityEmailController extends GetxController {
static final _emailPattern = RegExp(
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$',
);
static const resendSeconds = 60;
final emailController = TextEditingController();
final codeController = TextEditingController();
final hasSubmittedEmail = false.obs;
final emailInput = ''.obs;
final codeInput = ''.obs;
final secondsRemaining = resendSeconds.obs;
Timer? _resendTimer;
static final _passwordRegex = RegExp(
r'^(?=.*[0-9])(?=.*[A-Z]).{6,}$',
);
final passwordController = TextEditingController();
final passwordInput = ''.obs;
final isPasswordObscured = true.obs;
final confirmPasswordController = TextEditingController();
final confirmPasswordInput = ''.obs;
final isConfirmPasswordObscured = true.obs;
@override
void onInit() {
super.onInit();
emailController.addListener(_onEmailChanged);
codeController.addListener(_onCodeChanged);
passwordController.addListener(_onPasswordChanged);
confirmPasswordController.addListener(_onConfirmPasswordChanged);
}
void _onEmailChanged() {
emailInput.value = emailController.text.replaceAll(' ', '').trim();
}
void _onCodeChanged() {
codeInput.value = codeController.text.trim();
}
void _onPasswordChanged() {
passwordInput.value = passwordController.text.trim();
}
void _onConfirmPasswordChanged() {
confirmPasswordInput.value = confirmPasswordController.text.trim();
}
bool get isEmailValid => _emailPattern.hasMatch(emailInput.value);
bool get isPasswordValid =>
_passwordRegex.hasMatch(passwordInput.value) &&
passwordInput.value.isNotEmpty;
bool get isConfirmPasswordValid =>
_passwordRegex.hasMatch(confirmPasswordInput.value) &&
confirmPasswordInput.value.isNotEmpty &&
confirmPasswordInput.value == passwordInput.value;
bool get canSetPassword => isPasswordValid && isConfirmPasswordValid;
bool get showError => hasSubmittedEmail.value && !isEmailValid;
bool get canSubmitCode => codeInput.value.isNotEmpty;
void togglePasswordVisibility() {
isPasswordObscured.value = !isPasswordObscured.value;
}
void toggleConfirmPasswordVisibility() {
isConfirmPasswordObscured.value = !isConfirmPasswordObscured.value;
}
void sendVerificationEmail() {
hasSubmittedEmail.value = true;
if (!isEmailValid) return;
startResendCountdown();
Get.to(() => const SecurityEmailVerificationView());
}
void startResendCountdown() {
_resendTimer?.cancel();
secondsRemaining.value = resendSeconds;
_resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (secondsRemaining.value <= 1) {
timer.cancel();
secondsRemaining.value = 0;
return;
}
secondsRemaining.value -= 1;
});
}
void submitCode(BuildContext context) {
if (!canSubmitCode) return;
FocusScope.of(context).unfocus();
Get.to(() => const SetSecurityEmailPasswordView());
}
void setPassword(BuildContext context) {
if (!canSetPassword) return;
FocusScope.of(context).unfocus();
AppToast.show(context.l10n.settingsSaved);
TextInput.finishAutofillContext();
Get.back();
Get.back();
Get.back();
}
@override
void onClose() {
_resendTimer?.cancel();
emailController.dispose();
codeController.dispose();
passwordController.dispose();
confirmPasswordController.dispose();
super.onClose();
}
}
... ...
import 'dart:async';
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/widgets/my/security_email_controller.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
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 {
class AddSecurityEmailView extends GetView<AddSecurityEmailController> {
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 c = Get.isRegistered<AddSecurityEmailController>()
? controller
: Get.put(AddSecurityEmailController());
final l10n = context.l10n;
final showError = _hasSubmitted && !_isEmailValid;
final colors = context.colors;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: _securityEmailPageColor,
systemNavigationBarColor: colors.backgroundPage,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: _securityEmailPageColor,
backgroundColor: colors.backgroundPage,
appBar: const _SecurityEmailAppBar(),
body: SafeArea(
top: false,
... ... @@ -70,8 +39,8 @@ class _AddSecurityEmailViewState extends State<AddSecurityEmailView> {
const SizedBox(height: 16),
Text(
l10n.addSecurityEmail,
style: const TextStyle(
color: _securityEmailTitleColor,
style: TextStyle(
color: colors.textPrimary,
fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.25,
... ... @@ -81,8 +50,8 @@ class _AddSecurityEmailViewState extends State<AddSecurityEmailView> {
Text(
l10n.securityEmailDescription,
textAlign: TextAlign.center,
style: const TextStyle(
color: _securityEmailDescriptionColor,
style: TextStyle(
color: colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
... ... @@ -90,16 +59,12 @@ class _AddSecurityEmailViewState extends State<AddSecurityEmailView> {
),
const SizedBox(height: 32),
TextField(
controller: _emailController,
controller: c.emailController,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (_) => setState(() {}),
onSubmitted: (_) => _sendVerificationEmail(),
style: const TextStyle(
color: _securityEmailTitleColor,
onSubmitted: (_) => c.sendVerificationEmail(),
style: TextStyle(
color: colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
... ... @@ -114,35 +79,64 @@ class _AddSecurityEmailViewState extends State<AddSecurityEmailView> {
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
),
errorText: showError ? l10n.invalidEmailFormat : null,
errorStyle: const TextStyle(fontSize: 12),
border: _inputBorder(),
enabledBorder: _inputBorder(),
focusedBorder: _inputBorder(
color: _securityEmailBrandColor,
color: colors.primary,
),
suffixIcon: Obx(() {
return c.emailInput.value.isNotEmpty
? GestureDetector(
onTap: c.emailController.clear,
child: Icon(
Icons.cancel,
color: context.colors.textTertiary,
size: 18,
),
)
: SizedBox();
}),
),
),
Obx(() {
if (!c.showError) return const SizedBox(height: 0);
return Padding(
padding: const EdgeInsets.only(top: 8, left: 20),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
l10n.invalidEmailFormat,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
);
}),
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: Obx(
() => ElevatedButton(
onPressed:
c.isEmailValid ? c.sendVerificationEmail : null,
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
disabledBackgroundColor:
colors.primary.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.watchThemeNext),
),
child: Text(l10n.sendVerificationEmail),
),
),
],
... ... @@ -154,74 +148,25 @@ class _AddSecurityEmailViewState extends State<AddSecurityEmailView> {
}
}
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();
}
class SecurityEmailVerificationView
extends GetView<AddSecurityEmailController> {
const SecurityEmailVerificationView({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final colors = context.colors;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: _securityEmailPageColor,
systemNavigationBarColor: colors.backgroundPage,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: _securityEmailPageColor,
backgroundColor: colors.backgroundPage,
appBar: const _SecurityEmailAppBar(),
body: SafeArea(
top: false,
... ... @@ -232,20 +177,23 @@ class _SecurityEmailVerificationViewState
const SizedBox(height: 16),
Text(
l10n.confirmYourEmail,
style: const TextStyle(
color: _securityEmailTitleColor,
style: TextStyle(
color: colors.textPrimary,
fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
const SizedBox(height: 7),
Text.rich(
_verificationInstruction(
l10n.enterCodeSentTo(widget.email),
widget.email,
Obx(
() => Text.rich(
_verificationInstruction(
context,
l10n.enterCodeSentTo(controller.emailInput.value),
controller.emailInput.value,
),
textAlign: TextAlign.center,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 47),
Container(
... ... @@ -257,17 +205,15 @@ class _SecurityEmailVerificationViewState
child: Stack(
children: [
TextField(
controller: _codeController,
keyboardType: TextInputType.number,
controller: controller.codeController,
// keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
maxLength: 6,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly
],
onChanged: (_) => setState(() {}),
onSubmitted: (_) => _submitCode(),
style: const TextStyle(
color: _securityEmailTitleColor,
// inputFormatters: const [
// _DigitsOnlyFormatter(),
// ],
onSubmitted: (_) => controller.submitCode(context),
style: TextStyle(
color: colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
... ... @@ -289,60 +235,71 @@ class _SecurityEmailVerificationViewState
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),
),
child: Obx(
() {
final seconds = controller.secondsRemaining.value;
return TextButton(
onPressed: seconds == 0
? controller.startResendCountdown
: null,
style: TextButton.styleFrom(
foregroundColor: colors.primary,
disabledForegroundColor:
const Color(0xFFD9D9D9),
padding:
const EdgeInsets.symmetric(horizontal: 20),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
child: Text(
seconds == 0
? l10n.resend
: l10n.resendWithSeconds(seconds),
),
);
},
),
),
],
),
),
const SizedBox(height: 8),
const SizedBox(height: 60),
Text(
l10n.emailVerificationHelp,
textAlign: TextAlign.center,
style: const TextStyle(
color: _securityEmailDescriptionColor,
style: TextStyle(
color: colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.2,
),
),
const SizedBox(height: 64),
const SizedBox(height: 11),
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: Obx(
() => ElevatedButton(
onPressed: controller.canSubmitCode
? () => controller.submitCode(context)
: null,
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
disabledBackgroundColor:
colors.primary.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),
),
child: Text(l10n.submit),
),
),
],
... ... @@ -354,9 +311,14 @@ class _SecurityEmailVerificationViewState
}
}
TextSpan _verificationInstruction(String instruction, String email) {
const textStyle = TextStyle(
color: _securityEmailDescriptionColor,
TextSpan _verificationInstruction(
BuildContext context,
String instruction,
String email,
) {
final colors = context.colors;
final textStyle = TextStyle(
color: colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
... ... @@ -371,7 +333,7 @@ TextSpan _verificationInstruction(String instruction, String email) {
TextSpan(text: instruction.substring(0, emailStart)),
TextSpan(
text: instruction.substring(emailStart, emailEnd),
style: const TextStyle(color: _securityEmailTitleColor),
style: TextStyle(color: colors.textPrimary),
),
TextSpan(text: instruction.substring(emailEnd)),
],
... ... @@ -380,7 +342,10 @@ TextSpan _verificationInstruction(String instruction, String email) {
class _SecurityEmailAppBar extends StatelessWidget
implements PreferredSizeWidget {
const _SecurityEmailAppBar();
const _SecurityEmailAppBar({
this.onPressed,
});
final VoidCallback? onPressed;
@override
Size get preferredSize => const Size.fromHeight(44);
... ... @@ -388,13 +353,13 @@ class _SecurityEmailAppBar extends StatelessWidget
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: _securityEmailPageColor,
backgroundColor: context.colors.backgroundPage,
elevation: 0,
scrolledUnderElevation: 0,
toolbarHeight: 44,
leadingWidth: 60,
leading: IconButton(
onPressed: Get.back,
onPressed: onPressed ?? Get.back,
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
icon: Image.asset(
... ... @@ -413,3 +378,340 @@ OutlineInputBorder _inputBorder({Color color = Colors.transparent}) {
borderSide: BorderSide(color: color),
);
}
class SetSecurityEmailPasswordView extends GetView<AddSecurityEmailController> {
const SetSecurityEmailPasswordView({super.key});
@override
Widget build(BuildContext context) {
final c = Get.isRegistered<AddSecurityEmailController>()
? controller
: Get.put(AddSecurityEmailController());
final l10n = context.l10n;
final colors = context.colors;
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
statusBarBrightness: Brightness.light,
systemNavigationBarColor: colors.backgroundPage,
systemNavigationBarIconBrightness: Brightness.dark,
),
child: Scaffold(
backgroundColor: colors.backgroundPage,
appBar: _SecurityEmailAppBar(onPressed: () async {
final result = await DialogUtils.showCommonDialog(DialogMetaData(
title: context.l10n.setupIncomplete,
message: context.l10n
.setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup,
confirmText: context.l10n.setPassword,
cancelText: context.l10n.leave,
iconAsset: 'assets/images/common/ic_warning.png',
));
if (result.action == DialogAction.cancel) {
Get.back();
Get.back();
Get.back();
}
}),
body: SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
children: [
const SizedBox(height: 16),
Text(
l10n.setAPassword,
style: TextStyle(
color: colors.textPrimary,
fontSize: 24,
fontWeight: FontWeight.w600,
height: 1.25,
),
),
const SizedBox(height: 8),
Text(
l10n.setAPasswordToSignInWithYourEmail,
textAlign: TextAlign.center,
style: TextStyle(
color: colors.textSecondary,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.43,
),
),
const SizedBox(height: 48),
Row(
children: [
const SizedBox(width: 16),
Text(
context.l10n.enterYourPassword,
style: TextStyle(
color: const Color(0xFF141414),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 15),
AutofillGroup(
child: Column(
children: [
Offstage(
offstage: true,
child: TextField(
focusNode: FocusNode(canRequestFocus: false),
controller: c.emailController,
autofillHints: const [
AutofillHints.username,
AutofillHints.email
],
// readOnly: true,
// enabled: false,
),
),
Obx(() {
final isObscured = c.isPasswordObscured.value;
return TextField(
textInputAction: TextInputAction.next,
controller: c.passwordController,
// keyboardType: TextInputType.visiblePassword,
obscureText: isObscured,
autofillHints: const [AutofillHints.newPassword],
obscuringCharacter: '*',
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[\x21-\x7E]'),
),
],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.emailLoginYourPassword,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
),
suffixIconConstraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
suffixIcon: GestureDetector(
onTap: c.togglePasswordVisibility,
child: Padding(
padding: const EdgeInsets.only(right: 16),
child: Image.asset(
isObscured
? 'assets/images/common/ic_hide_password.png'
: 'assets/images/common/ic_show_password.webp',
color: context.colors.textTertiary,
width: 24,
height: 24,
),
),
),
),
);
}),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
textAlign: TextAlign.center,
context.l10n
.passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter,
style: TextStyle(
color: context.colors.textTertiary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(height: 32),
Row(
children: [
const SizedBox(width: 16),
Text(
context.l10n.confirmNewPassword,
style: TextStyle(
color: const Color(0xFF141414),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 15),
Obx(() {
final isObscured = c.isConfirmPasswordObscured.value;
return TextField(
controller: c.confirmPasswordController,
// keyboardType: TextInputType.visiblePassword,
obscureText: isObscured,
autofillHints: const [AutofillHints.newPassword],
obscuringCharacter: '*',
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
onSubmitted: (_) {
controller.setPassword(context);
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[\x21-\x7E]'),
),
],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.emailLoginYourPassword,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
),
suffixIconConstraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
suffixIcon: GestureDetector(
onTap: c.toggleConfirmPasswordVisibility,
child: Padding(
padding: const EdgeInsets.only(right: 16),
child: Image.asset(
isObscured
? 'assets/images/common/ic_hide_password.png'
: 'assets/images/common/ic_show_password.webp',
color: context.colors.textTertiary,
width: 24,
height: 24,
),
),
),
),
);
}),
],
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
textAlign: TextAlign.center,
context.l10n
.passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter,
style: TextStyle(
color: context.colors.textTertiary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
height: 48,
child: Obx(
() => ElevatedButton(
onPressed: c.canSetPassword
? () => c.setPassword(context)
: null,
style: ElevatedButton.styleFrom(
backgroundColor: colors.primary,
disabledBackgroundColor:
colors.primary.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(context.l10n.setPassword),
),
),
),
],
),
),
),
),
);
}
}
... ...
... ... @@ -5,12 +5,13 @@ import 'package:doublefeel_flutter/app/modules/login/views/email_login_view.dart
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/constants/app_const.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/config_api.dart';
import 'package:doublefeel_flutter/core/services/app_config_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:get/get.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
... ... @@ -195,16 +196,22 @@ class LoginController extends GetxController {
}
void openUserTerms() {
String url = environmentConfig.region.value == AppRegion.china
? AppConst.userTerms
: AppConst.userTermsGlobal;
Get.toNamed(
AppRoutes.webview,
parameters: {'url': AppConst.userTerms},
parameters: {'url': url},
);
}
void openPrivacyPolicy() {
String url = environmentConfig.region.value == AppRegion.china
? AppConst.privacyPolicy
: AppConst.privacyPolicyGlobal;
Get.toNamed(
AppRoutes.webview,
parameters: {'url': AppConst.privacyPolicy},
parameters: {'url': url},
);
}
... ... @@ -226,13 +233,21 @@ class LoginController extends GetxController {
passwordController.addListener(() {
passwordInput.value = passwordController.text;
});
resetPasswordCodeController.addListener(() {
resetPasswordCode.value = resetPasswordCodeController.text.trim();
});
}
bool get canResetPassword =>
resetPasswordCode.value.isNotEmpty && isPasswordValid;
@override
void onClose() {
_countdownTimer?.cancel();
phoneController.dispose();
codeController.dispose();
passwordController.dispose();
resetPasswordCodeController.dispose();
codeFocusNode.dispose();
super.onClose();
}
... ... @@ -332,7 +347,7 @@ class LoginController extends GetxController {
hasTriedEmailLogin.value = true;
if (!isEmailValid) {
AppToast.show('Invalid email format');
AppToast.show(l10n.invalidEmailFormat);
return;
}
... ... @@ -356,11 +371,15 @@ class LoginController extends GetxController {
// } finally {
// isLoggingIn.value = false;
// }
var success = true;
if (success) {
TextInput.finishAutofillContext();
}
}
Future<void> sendRestEmail() async {
if (!isEmailValid) {
AppToast.show('Invalid email format');
AppToast.show(l10n.invalidEmailFormat);
return;
}
... ... @@ -380,22 +399,20 @@ class LoginController extends GetxController {
// } finally {
// isLoggingIn.value = false;
// }
Get.to(EmailLoginNewPasswordView());
Get.to(() => EmailLoginNewPasswordView());
}
Future<void> resetPassword() async {
if (!isEmailValid) {
AppToast.show('Invalid email format');
return;
}
if (!isPasswordValid) {
AppToast.show('Invalid password format');
AppToast.show(l10n.invalidEmailFormat);
return;
}
if (resetPasswordCodeController.text.isEmpty) {
AppToast.show('Invalid code');
AppToast.show(l10n.invalidCode);
return;
}
// TODO: 邮箱登录接口待接入,API 路径由后端确认后在此调用
// isLoggingIn.value = true;
// try {
... ... @@ -412,6 +429,8 @@ class LoginController extends GetxController {
// } finally {
// isLoggingIn.value = false;
// }
AppToast.show(l10n.paswordHasBeenChanged);
Get.back();
Get.back();
}
... ...
import 'package:doublefeel_flutter/core/theme/app_theme.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:flutter/material.dart';
... ... @@ -114,10 +115,15 @@ class EmailLoginContactUsBottomSheet extends StatelessWidget {
}
Future<void> sendFeedbackEmail({
required String userId,
required String? userId,
String pageName = 'Setting page',
}) async {
final String cleanUserId = userId.trim().isEmpty ? 'N/A' : userId.trim();
String cleanUserId;
if (userId == null || userId == '0') {
cleanUserId = 'N/A';
} else {
cleanUserId = userId.trim().isEmpty ? 'N/A' : userId.trim();
}
final String body =
'This issue was reported from $pageName by User ID: $cleanUserId.';
... ... @@ -152,6 +158,12 @@ class _UserIdField extends StatelessWidget {
@override
Widget build(BuildContext context) {
String cleanUserId = userId;
if (userId == '0') {
cleanUserId = 'N/A';
} else {
cleanUserId = userId.trim().isEmpty ? 'N/A' : userId.trim();
}
return Container(
height: 48,
margin: const EdgeInsets.symmetric(horizontal: 16),
... ... @@ -168,7 +180,7 @@ class _UserIdField extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 8),
child: Text(
userId,
cleanUserId,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
... ... @@ -183,8 +195,8 @@ class _UserIdField extends StatelessWidget {
color: const Color(0xFF7B9BFB),
child: InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: userId));
AppToast.show('Copied');
Clipboard.setData(ClipboardData(text: cleanUserId));
AppToast.show(context.l10n.copiedSuccessfully);
},
child: SizedBox(
width: 48,
... ... @@ -214,28 +226,28 @@ class _SendEmailButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Material(
color: const Color(0xFF845EEE),
color: context.colors.primary,
borderRadius: BorderRadius.circular(24),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(24),
child: const SizedBox(
child: SizedBox(
width: 220,
height: 48,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image(
const Image(
image: AssetImage(
'assets/images/common/ic_contact_us.png',
),
width: 24,
height: 24,
),
SizedBox(width: 6),
const SizedBox(width: 6),
Text(
'Send Email',
style: TextStyle(
context.l10n.sendEmail,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
... ...
... ... @@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../controllers/login_controller.dart';
import 'email_login_contact_us_bottom_sheet.dart';
... ... @@ -73,7 +72,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
Transform.translate(
offset: const Offset(0, -8),
child: Text(
'Check your email',
context.l10n.checkYourEmail,
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF0F0F11),
... ... @@ -88,7 +87,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
TextSpan(
children: [
TextSpan(
text: 'We’ve sent a code to ',
text: l10n.weVeSentACodeTo,
style: TextStyle(
color: const Color(0xFF78787D),
fontSize: 14,
... ... @@ -96,6 +95,9 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
),
),
TextSpan(
text: ' ',
),
TextSpan(
text: controller.emailInput.value,
style: TextStyle(
color: const Color(0xFF0F0F11),
... ... @@ -104,8 +106,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
),
),
TextSpan(
text:
'. Didn’t get it? Check your spam folder or try again.',
text: l10n.didnTGetItCheckYourSpamFolderOrTryAgain,
style: TextStyle(
color: const Color(0xFF78787D),
fontSize: 14,
... ... @@ -128,7 +129,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
Padding(
padding: const EdgeInsets.only(left: 16, bottom: 15),
child: Text(
'Code from email',
context.l10n.codeFromEmail,
style: TextStyle(
color: const Color(0xFF141414),
fontSize: 14,
... ... @@ -136,67 +137,65 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
),
),
),
Obx(() {
controller.resetPasswordCode.value;
return TextField(
controller: controller.resetPasswordCodeController,
keyboardType: TextInputType.text,
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
// 获取默认的菜单项列表
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
TextField(
controller: controller.resetPasswordCodeController,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.next,
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
// 获取默认的菜单项列表
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
// 例如:只保留“粘贴” (Paste)
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9@._\-+]'),
),
],
style: TextStyle(
color: context.colors.textPrimary,
// 例如:只保留“粘贴” (Paste)
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
// inputFormatters: [
// FilteringTextInputFormatter.deny(RegExp(r'\s')),
// FilteringTextInputFormatter.allow(
// RegExp(r'[a-zA-Z0-9@._\-+]'),
// ),
// ],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.code,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: 'Code',
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Color(0xFFF3F3F3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
counterText: '',
filled: true,
fillColor: Color(0xFFF3F3F3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
),
);
}),
),
),
SizedBox(
height: 20,
... ... @@ -204,7 +203,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
Padding(
padding: const EdgeInsets.only(left: 16, bottom: 15),
child: Text(
'New password',
l10n.newPassword,
style: TextStyle(
color: const Color(0xFF141414),
fontSize: 14,
... ... @@ -219,6 +218,13 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
controller: controller.passwordController,
keyboardType: TextInputType.visiblePassword,
obscureText: isObscured,
autofillHints: const [AutofillHints.newPassword],
onSubmitted: (value) {
if (controller.canResetPassword) {
FocusScope.of(context).unfocus();
controller.resetPassword();
}
},
obscuringCharacter: '*',
// maxLength: 13,
contextMenuBuilder: (BuildContext context,
... ... @@ -304,7 +310,8 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
!pwOk &&
controller.passwordInput.value.isNotEmpty;
return Text(
'Password must be at least 6 characters and include 1 number and 1 uppercase letter.',
context.l10n
.passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter,
style: TextStyle(
color: isError
? Colors.red
... ... @@ -318,11 +325,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
const SizedBox(height: 39),
Obx(() {
final canProceed = controller
.resetPasswordCodeController
.text
.isNotEmpty &&
controller.isPasswordValid;
final canProceed = controller.canResetPassword;
return SizedBox(
height: 48,
child: ElevatedButton(
... ... @@ -343,7 +346,7 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
fontWeight: FontWeight.w600,
),
),
child: Text('Reset password'),
child: Text(context.l10n.resetPassword),
),
);
}),
... ... @@ -352,11 +355,11 @@ class EmailLoginNewPasswordView extends GetView<LoginController> {
Center(
child: GestureDetector(
onTap: () => showEmailLoginContactUsBottomSheet(
userId: controller.emailInput.value.trim(),
pageName: 'Reset password page',
userId: '0',
pageName: 'Log In page',
),
child: Text(
'Having trouble? Contact us',
l10n.havingTroubleContactUs,
style: TextStyle(
color: const Color(0xFF39CAF4),
fontSize: 14,
... ...
... ... @@ -71,7 +71,7 @@ class EmailLoginResetPasswordView extends GetView<LoginController> {
Transform.translate(
offset: const Offset(0, -8),
child: Text(
'Reset password',
context.l10n.resetPassword,
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF0F0F11),
... ... @@ -82,7 +82,7 @@ class EmailLoginResetPasswordView extends GetView<LoginController> {
),
Text(
'''You'll receive a code via email to reset your password.''',
context.l10n.youLlReceiveACodeViaEmailToResetYourPassword,
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF78787D),
... ... @@ -103,6 +103,7 @@ class EmailLoginResetPasswordView extends GetView<LoginController> {
final emailNotEmpty =
controller.emailInput.value.isNotEmpty;
return TextField(
autofillHints: const [AutofillHints.email],
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
// maxLength: 13,
... ... @@ -120,12 +121,12 @@ class EmailLoginResetPasswordView extends GetView<LoginController> {
buttonItems: buttonItems,
);
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9@._\-+]'),
),
],
// inputFormatters: [
// FilteringTextInputFormatter.deny(RegExp(r'\s')),
// FilteringTextInputFormatter.allow(
// RegExp(r'[a-zA-Z0-9@._\-+]'),
// ),
// ],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
... ... @@ -187,7 +188,8 @@ class EmailLoginResetPasswordView extends GetView<LoginController> {
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'This email is not registered. Please check and try again.',
context.l10n
.thisEmailIsNotRegisteredPleaseCheckAndTryAgain,
style: TextStyle(
color: Colors.red,
fontSize: 12,
... ... @@ -224,7 +226,7 @@ class EmailLoginResetPasswordView extends GetView<LoginController> {
fontWeight: FontWeight.w600,
),
),
child: Text('Send reset email'),
child: Text(context.l10n.sendResetEmail),
),
);
}),
... ...
... ... @@ -83,7 +83,7 @@ class EmailLoginView extends GetView<LoginController> {
),
Text(
'Enter your email and password',
l10n.enterYourEmailAndPassword,
textAlign: TextAlign.center,
style: TextStyle(
color: const Color(0xFF78787D),
... ... @@ -96,117 +96,22 @@ class EmailLoginView extends GetView<LoginController> {
// 输入区
Padding(
padding: const EdgeInsets.symmetric(horizontal: 28),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// email输入框
Obx(() {
final emailNotEmpty =
controller.emailInput.value.isNotEmpty;
return TextField(
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
// maxLength: 13,
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
// 获取默认的菜单项列表
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
// 例如:只保留“粘贴” (Paste)
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[a-zA-Z0-9@._\-+]'),
),
],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.emailLoginEmailHint,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Color(0xFFF3F3F3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
),
suffixIcon: emailNotEmpty
? GestureDetector(
onTap: controller.emailController.clear,
child: Icon(
Icons.cancel,
color: context.colors.textTertiary,
size: 18,
),
)
: null,
),
);
}),
SizedBox(
height: 20,
child: Obx(() {
final hasTried =
controller.hasTriedEmailLogin.value;
final emailOk = controller.isEmailValid;
final showError = hasTried && !emailOk;
return Visibility(
visible: showError,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
'Invalid email format',
style: TextStyle(
color: Colors.red,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
),
);
}),
),
Obx(() {
final isObscured =
controller.isPasswordObscured.value;
return TextField(
controller: controller.passwordController,
keyboardType: TextInputType.visiblePassword,
obscureText: isObscured,
child: AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// email输入框
Obx(() {
final emailNotEmpty =
controller.emailInput.value.isNotEmpty;
return TextField(
autofillHints: const [
AutofillHints.email,
AutofillHints.username
],
textInputAction: TextInputAction.next,
controller: controller.emailController,
keyboardType: TextInputType.emailAddress,
// maxLength: 13,
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
... ... @@ -214,7 +119,7 @@ class EmailLoginView extends GetView<LoginController> {
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
// 例如:只保留"粘贴" (Paste)
// 例如:只保留“粘贴” (Paste)
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar.buttonItems(
... ... @@ -222,146 +127,271 @@ class EmailLoginView extends GetView<LoginController> {
buttonItems: buttonItems,
);
},
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[\x21-\x7E]'),
),
],
// inputFormatters: [
// FilteringTextInputFormatter.deny(RegExp(r'\s')),
// FilteringTextInputFormatter.allow(
// RegExp(r'[a-zA-Z0-9@._\-+]'),
// ),
// ],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
decoration: InputDecoration(
hintText: l10n.emailLoginYourPassword,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Color(0xFFF3F3F3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
hintText: l10n.emailLoginEmailHint,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Color(0xFFF3F3F3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
suffixIcon: emailNotEmpty
? GestureDetector(
onTap: controller.emailController.clear,
child: Icon(
Icons.cancel,
color: context.colors.textTertiary,
size: 18,
),
)
: null,
),
);
}),
SizedBox(
height: 20,
child: Obx(() {
final hasTried =
controller.hasTriedEmailLogin.value;
final emailOk = controller.isEmailValid;
final showError = hasTried && !emailOk;
return Visibility(
visible: showError,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
context.l10n.invalidEmailFormat,
style: TextStyle(
color: Colors.red,
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
suffixIconConstraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
);
}),
),
Obx(() {
final isObscured =
controller.isPasswordObscured.value;
return TextField(
controller: controller.passwordController,
keyboardType: TextInputType.visiblePassword,
obscureText: isObscured,
autofillHints: const [
AutofillHints.password,
],
// maxLength: 13,
contextMenuBuilder: (BuildContext context,
EditableTextState editableTextState) {
// 获取默认的菜单项列表
List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
// 例如:只保留"粘贴" (Paste)
buttonItems.removeWhere((item) =>
item.type != ContextMenuButtonType.paste);
return AdaptiveTextSelectionToolbar
.buttonItems(
anchors:
editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
inputFormatters: [
FilteringTextInputFormatter.deny(
RegExp(r'\s')),
FilteringTextInputFormatter.allow(
RegExp(r'[\x21-\x7E]'),
),
suffixIcon: GestureDetector(
onTap: controller.togglePasswordVisibility,
child: Padding(
padding: const EdgeInsets.only(right: 16),
child: Image.asset(
isObscured
? 'assets/images/common/ic_hide_password.png'
: 'assets/images/common/ic_show_password.webp',
color: context.colors.textTertiary,
width: 24,
height: 24,
],
style: TextStyle(
color: context.colors.textPrimary,
fontSize: 16,
fontWeight: FontWeight.w400,
),
onSubmitted: (_) {
final isLoggingIn =
controller.isLoggingIn.value;
final canProceed =
controller.isPasswordValid &&
controller.isEmailValid;
if (!isLoggingIn && canProceed) {
FocusScope.of(context).nextFocus();
controller.emailLogin();
}
},
decoration: InputDecoration(
hintText: l10n.emailLoginYourPassword,
hintStyle: TextStyle(
color: context.colors.textTertiary,
fontSize: 16,
),
counterText: '',
filled: true,
fillColor: Color(0xFFF3F3F3),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(27),
borderSide: BorderSide(
color: context.colors.primary,
width: 1,
),
),
)));
}),
const SizedBox(height: 8),
suffixIconConstraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
suffixIcon: GestureDetector(
onTap:
controller.togglePasswordVisibility,
child: Padding(
padding:
const EdgeInsets.only(right: 16),
child: Image.asset(
isObscured
? 'assets/images/common/ic_hide_password.png'
: 'assets/images/common/ic_show_password.webp',
color: context.colors.textTertiary,
width: 24,
height: 24,
),
),
)));
}),
// 密码格式提示
Obx(() {
final hasTried = controller.hasTriedEmailLogin.value;
final pwOk = controller.isPasswordValid;
final isError = hasTried && !pwOk;
return Text(
'Password must be at least 6 characters and include 1 number and 1 uppercase letter.',
style: TextStyle(
color: isError
? Colors.red
: context.colors.textTertiary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
);
}),
const SizedBox(height: 8),
const SizedBox(height: 24),
Center(
child: GestureDetector(
onTap: () {
Get.to(() => const EmailLoginResetPasswordView());
},
child: Text(
'Forgot password?',
// 密码格式提示
Obx(() {
final hasTried =
controller.hasTriedEmailLogin.value;
final pwOk = controller.isPasswordValid;
final isError = hasTried && !pwOk;
return Text(
context.l10n
.passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter,
style: TextStyle(
color: const Color(0xFF7B9BFB),
fontSize: 14,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: const Color(0xFF7B9BFB),
color: isError
? Colors.red
: context.colors.textTertiary,
fontSize: 12,
fontWeight: FontWeight.w400,
),
textAlign: TextAlign.center,
);
}),
const SizedBox(height: 24),
Center(
child: GestureDetector(
onTap: () {
Get.to(
() => const EmailLoginResetPasswordView());
},
child: Text(
l10n.forgotPassword,
style: TextStyle(
color: const Color(0xFF7B9BFB),
fontSize: 14,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: const Color(0xFF7B9BFB),
),
),
),
),
),
const SizedBox(height: 48),
const SizedBox(height: 48),
// Continue 按钮
Obx(() {
final isLoggingIn = controller.isLoggingIn.value;
final canProceed = controller.isPasswordValid &&
controller.isEmailValid;
return SizedBox(
height: 48,
child: ElevatedButton(
onPressed: (!isLoggingIn && canProceed)
? controller.emailLogin
: null,
style: ElevatedButton.styleFrom(
backgroundColor: context.colors.primary,
foregroundColor: Colors.white,
disabledBackgroundColor: context.colors.primary
.withValues(alpha: 0.4),
disabledForegroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
// Continue 按钮
Obx(() {
final isLoggingIn = controller.isLoggingIn.value;
final canProceed = controller.isPasswordValid &&
controller.isEmailValid;
return SizedBox(
height: 48,
child: ElevatedButton(
onPressed: (!isLoggingIn && canProceed)
? controller.emailLogin
: null,
style: ElevatedButton.styleFrom(
backgroundColor: context.colors.primary,
foregroundColor: Colors.white,
disabledBackgroundColor: context
.colors.primary
.withValues(alpha: 0.4),
disabledForegroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
textStyle: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
child: isLoggingIn
? Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const _SpinningLoader(),
const SizedBox(width: 8),
Text(l10n.phoneLoginLoggingIn),
],
)
: Text(l10n.continueButton),
),
child: isLoggingIn
? Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
const _SpinningLoader(),
const SizedBox(width: 8),
Text(l10n.phoneLoginLoggingIn),
],
)
: Text(l10n.continueButton),
),
);
}),
],
);
}),
],
),
),
),
],
... ... @@ -427,34 +457,3 @@ class _SpinningLoaderState extends State<_SpinningLoader>
);
}
}
// ── 手机号格式化 ───────────────────────────────────────────────────
class _PhoneTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final text = newValue.text.replaceAll(' ', '');
if (text.isEmpty) {
return newValue;
}
final buffer = StringBuffer();
for (int i = 0; i < text.length; i++) {
buffer.write(text[i]);
final nonLeadingIndex = i + 1;
if (nonLeadingIndex == 3 || nonLeadingIndex == 7) {
if (nonLeadingIndex < text.length) {
buffer.write(' ');
}
}
}
final string = buffer.toString();
return newValue.copyWith(
text: string,
selection: TextSelection.collapsed(offset: string.length),
);
}
}
... ...
import 'dart:async';
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/app_const.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
... ... @@ -60,6 +61,7 @@ class PurchaseController extends GetxController {
bool isFromOnboard = false;
String channelType = '';
final environmentConfig = Get.find<AppEnvironmentConfig>();
@override
void onInit() {
... ... @@ -396,16 +398,22 @@ class PurchaseController extends GetxController {
}
void openUserTerms() {
String url = environmentConfig.region.value == AppRegion.china
? AppConst.userTerms
: AppConst.userTermsGlobal;
Get.toNamed(
AppRoutes.webview,
parameters: {'url': AppConst.userTerms},
parameters: {'url': url},
);
}
void openPrivacyPolicy() {
String url = environmentConfig.region.value == AppRegion.china
? AppConst.privacyPolicy
: AppConst.privacyPolicyGlobal;
Get.toNamed(
AppRoutes.webview,
parameters: {'url': AppConst.privacyPolicy},
parameters: {'url': url},
);
}
... ...
import 'package:doublefeel_flutter/app/modules/friends/bindings/friend_home_binding.dart';
import 'package:doublefeel_flutter/app/modules/friends/views/friend_home_page.dart';
import 'package:doublefeel_flutter/app/modules/login/views/email_login_view.dart';
import 'package:doublefeel_flutter/app/modules/home/widgets/my/security_email_controller.dart';
import 'package:doublefeel_flutter/app/modules/membership_offer/bindings/membership_detail_binding.dart';
import 'package:doublefeel_flutter/app/modules/membership_offer/views/membership_detail_view.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
... ... @@ -50,6 +50,7 @@ import '../modules/watch_theme/views/watch_theme_preview_view.dart';
import '../modules/watch_theme/views/watch_theme_view.dart';
import '../modules/account_settings/bindings/account_settings_binding.dart';
import '../modules/account_settings/views/account_settings_view.dart';
import '../modules/home/widgets/my/security_email_views.dart';
import '../modules/webview/bindings/webview_binding.dart';
import '../modules/webview/views/webview_page.dart';
... ... @@ -216,5 +217,14 @@ abstract final class AppPages {
page: () => const AccountSettingsView(),
binding: AccountSettingsBinding(),
),
GetPage(
name: Routes.ADD_SECURITY_EMAIL,
page: () => const AddSecurityEmailView(),
binding: BindingsBuilder(() {
Get.lazyPut<AddSecurityEmailController>(
() => AddSecurityEmailController(),
);
}),
),
];
}
... ...
... ... @@ -26,6 +26,7 @@ abstract class Routes {
static const FRIEND_TREND = _Paths.FRIEND_TREND;
static const FRIEND_HOME = _Paths.FRIEND_HOME;
static const APPLE_HEALTH_UPLOAD_TEST = _Paths.APPLE_HEALTH_UPLOAD_TEST;
static const ADD_SECURITY_EMAIL = _Paths.ADD_SECURITY_EMAIL;
}
abstract class _Paths {
... ... @@ -53,4 +54,5 @@ abstract class _Paths {
static const FRIEND_TREND = '/friend-trend';
static const FRIEND_HOME = '/friend-home';
static const APPLE_HEALTH_UPLOAD_TEST = '/apple-health-upload-test';
static const ADD_SECURITY_EMAIL = '/add-security-email';
}
... ...
... ... @@ -25,6 +25,10 @@ abstract final class AppConst {
'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.html';
static const String privacyPolicy =
'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E9%9A%90%E7%A7%81%E5%8D%8F%E8%AE%AE.html';
static const String userTermsGlobal =
'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.html';
static const String privacyPolicyGlobal =
'https://cdn.doublefeel.cn/doublefeel/protocol/DoubleFeel%E9%9A%90%E7%A7%81%E5%8D%8F%E8%AE%AE.html';
static const String userSubmissionAgreement =
'https://cdn.doublefeel.cn/doublefeel/protocol/creator.html';
}
... ...
... ... @@ -263,12 +263,11 @@
"appleAccount": "Apple Account",
"googleAccount": "Google Account",
"securityEmail": "Email",
"addSecurityEmail": "Add security email",
"securityEmailDescription": "Use a security email to help protect your account.",
"addSecurityEmail": "Add an email",
"securityEmailDescription": "Adding an email address makes it easier to recover your account. For your account security, please use an email address you own.",
"securityEmailHint": "Email address",
"sendVerificationEmail": "Send verification email",
"confirmYourEmail": "Confirm your email",
"enterCodeSentTo": "Enter the code sent to {email}",
"enterCodeSentTo": "Enter the code sent to \n{email}",
"@enterCodeSentTo": {
"placeholders": {
"email": {}
... ... @@ -824,5 +823,32 @@
"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."
"sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "Send us your User ID as it will help us identify the problem faster.",
"setAPassword": "Set a Password",
"setAPasswordToSignInWithYourEmail": "Set a password to sign in with your email.",
"settingsSaved": "Settings saved",
"leave": "Leave",
"setPassword": "Set Password",
"setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup": "Set a password to add this email successfully. Leaving now will cancel this setup.",
"setupIncomplete": "Setup Incomplete",
"enterYourPassword": "Enter your password",
"confirmNewPassword": "Confirm new password",
"passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter": "Password must be at least 6 characters and include 1 number and 1 uppercase letter.",
"forgotPassword": "Forgot password?",
"enterYourEmailAndPassword": "Enter your email and password",
"newPassword": "New password",
"weVeSentACodeTo": "We’ve sent a code to",
"didnTGetItCheckYourSpamFolderOrTryAgain": ". Didn’t get it? Check your spam folder or try again.",
"checkYourEmail": "Check your email",
"code": "Code",
"resetPassword": "Reset password",
"havingTroubleContactUs": "Having trouble? Contact us",
"sendEmail": "Send Email",
"thisEmailIsNotRegisteredPleaseCheckAndTryAgain": "This email is not registered. Please check and try again.",
"youLlReceiveACodeViaEmailToResetYourPassword": "You'll receive a code via email to reset your password.",
"codeFromEmail": "Code from email",
"sendResetEmail": "Send reset email",
"invalidCode": "Invalid code",
"paswordHasBeenChanged": "Pasword has been changed",
"copiedSuccessfully": "Copied successfully"
}
\ No newline at end of file
... ...
... ... @@ -349,7 +349,6 @@
"addSecurityEmail": "添加安全邮箱",
"securityEmailDescription": "添加安全邮箱,帮助保护你的账号。",
"securityEmailHint": "请输入邮箱地址",
"sendVerificationEmail": "发送验证邮件",
"confirmYourEmail": "验证你的邮箱",
"enterCodeSentTo": "请输入发送至 {email} 的验证码",
"@enterCodeSentTo": {
... ... @@ -1217,5 +1216,32 @@
"yourAccountWasSignedOutDueToAnotherDeviceLoginOrTokenExpirationPleaseLogInAgainToContinue": "由于其他设备登录或令牌过期,您的账户已被注销。请重新登录以继续操作。",
"contactUs": "联系我们",
"pleaseDescribeTheProblemClearlyAndIncludeScreenRecordingsIfPossible": "请清楚地描述问题,并尽可能附上屏幕录像。",
"sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。"
"sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster": "请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。",
"setAPassword": "设置密码",
"setAPasswordToSignInWithYourEmail": "设置密码,以便使用您的电子邮箱登录。",
"settingsSaved": "设置已保存",
"leave": "离开",
"setPassword": "设置密码",
"setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup": "请设置密码以成功添加此电子邮箱。如果现在退出,将取消此设置。",
"setupIncomplete": "设置未完成",
"enterYourPassword": "请输入密码",
"confirmNewPassword": "确认新密码",
"passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter": "密码必须至少包含 6 个字符,并包含 1 个数字和 1 个大写字母。",
"forgotPassword": "忘记密码了吗?",
"enterYourEmailAndPassword": "请输入您的电子邮箱和密码",
"newPassword": "新密码",
"weVeSentACodeTo": "我们已向 ",
"didnTGetItCheckYourSpamFolderOrTryAgain": ". 没收到吗?请检查您的垃圾邮件文件夹或重试一下。",
"checkYourEmail": "请查看您的电子邮件",
"code": "代码",
"resetPassword": "重置密码",
"havingTroubleContactUs": "遇到问题了吗?请联系我们",
"sendEmail": "发送电子邮件",
"thisEmailIsNotRegisteredPleaseCheckAndTryAgain": "该邮箱未注册。请检查后重试。",
"youLlReceiveACodeViaEmailToResetYourPassword": "您将通过电子邮件收到一个用于重置密码的验证码。",
"codeFromEmail": "来自电子邮件的代码",
"sendResetEmail": "发送重置邮件",
"invalidCode": "无效代码",
"paswordHasBeenChanged": "密码已更改",
"copiedSuccessfully": "复制成功"
}
\ No newline at end of file
... ...
... ... @@ -1695,12 +1695,6 @@ abstract class AppLocalizations {
/// **'请输入邮箱地址'**
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:
... ... @@ -4349,6 +4343,170 @@ abstract class AppLocalizations {
/// In zh, this message translates to:
/// **'请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。'**
String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster;
/// No description provided for @setAPassword.
///
/// In zh, this message translates to:
/// **'设置密码'**
String get setAPassword;
/// No description provided for @setAPasswordToSignInWithYourEmail.
///
/// In zh, this message translates to:
/// **'设置密码,以便使用您的电子邮箱登录。'**
String get setAPasswordToSignInWithYourEmail;
/// No description provided for @settingsSaved.
///
/// In zh, this message translates to:
/// **'设置已保存'**
String get settingsSaved;
/// No description provided for @leave.
///
/// In zh, this message translates to:
/// **'离开'**
String get leave;
/// No description provided for @setPassword.
///
/// In zh, this message translates to:
/// **'设置密码'**
String get setPassword;
/// No description provided for @setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup.
///
/// In zh, this message translates to:
/// **'请设置密码以成功添加此电子邮箱。如果现在退出,将取消此设置。'**
String
get setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup;
/// No description provided for @setupIncomplete.
///
/// In zh, this message translates to:
/// **'设置未完成'**
String get setupIncomplete;
/// No description provided for @enterYourPassword.
///
/// In zh, this message translates to:
/// **'请输入密码'**
String get enterYourPassword;
/// No description provided for @confirmNewPassword.
///
/// In zh, this message translates to:
/// **'确认新密码'**
String get confirmNewPassword;
/// No description provided for @passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter.
///
/// In zh, this message translates to:
/// **'密码必须至少包含 6 个字符,并包含 1 个数字和 1 个大写字母。'**
String
get passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter;
/// No description provided for @forgotPassword.
///
/// In zh, this message translates to:
/// **'忘记密码了吗?'**
String get forgotPassword;
/// No description provided for @enterYourEmailAndPassword.
///
/// In zh, this message translates to:
/// **'请输入您的电子邮箱和密码'**
String get enterYourEmailAndPassword;
/// No description provided for @newPassword.
///
/// In zh, this message translates to:
/// **'新密码'**
String get newPassword;
/// No description provided for @weVeSentACodeTo.
///
/// In zh, this message translates to:
/// **'我们已向 '**
String get weVeSentACodeTo;
/// No description provided for @didnTGetItCheckYourSpamFolderOrTryAgain.
///
/// In zh, this message translates to:
/// **'. 没收到吗?请检查您的垃圾邮件文件夹或重试一下。'**
String get didnTGetItCheckYourSpamFolderOrTryAgain;
/// No description provided for @checkYourEmail.
///
/// In zh, this message translates to:
/// **'请查看您的电子邮件'**
String get checkYourEmail;
/// No description provided for @code.
///
/// In zh, this message translates to:
/// **'代码'**
String get code;
/// No description provided for @resetPassword.
///
/// In zh, this message translates to:
/// **'重置密码'**
String get resetPassword;
/// No description provided for @havingTroubleContactUs.
///
/// In zh, this message translates to:
/// **'遇到问题了吗?请联系我们'**
String get havingTroubleContactUs;
/// No description provided for @sendEmail.
///
/// In zh, this message translates to:
/// **'发送电子邮件'**
String get sendEmail;
/// No description provided for @thisEmailIsNotRegisteredPleaseCheckAndTryAgain.
///
/// In zh, this message translates to:
/// **'该邮箱未注册。请检查后重试。'**
String get thisEmailIsNotRegisteredPleaseCheckAndTryAgain;
/// No description provided for @youLlReceiveACodeViaEmailToResetYourPassword.
///
/// In zh, this message translates to:
/// **'您将通过电子邮件收到一个用于重置密码的验证码。'**
String get youLlReceiveACodeViaEmailToResetYourPassword;
/// No description provided for @codeFromEmail.
///
/// In zh, this message translates to:
/// **'来自电子邮件的代码'**
String get codeFromEmail;
/// No description provided for @sendResetEmail.
///
/// In zh, this message translates to:
/// **'发送重置邮件'**
String get sendResetEmail;
/// No description provided for @invalidCode.
///
/// In zh, this message translates to:
/// **'无效代码'**
String get invalidCode;
/// No description provided for @paswordHasBeenChanged.
///
/// In zh, this message translates to:
/// **'密码已更改'**
String get paswordHasBeenChanged;
/// No description provided for @copiedSuccessfully.
///
/// In zh, this message translates to:
/// **'复制成功'**
String get copiedSuccessfully;
}
class _AppLocalizationsDelegate
... ...
... ... @@ -911,24 +911,21 @@ class AppLocalizationsEn extends AppLocalizations {
String get securityEmail => 'Email';
@override
String get addSecurityEmail => 'Add security email';
String get addSecurityEmail => 'Add an email';
@override
String get securityEmailDescription =>
'Use a security email to help protect your account.';
'Adding an email address makes it easier to recover your account. For your account security, please use an email address you own.';
@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';
return 'Enter the code sent to \n$email';
}
@override
... ... @@ -2443,4 +2440,91 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster =>
'Send us your User ID as it will help us identify the problem faster.';
@override
String get setAPassword => 'Set a Password';
@override
String get setAPasswordToSignInWithYourEmail =>
'Set a password to sign in with your email.';
@override
String get settingsSaved => 'Settings saved';
@override
String get leave => 'Leave';
@override
String get setPassword => 'Set Password';
@override
String get setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup =>
'Set a password to add this email successfully. Leaving now will cancel this setup.';
@override
String get setupIncomplete => 'Setup Incomplete';
@override
String get enterYourPassword => 'Enter your password';
@override
String get confirmNewPassword => 'Confirm new password';
@override
String get passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter =>
'Password must be at least 6 characters and include 1 number and 1 uppercase letter.';
@override
String get forgotPassword => 'Forgot password?';
@override
String get enterYourEmailAndPassword => 'Enter your email and password';
@override
String get newPassword => 'New password';
@override
String get weVeSentACodeTo => 'We’ve sent a code to';
@override
String get didnTGetItCheckYourSpamFolderOrTryAgain =>
'. Didn’t get it? Check your spam folder or try again.';
@override
String get checkYourEmail => 'Check your email';
@override
String get code => 'Code';
@override
String get resetPassword => 'Reset password';
@override
String get havingTroubleContactUs => 'Having trouble? Contact us';
@override
String get sendEmail => 'Send Email';
@override
String get thisEmailIsNotRegisteredPleaseCheckAndTryAgain =>
'This email is not registered. Please check and try again.';
@override
String get youLlReceiveACodeViaEmailToResetYourPassword =>
'You\'ll receive a code via email to reset your password.';
@override
String get codeFromEmail => 'Code from email';
@override
String get sendResetEmail => 'Send reset email';
@override
String get invalidCode => 'Invalid code';
@override
String get paswordHasBeenChanged => 'Pasword has been changed';
@override
String get copiedSuccessfully => 'Copied successfully';
}
... ...
... ... @@ -870,9 +870,6 @@ class AppLocalizationsZh extends AppLocalizations {
String get securityEmailHint => '请输入邮箱地址';
@override
String get sendVerificationEmail => '发送验证邮件';
@override
String get confirmYourEmail => '验证你的邮箱';
@override
... ... @@ -2344,4 +2341,91 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get sendUsYourUserIdAsItWillHelpUsIdentifyTheProblemFaster =>
'请将您的用户 ID 发送给我们,这将有助于我们更快地查明问题所在。';
@override
String get setAPassword => '设置密码';
@override
String get setAPasswordToSignInWithYourEmail => '设置密码,以便使用您的电子邮箱登录。';
@override
String get settingsSaved => '设置已保存';
@override
String get leave => '离开';
@override
String get setPassword => '设置密码';
@override
String
get setAPasswordToAddThisEmailSuccessfullyLeavingNowWillCancelThisSetup =>
'请设置密码以成功添加此电子邮箱。如果现在退出,将取消此设置。';
@override
String get setupIncomplete => '设置未完成';
@override
String get enterYourPassword => '请输入密码';
@override
String get confirmNewPassword => '确认新密码';
@override
String
get passwordMustBeAtLeast6CharactersAndInclude1NumberAnd1UppercaseLetter =>
'密码必须至少包含 6 个字符,并包含 1 个数字和 1 个大写字母。';
@override
String get forgotPassword => '忘记密码了吗?';
@override
String get enterYourEmailAndPassword => '请输入您的电子邮箱和密码';
@override
String get newPassword => '新密码';
@override
String get weVeSentACodeTo => '我们已向 ';
@override
String get didnTGetItCheckYourSpamFolderOrTryAgain =>
'. 没收到吗?请检查您的垃圾邮件文件夹或重试一下。';
@override
String get checkYourEmail => '请查看您的电子邮件';
@override
String get code => '代码';
@override
String get resetPassword => '重置密码';
@override
String get havingTroubleContactUs => '遇到问题了吗?请联系我们';
@override
String get sendEmail => '发送电子邮件';
@override
String get thisEmailIsNotRegisteredPleaseCheckAndTryAgain => '该邮箱未注册。请检查后重试。';
@override
String get youLlReceiveACodeViaEmailToResetYourPassword =>
'您将通过电子邮件收到一个用于重置密码的验证码。';
@override
String get codeFromEmail => '来自电子邮件的代码';
@override
String get sendResetEmail => '发送重置邮件';
@override
String get invalidCode => '无效代码';
@override
String get paswordHasBeenChanged => '密码已更改';
@override
String get copiedSuccessfully => '复制成功';
}
... ...