More code refractor in auth/accounts section
This commit is contained in:
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_accounts/pages/email_entry_page.dart';
|
||||
import 'package:ente_accounts/pages/login_page.dart';
|
||||
import 'package:ente_accounts/pages/password_entry_page.dart';
|
||||
import 'package:ente_auth/app/view/app.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
@@ -9,9 +11,7 @@ import 'package:ente_auth/events/trigger_logout_event.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/locale.dart';
|
||||
import 'package:ente_auth/theme/text_style.dart';
|
||||
import 'package:ente_auth/ui/account/login_page.dart';
|
||||
import 'package:ente_auth/ui/account/logout_dialog.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
||||
import 'package:ente_auth/ui/common/gradient_button.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
@@ -265,8 +265,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
// No key
|
||||
if (Configuration.instance.getKeyAttributes() == null) {
|
||||
// Never had a key
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
page = PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.set,
|
||||
const HomePage(),
|
||||
);
|
||||
} else if (Configuration.instance.getKey() == null) {
|
||||
// Yet to decrypt the key
|
||||
@@ -288,13 +290,15 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
void _navigateToSignInPage() {
|
||||
Widget page;
|
||||
if (Configuration.instance.getEncryptedToken() == null) {
|
||||
page = const LoginPage();
|
||||
page = LoginPage(Configuration.instance);
|
||||
} else {
|
||||
// No key
|
||||
if (Configuration.instance.getKeyAttributes() == null) {
|
||||
// Never had a key
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
page = PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.set,
|
||||
const HomePage(),
|
||||
);
|
||||
} else if (Configuration.instance.getKey() == null) {
|
||||
// Yet to decrypt the key
|
||||
|
||||
@@ -10,21 +10,21 @@ import 'package:ente_accounts/models/set_keys_request.dart';
|
||||
import 'package:ente_accounts/models/set_recovery_key_request.dart';
|
||||
import 'package:ente_accounts/models/two_factor.dart';
|
||||
import 'package:ente_accounts/models/user_details.dart';
|
||||
import 'package:ente_accounts/pages/login_page.dart';
|
||||
import 'package:ente_accounts/pages/ott_verification_page.dart';
|
||||
import 'package:ente_accounts/pages/passkey_page.dart';
|
||||
import 'package:ente_accounts/pages/password_entry_page.dart';
|
||||
import 'package:ente_accounts/pages/two_factor_authentication_page.dart';
|
||||
import 'package:ente_accounts/pages/two_factor_recovery_page.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/models/api/user/srp.dart';
|
||||
import 'package:ente_auth/ui/account/login_page.dart';
|
||||
import 'package:ente_auth/ui/account/ott_verification_page.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/models/api/user/srp.dart';
|
||||
import 'package:ente_auth/ui/account/password_reentry_page.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_page.dart';
|
||||
import 'package:ente_auth/ui/common/progress_dialog.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/passkey_page.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_base/models/key_attributes.dart';
|
||||
@@ -405,6 +405,7 @@ class UserService {
|
||||
}
|
||||
if (passkeySessionID.isNotEmpty) {
|
||||
page = PasskeyPage(
|
||||
Configuration.instance,
|
||||
passkeySessionID,
|
||||
totp2FASessionID: twoFASessionID,
|
||||
accountsUrl: accountsUrl,
|
||||
@@ -420,8 +421,10 @@ class UserService {
|
||||
page = const PasswordReentryPage();
|
||||
}
|
||||
} else {
|
||||
page = const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.set,
|
||||
page = PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.set,
|
||||
const HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -721,6 +724,7 @@ class UserService {
|
||||
Configuration.instance.setVolatilePassword(userPassword);
|
||||
if (passkeySessionID.isNotEmpty) {
|
||||
page = PasskeyPage(
|
||||
Configuration.instance,
|
||||
passkeySessionID,
|
||||
totp2FASessionID: twoFASessionID,
|
||||
accountsUrl: accountsUrl,
|
||||
@@ -838,7 +842,7 @@ class UserService {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
return LoginPage(Configuration.instance);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
@@ -904,7 +908,7 @@ class UserService {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
return LoginPage(Configuration.instance);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
@@ -1002,7 +1006,7 @@ class UserService {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
return LoginPage(Configuration.instance);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
|
||||
@@ -1,516 +0,0 @@
|
||||
// import 'package:email_validator/email_validator.dart';
|
||||
// import 'package:ente_auth/core/configuration.dart';
|
||||
// import 'package:ente_auth/ente_theme_data.dart';
|
||||
// import 'package:ente_auth/l10n/l10n.dart';
|
||||
// import 'package:ente_auth/services/user_service.dart';
|
||||
// import 'package:ente_auth/theme/ente_theme.dart';
|
||||
// import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
// import 'package:ente_auth/utils/platform_util.dart';
|
||||
// import 'package:ente_auth/utils/toast_util.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter/services.dart';
|
||||
// import 'package:password_strength/password_strength.dart';
|
||||
// import 'package:step_progress_indicator/step_progress_indicator.dart';
|
||||
// import "package:styled_text/styled_text.dart";
|
||||
|
||||
// class EmailEntryPage extends StatefulWidget {
|
||||
// const EmailEntryPage({super.key});
|
||||
|
||||
// @override
|
||||
// State<EmailEntryPage> createState() => _EmailEntryPageState();
|
||||
// }
|
||||
|
||||
// class _EmailEntryPageState extends State<EmailEntryPage> {
|
||||
// static const kMildPasswordStrengthThreshold = 0.4;
|
||||
// static const kStrongPasswordStrengthThreshold = 0.7;
|
||||
|
||||
// final _config = Configuration.instance;
|
||||
// final _passwordController1 = TextEditingController();
|
||||
// final _passwordController2 = TextEditingController();
|
||||
// final Color _validFieldValueColor = const Color.fromARGB(51, 157, 45, 194);
|
||||
|
||||
// String? _email;
|
||||
// String? _password;
|
||||
// String _cnfPassword = '';
|
||||
// String _referralSource = '';
|
||||
// double _passwordStrength = 0.0;
|
||||
// bool _emailIsValid = false;
|
||||
// bool _hasAgreedToTOS = true;
|
||||
// bool _hasAgreedToE2E = false;
|
||||
// bool _password1Visible = false;
|
||||
// bool _password2Visible = false;
|
||||
// bool _passwordsMatch = false;
|
||||
|
||||
// final _password1FocusNode = FocusNode();
|
||||
// final _password2FocusNode = FocusNode();
|
||||
// bool _password1InFocus = false;
|
||||
// bool _password2InFocus = false;
|
||||
// bool _passwordIsValid = false;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// _email = _config.getEmail();
|
||||
// _password1FocusNode.addListener(() {
|
||||
// setState(() {
|
||||
// _password1InFocus = _password1FocusNode.hasFocus;
|
||||
// });
|
||||
// });
|
||||
// _password2FocusNode.addListener(() {
|
||||
// setState(() {
|
||||
// _password2InFocus = _password2FocusNode.hasFocus;
|
||||
// });
|
||||
// });
|
||||
// super.initState();
|
||||
// }
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
|
||||
|
||||
// FloatingActionButtonLocation? fabLocation() {
|
||||
// if (isKeypadOpen) {
|
||||
// return null;
|
||||
// } else {
|
||||
// return FloatingActionButtonLocation.centerFloat;
|
||||
// }
|
||||
// }
|
||||
|
||||
// final appBar = AppBar(
|
||||
// elevation: 0,
|
||||
// leading: IconButton(
|
||||
// icon: const Icon(Icons.arrow_back),
|
||||
// color: Theme.of(context).iconTheme.color,
|
||||
// onPressed: () {
|
||||
// Navigator.of(context).pop();
|
||||
// },
|
||||
// ),
|
||||
// title: Material(
|
||||
// type: MaterialType.transparency,
|
||||
// child: StepProgressIndicator(
|
||||
// totalSteps: 4,
|
||||
// currentStep: 1,
|
||||
// selectedColor: Theme.of(context).colorScheme.alternativeColor,
|
||||
// roundedEdges: const Radius.circular(10),
|
||||
// unselectedColor:
|
||||
// Theme.of(context).colorScheme.stepProgressUnselectedColor,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// return Scaffold(
|
||||
// resizeToAvoidBottomInset: isKeypadOpen,
|
||||
// appBar: appBar,
|
||||
// body: _getBody(),
|
||||
// floatingActionButton: DynamicFAB(
|
||||
// isKeypadOpen: isKeypadOpen,
|
||||
// isFormValid: _isFormValid(),
|
||||
// buttonText: context.l10n.createAccount,
|
||||
// onPressedFunction: () {
|
||||
// UserService.instance.setEmail(_email!);
|
||||
// _config.setVolatilePassword(_passwordController1.text);
|
||||
// UserService.instance.setRefSource(_referralSource);
|
||||
// UserService.instance.sendOtt(
|
||||
// context,
|
||||
// _email!,
|
||||
// isCreateAccountScreen: true,
|
||||
// purpose: "signup",
|
||||
// );
|
||||
// FocusScope.of(context).unfocus();
|
||||
// },
|
||||
// ),
|
||||
// floatingActionButtonLocation: fabLocation(),
|
||||
// floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Widget _getBody() {
|
||||
// var passwordStrengthText = context.l10n.weakStrength;
|
||||
// var passwordStrengthColor = Colors.redAccent;
|
||||
// if (_passwordStrength > kStrongPasswordStrengthThreshold) {
|
||||
// passwordStrengthText = context.l10n.strongStrength;
|
||||
// passwordStrengthColor = Colors.greenAccent;
|
||||
// } else if (_passwordStrength > kMildPasswordStrengthThreshold) {
|
||||
// passwordStrengthText = context.l10n.moderateStrength;
|
||||
// passwordStrengthColor = Colors.orangeAccent;
|
||||
// }
|
||||
// return Column(
|
||||
// children: [
|
||||
// Expanded(
|
||||
// child: AutofillGroup(
|
||||
// child: ListView(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
// child: Text(
|
||||
// context.l10n.createNewAccount,
|
||||
// style: Theme.of(context).textTheme.headlineMedium,
|
||||
// ),
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
// child: TextFormField(
|
||||
// style: Theme.of(context).textTheme.titleMedium,
|
||||
// autofillHints: const [AutofillHints.email],
|
||||
// decoration: InputDecoration(
|
||||
// fillColor: _emailIsValid ? _validFieldValueColor : null,
|
||||
// filled: true,
|
||||
// hintText: context.l10n.email,
|
||||
// contentPadding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16,
|
||||
// vertical: 14,
|
||||
// ),
|
||||
// border: UnderlineInputBorder(
|
||||
// borderSide: BorderSide.none,
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// ),
|
||||
// suffixIcon: _emailIsValid
|
||||
// ? Icon(
|
||||
// Icons.check,
|
||||
// size: 20,
|
||||
// color: Theme.of(context)
|
||||
// .inputDecorationTheme
|
||||
// .focusedBorder!
|
||||
// .borderSide
|
||||
// .color,
|
||||
// )
|
||||
// : null,
|
||||
// ),
|
||||
// onChanged: (value) {
|
||||
// _email = value.trim();
|
||||
// if (_emailIsValid != EmailValidator.validate(_email!)) {
|
||||
// setState(() {
|
||||
// _emailIsValid = EmailValidator.validate(_email!);
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// autocorrect: false,
|
||||
// keyboardType: TextInputType.emailAddress,
|
||||
// //initialValue: _email,
|
||||
// textInputAction: TextInputAction.next,
|
||||
// ),
|
||||
// ),
|
||||
// const Padding(padding: EdgeInsets.all(4)),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
// child: TextFormField(
|
||||
// keyboardType: TextInputType.text,
|
||||
// textInputAction: TextInputAction.next,
|
||||
// controller: _passwordController1,
|
||||
// obscureText: !_password1Visible,
|
||||
// enableSuggestions: true,
|
||||
// autofillHints: const [AutofillHints.newPassword],
|
||||
// decoration: InputDecoration(
|
||||
// fillColor:
|
||||
// _passwordIsValid ? _validFieldValueColor : null,
|
||||
// filled: true,
|
||||
// hintText: context.l10n.password,
|
||||
// contentPadding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16,
|
||||
// vertical: 14,
|
||||
// ),
|
||||
// suffixIcon: _password1InFocus
|
||||
// ? IconButton(
|
||||
// icon: Icon(
|
||||
// _password1Visible
|
||||
// ? Icons.visibility
|
||||
// : Icons.visibility_off,
|
||||
// color: Theme.of(context).iconTheme.color,
|
||||
// size: 20,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// setState(() {
|
||||
// _password1Visible = !_password1Visible;
|
||||
// });
|
||||
// },
|
||||
// )
|
||||
// : _passwordIsValid
|
||||
// ? Icon(
|
||||
// Icons.check,
|
||||
// color: Theme.of(context)
|
||||
// .inputDecorationTheme
|
||||
// .focusedBorder!
|
||||
// .borderSide
|
||||
// .color,
|
||||
// )
|
||||
// : null,
|
||||
// border: UnderlineInputBorder(
|
||||
// borderSide: BorderSide.none,
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// ),
|
||||
// ),
|
||||
// focusNode: _password1FocusNode,
|
||||
// onChanged: (password) {
|
||||
// if (password != _password) {
|
||||
// setState(() {
|
||||
// _password = password;
|
||||
// _passwordStrength =
|
||||
// estimatePasswordStrength(password);
|
||||
// _passwordIsValid = _passwordStrength >=
|
||||
// kMildPasswordStrengthThreshold;
|
||||
// _passwordsMatch = _password == _cnfPassword;
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
// onEditingComplete: () {
|
||||
// _password1FocusNode.unfocus();
|
||||
// _password2FocusNode.requestFocus();
|
||||
// TextInput.finishAutofillContext();
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 8),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
// child: TextFormField(
|
||||
// keyboardType: TextInputType.visiblePassword,
|
||||
// controller: _passwordController2,
|
||||
// obscureText: !_password2Visible,
|
||||
// autofillHints: const [AutofillHints.newPassword],
|
||||
// onEditingComplete: () => TextInput.finishAutofillContext(),
|
||||
// decoration: InputDecoration(
|
||||
// fillColor: _passwordsMatch && _passwordIsValid
|
||||
// ? _validFieldValueColor
|
||||
// : null,
|
||||
// filled: true,
|
||||
// hintText: context.l10n.confirmPassword,
|
||||
// contentPadding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16,
|
||||
// vertical: 14,
|
||||
// ),
|
||||
// suffixIcon: _password2InFocus
|
||||
// ? IconButton(
|
||||
// icon: Icon(
|
||||
// _password2Visible
|
||||
// ? Icons.visibility
|
||||
// : Icons.visibility_off,
|
||||
// color: Theme.of(context).iconTheme.color,
|
||||
// size: 20,
|
||||
// ),
|
||||
// onPressed: () {
|
||||
// setState(() {
|
||||
// _password2Visible = !_password2Visible;
|
||||
// });
|
||||
// },
|
||||
// )
|
||||
// : _passwordsMatch
|
||||
// ? Icon(
|
||||
// Icons.check,
|
||||
// color: Theme.of(context)
|
||||
// .inputDecorationTheme
|
||||
// .focusedBorder!
|
||||
// .borderSide
|
||||
// .color,
|
||||
// )
|
||||
// : null,
|
||||
// border: UnderlineInputBorder(
|
||||
// borderSide: BorderSide.none,
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// ),
|
||||
// ),
|
||||
// focusNode: _password2FocusNode,
|
||||
// onChanged: (cnfPassword) {
|
||||
// setState(() {
|
||||
// _cnfPassword = cnfPassword;
|
||||
// if (_password != null || _password != '') {
|
||||
// _passwordsMatch = _password == _cnfPassword;
|
||||
// }
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// Opacity(
|
||||
// opacity: (_password != '') && _password1InFocus ? 1 : 0,
|
||||
// child: Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
// child: Text(
|
||||
// context.l10n.passwordStrength(passwordStrengthText),
|
||||
// style: TextStyle(
|
||||
// color: passwordStrengthColor,
|
||||
// fontWeight: FontWeight.w500,
|
||||
// fontSize: 12,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
// Padding(
|
||||
// padding:
|
||||
// const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
|
||||
// child: Text(
|
||||
// context.l10n.hearUsWhereTitle,
|
||||
// style: getEnteTextTheme(context).smallFaint,
|
||||
// ),
|
||||
// ),
|
||||
// const SizedBox(height: 4),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
// child: TextFormField(
|
||||
// style: Theme.of(context).textTheme.titleMedium,
|
||||
// decoration: InputDecoration(
|
||||
// fillColor: null,
|
||||
// filled: true,
|
||||
// contentPadding: const EdgeInsets.symmetric(
|
||||
// horizontal: 16,
|
||||
// vertical: 14,
|
||||
// ),
|
||||
// border: UnderlineInputBorder(
|
||||
// borderSide: BorderSide.none,
|
||||
// borderRadius: BorderRadius.circular(6),
|
||||
// ),
|
||||
// suffixIcon: InkWell(
|
||||
// onTap: () {
|
||||
// showToast(
|
||||
// context,
|
||||
// context.l10n.hearUsExplanation,
|
||||
// );
|
||||
// },
|
||||
// child: Icon(
|
||||
// Icons.info_outline_rounded,
|
||||
// color: getEnteColorScheme(context).strokeMuted,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// onChanged: (value) {
|
||||
// _referralSource = value.trim();
|
||||
// },
|
||||
// autocorrect: false,
|
||||
// keyboardType: TextInputType.text,
|
||||
// textInputAction: TextInputAction.next,
|
||||
// ),
|
||||
// ),
|
||||
// const Divider(thickness: 1),
|
||||
// const SizedBox(height: 12),
|
||||
// _getAgreement(),
|
||||
// const SizedBox(height: 40),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// }
|
||||
|
||||
// Container _getAgreement() {
|
||||
// return Container(
|
||||
// padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
// child: Column(
|
||||
// children: [
|
||||
// _getTOSAgreement(),
|
||||
// _getPasswordAgreement(),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Widget _getTOSAgreement() {
|
||||
// return GestureDetector(
|
||||
// onTap: () {
|
||||
// setState(() {
|
||||
// _hasAgreedToTOS = !_hasAgreedToTOS;
|
||||
// });
|
||||
// },
|
||||
// behavior: HitTestBehavior.translucent,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Checkbox(
|
||||
// value: _hasAgreedToTOS,
|
||||
// side: CheckboxTheme.of(context).side,
|
||||
// onChanged: (value) {
|
||||
// setState(() {
|
||||
// _hasAgreedToTOS = value!;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: StyledText(
|
||||
// text: context.l10n.signUpTerms,
|
||||
// style: Theme.of(context)
|
||||
// .textTheme
|
||||
// .titleMedium!
|
||||
// .copyWith(fontSize: 12),
|
||||
// tags: {
|
||||
// 'u-terms': StyledTextActionTag(
|
||||
// (String? text, Map<String?, String?> attrs) =>
|
||||
// PlatformUtil.openWebView(
|
||||
// context,
|
||||
// context.l10n.termsOfServicesTitle,
|
||||
// "https://ente.io/terms",
|
||||
// ),
|
||||
// style: const TextStyle(
|
||||
// decoration: TextDecoration.underline,
|
||||
// ),
|
||||
// ),
|
||||
// 'u-policy': StyledTextActionTag(
|
||||
// (String? text, Map<String?, String?> attrs) =>
|
||||
// PlatformUtil.openWebView(
|
||||
// context,
|
||||
// context.l10n.privacyPolicyTitle,
|
||||
// "https://ente.io/privacy",
|
||||
// ),
|
||||
// style: const TextStyle(
|
||||
// decoration: TextDecoration.underline,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// Widget _getPasswordAgreement() {
|
||||
// return GestureDetector(
|
||||
// onTap: () {
|
||||
// setState(() {
|
||||
// _hasAgreedToE2E = !_hasAgreedToE2E;
|
||||
// });
|
||||
// },
|
||||
// behavior: HitTestBehavior.translucent,
|
||||
// child: Row(
|
||||
// children: [
|
||||
// Checkbox(
|
||||
// value: _hasAgreedToE2E,
|
||||
// side: CheckboxTheme.of(context).side,
|
||||
// onChanged: (value) {
|
||||
// setState(() {
|
||||
// _hasAgreedToE2E = value!;
|
||||
// });
|
||||
// },
|
||||
// ),
|
||||
// Expanded(
|
||||
// child: StyledText(
|
||||
// text: context.l10n.ackPasswordLostWarning,
|
||||
// style: Theme.of(context)
|
||||
// .textTheme
|
||||
// .titleMedium!
|
||||
// .copyWith(fontSize: 12),
|
||||
// tags: {
|
||||
// 'underline': StyledTextActionTag(
|
||||
// (String? text, Map<String?, String?> attrs) =>
|
||||
// PlatformUtil.openWebView(
|
||||
// context,
|
||||
// context.l10n.encryption,
|
||||
// "https://ente.io/architecture",
|
||||
// ),
|
||||
// style: const TextStyle(
|
||||
// decoration: TextDecoration.underline,
|
||||
// ),
|
||||
// ),
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
|
||||
// bool _isFormValid() {
|
||||
// return _emailIsValid &&
|
||||
// _passwordsMatch &&
|
||||
// _hasAgreedToTOS &&
|
||||
// _hasAgreedToE2E &&
|
||||
// _passwordIsValid;
|
||||
// }
|
||||
// }
|
||||
@@ -1,227 +0,0 @@
|
||||
import 'package:email_validator/email_validator.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/api/user/srp.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/account/login_pwd_verification_page.dart';
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:styled_text/styled_text.dart";
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _config = Configuration.instance;
|
||||
bool _emailIsValid = false;
|
||||
String? _email;
|
||||
Color? _emailInputFieldColor;
|
||||
final Logger _logger = Logger('_LoginPageState');
|
||||
|
||||
Future<void> onPressed() async {
|
||||
await UserService.instance.setEmail(_email!);
|
||||
Configuration.instance.resetVolatilePassword();
|
||||
SrpAttributes? attr;
|
||||
bool isEmailVerificationEnabled = true;
|
||||
try {
|
||||
attr = await UserService.instance.getSrpAttributes(_email!);
|
||||
isEmailVerificationEnabled = attr.isEmailMFAEnabled;
|
||||
} catch (e) {
|
||||
if (e is! SrpSetupNotCompleteError) {
|
||||
_logger.severe('Error getting SRP attributes', e);
|
||||
}
|
||||
}
|
||||
if (attr != null && !isEmailVerificationEnabled) {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return LoginPasswordVerificationPage(
|
||||
srpAttributes: attr!,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
_email!,
|
||||
isCreateAccountScreen: false,
|
||||
purpose: 'login',
|
||||
);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_email = _config.getEmail();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
|
||||
|
||||
FloatingActionButtonLocation? fabLocation() {
|
||||
if (isKeypadOpen) {
|
||||
return null;
|
||||
} else {
|
||||
return FloatingActionButtonLocation.centerFloat;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: isKeypadOpen,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
floatingActionButton: DynamicFAB(
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: _emailIsValid,
|
||||
buttonText: context.l10n.logInLabel,
|
||||
onPressedFunction: onPressed,
|
||||
),
|
||||
floatingActionButtonLocation: fabLocation(),
|
||||
floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AutofillGroup(
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
|
||||
child: Text(
|
||||
l10n.welcomeBack,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||||
child: TextFormField(
|
||||
autofillHints: const [AutofillHints.email],
|
||||
onFieldSubmitted:
|
||||
_emailIsValid ? (value) => onPressed() : null,
|
||||
decoration: InputDecoration(
|
||||
fillColor: _emailInputFieldColor,
|
||||
filled: true,
|
||||
hintText: l10n.email,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 15,
|
||||
vertical: 15,
|
||||
),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _emailIsValid
|
||||
? Icon(
|
||||
Icons.check,
|
||||
size: 20,
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder!
|
||||
.borderSide
|
||||
.color,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_email = value.trim();
|
||||
_emailIsValid = EmailValidator.validate(_email!);
|
||||
if (_emailIsValid) {
|
||||
_emailInputFieldColor =
|
||||
const Color.fromARGB(51, 157, 45, 194);
|
||||
} else {
|
||||
_emailInputFieldColor = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
//initialValue: _email,
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 18),
|
||||
child: Divider(
|
||||
thickness: 1,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: StyledText(
|
||||
text: context.l10n.loginTerms,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 12),
|
||||
tags: {
|
||||
'u-terms': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) =>
|
||||
PlatformUtil.openWebView(
|
||||
context,
|
||||
context.l10n.termsOfServicesTitle,
|
||||
"https://ente.io/terms",
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
'u-policy': StyledTextActionTag(
|
||||
(String? text, Map<String?, String?> attrs) =>
|
||||
PlatformUtil.openWebView(
|
||||
context,
|
||||
context.l10n.privacyPolicyTitle,
|
||||
"https://ente.io/privacy",
|
||||
),
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
import "package:dio/dio.dart";
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import "package:ente_auth/models/api/user/srp.dart";
|
||||
import "package:ente_auth/services/user_service.dart";
|
||||
import "package:ente_auth/theme/ente_theme.dart";
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
import "package:ente_auth/ui/components/buttons/button_widget.dart";
|
||||
import "package:ente_auth/utils/dialog_util.dart";
|
||||
import "package:ente_auth/utils/email_util.dart";
|
||||
import "package:ente_crypto_dart/ente_crypto_dart.dart";
|
||||
import 'package:flutter/material.dart';
|
||||
import "package:logging/logging.dart";
|
||||
|
||||
// LoginPasswordVerificationPage is a page that allows the user to enter their password to verify their identity.
|
||||
// If the password is correct, then the user is either directed to
|
||||
// PasswordReentryPage (if the user has not yet set up 2FA) or TwoFactorAuthenticationPage (if the user has set up 2FA).
|
||||
// In the PasswordReentryPage, the password is auto-filled based on the
|
||||
// volatile password.
|
||||
class LoginPasswordVerificationPage extends StatefulWidget {
|
||||
final SrpAttributes srpAttributes;
|
||||
const LoginPasswordVerificationPage({super.key, required this.srpAttributes});
|
||||
|
||||
@override
|
||||
State<LoginPasswordVerificationPage> createState() =>
|
||||
_LoginPasswordVerificationPageState();
|
||||
}
|
||||
|
||||
class _LoginPasswordVerificationPageState
|
||||
extends State<LoginPasswordVerificationPage> {
|
||||
final _logger = Logger((_LoginPasswordVerificationPageState).toString());
|
||||
final _passwordController = TextEditingController();
|
||||
final FocusNode _passwordFocusNode = FocusNode();
|
||||
String? email;
|
||||
bool _passwordInFocus = false;
|
||||
bool _passwordVisible = false;
|
||||
|
||||
Future<void> onPressed() async {
|
||||
FocusScope.of(context).unfocus();
|
||||
await verifyPassword(context, _passwordController.text);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
email = Configuration.instance.getEmail();
|
||||
_passwordFocusNode.addListener(() {
|
||||
setState(() {
|
||||
_passwordInFocus = _passwordFocusNode.hasFocus;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
|
||||
|
||||
FloatingActionButtonLocation? fabLocation() {
|
||||
if (isKeypadOpen) {
|
||||
return null;
|
||||
} else {
|
||||
return FloatingActionButtonLocation.centerFloat;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: isKeypadOpen,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
floatingActionButton: DynamicFAB(
|
||||
key: const ValueKey("verifyPasswordButton"),
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: _passwordController.text.isNotEmpty,
|
||||
buttonText: context.l10n.logInLabel,
|
||||
onPressedFunction: onPressed,
|
||||
),
|
||||
floatingActionButtonLocation: fabLocation(),
|
||||
floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> verifyPassword(BuildContext context, String password) async {
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
context.l10n.pleaseWait,
|
||||
isDismissible: true,
|
||||
);
|
||||
await dialog.show();
|
||||
try {
|
||||
await UserService.instance.verifyEmailViaPassword(
|
||||
context,
|
||||
widget.srpAttributes,
|
||||
password,
|
||||
dialog,
|
||||
);
|
||||
} on DioException catch (e, s) {
|
||||
await dialog.hide();
|
||||
if (e.response != null && e.response!.statusCode == 401) {
|
||||
_logger.severe('server reject, failed verify SRP login', e, s);
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.incorrectPasswordTitle,
|
||||
context.l10n.pleaseTryAgain,
|
||||
);
|
||||
} else {
|
||||
_logger.severe('API failure during SRP login', e, s);
|
||||
if (e.type == DioExceptionType.connectionError) {
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.noInternetConnection,
|
||||
context.l10n.pleaseCheckYourInternetConnectionAndTryAgain,
|
||||
);
|
||||
} else {
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.oops,
|
||||
context.l10n.verificationFailedPleaseTryAgain,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.info('error during loginViaPassword', e);
|
||||
await dialog.hide();
|
||||
if (e is LoginKeyDerivationError) {
|
||||
_logger.severe('loginKey derivation error', e, s);
|
||||
// LoginKey err, perform regular login via ott verification
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
email!,
|
||||
isCreateAccountScreen: true,
|
||||
);
|
||||
return;
|
||||
} else if (e is KeyDerivationError) {
|
||||
// device is not powerful enough to perform derive key
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: context.l10n.recreatePasswordTitle,
|
||||
body: context.l10n.recreatePasswordBody,
|
||||
firstButtonLabel: context.l10n.useRecoveryKey,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
email!,
|
||||
isResetPasswordScreen: true,
|
||||
);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
_logger.severe('unexpected error while verifying password', e, s);
|
||||
await _showContactSupportDialog(
|
||||
context,
|
||||
context.l10n.oops,
|
||||
context.l10n.verificationFailedPleaseTryAgain,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showContactSupportDialog(
|
||||
BuildContext context,
|
||||
String title,
|
||||
String message,
|
||||
) async {
|
||||
final dialogChoice = await showChoiceDialog(
|
||||
context,
|
||||
title: title,
|
||||
body: message,
|
||||
firstButtonLabel: context.l10n.contactSupport,
|
||||
secondButtonLabel: context.l10n.ok,
|
||||
);
|
||||
if (dialogChoice!.action == ButtonAction.first) {
|
||||
await sendLogs(
|
||||
context,
|
||||
context.l10n.contactSupport,
|
||||
postShare: () {},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AutofillGroup(
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
|
||||
child: Text(
|
||||
context.l10n.enterPassword,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: 30,
|
||||
left: 22,
|
||||
right: 20,
|
||||
),
|
||||
child: Text(
|
||||
email ?? '',
|
||||
style: getEnteTextTheme(context).smallMuted,
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
// hidden textForm for suggesting auto-fill service for saving
|
||||
// password
|
||||
visible: false,
|
||||
child: TextFormField(
|
||||
autofillHints: const [
|
||||
AutofillHints.email,
|
||||
],
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
initialValue: email,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
|
||||
child: TextFormField(
|
||||
onFieldSubmitted: _passwordController.text.isNotEmpty
|
||||
? (_) => onPressed()
|
||||
: null,
|
||||
key: const ValueKey("passwordInputField"),
|
||||
autofillHints: const [AutofillHints.password],
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.enterYourPasswordHint,
|
||||
filled: true,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _passwordInFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_passwordVisible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_passwordVisible = !_passwordVisible;
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
),
|
||||
controller: _passwordController,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
obscureText: !_passwordVisible,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
focusNode: _passwordFocusNode,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 18),
|
||||
child: Divider(
|
||||
thickness: 1,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
await UserService.instance.sendOtt(
|
||||
context,
|
||||
email!,
|
||||
isResetPasswordScreen: true,
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.l10n.forgotPassword,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
final dialog = createProgressDialog(
|
||||
context,
|
||||
context.l10n.pleaseWait,
|
||||
);
|
||||
await dialog.show();
|
||||
await Configuration.instance.logout();
|
||||
await dialog.hide();
|
||||
Navigator.of(context)
|
||||
.popUntil((route) => route.isFirst);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.l10n.changeEmail,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:step_progress_indicator/step_progress_indicator.dart';
|
||||
import 'package:styled_text/styled_text.dart';
|
||||
|
||||
class OTTVerificationPage extends StatefulWidget {
|
||||
final String email;
|
||||
final bool isChangeEmail;
|
||||
final bool isCreateAccountScreen;
|
||||
final bool isResetPasswordScreen;
|
||||
|
||||
const OTTVerificationPage(
|
||||
this.email, {
|
||||
this.isChangeEmail = false,
|
||||
this.isCreateAccountScreen = false,
|
||||
this.isResetPasswordScreen = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OTTVerificationPage> createState() => _OTTVerificationPageState();
|
||||
}
|
||||
|
||||
class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
final _verificationCodeController = TextEditingController();
|
||||
|
||||
Future<void> onPressed() async {
|
||||
if (widget.isChangeEmail) {
|
||||
await UserService.instance.changeEmail(
|
||||
context,
|
||||
widget.email,
|
||||
_verificationCodeController.text,
|
||||
);
|
||||
} else {
|
||||
await UserService.instance.verifyEmail(
|
||||
context,
|
||||
_verificationCodeController.text,
|
||||
isResettingPasswordScreen: widget.isResetPasswordScreen,
|
||||
);
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
|
||||
|
||||
FloatingActionButtonLocation? fabLocation() {
|
||||
if (isKeypadOpen) {
|
||||
return null;
|
||||
} else {
|
||||
return FloatingActionButtonLocation.centerFloat;
|
||||
}
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: isKeypadOpen,
|
||||
appBar: AppBar(
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
title: widget.isCreateAccountScreen
|
||||
? Material(
|
||||
type: MaterialType.transparency,
|
||||
child: StepProgressIndicator(
|
||||
totalSteps: 4,
|
||||
currentStep: 2,
|
||||
selectedColor: Theme.of(context).colorScheme.alternativeColor,
|
||||
roundedEdges: const Radius.circular(10),
|
||||
unselectedColor:
|
||||
Theme.of(context).colorScheme.stepProgressUnselectedColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: _getBody(),
|
||||
floatingActionButton: DynamicFAB(
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: _verificationCodeController.text.isNotEmpty,
|
||||
buttonText: l10n.verify,
|
||||
onPressedFunction: onPressed,
|
||||
),
|
||||
floatingActionButtonLocation: fabLocation(),
|
||||
floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final l10n = context.l10n;
|
||||
return ListView(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 30, 20, 15),
|
||||
child: Text(
|
||||
l10n.verifyEmail,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
|
||||
child: StyledText(
|
||||
text: l10n.weHaveSendEmailTo(widget.email),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
tags: {
|
||||
'green': StyledTextTag(
|
||||
style: TextStyle(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.alternativeColor,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
widget.isResetPasswordScreen
|
||||
? Text(
|
||||
l10n.toResetVerifyEmail,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
)
|
||||
: Text(
|
||||
l10n.checkInboxAndSpamFolder,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.2,
|
||||
height: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||
child: TextFormField(
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
onFieldSubmitted: _verificationCodeController.text.isNotEmpty
|
||||
? (_) => onPressed()
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
hintText: l10n.tapToEnterCode,
|
||||
contentPadding: const EdgeInsets.all(15),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
controller: _verificationCodeController,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
thickness: 1,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
UserService.instance.sendOtt(
|
||||
context,
|
||||
widget.email,
|
||||
isCreateAccountScreen: widget.isCreateAccountScreen,
|
||||
isChangeEmail: widget.isChangeEmail,
|
||||
isResetPasswordScreen: widget.isResetPasswordScreen,
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
l10n.resendEmail,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
// );
|
||||
}
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_key_page.dart';
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/platform_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:password_strength/password_strength.dart';
|
||||
import 'package:styled_text/styled_text.dart';
|
||||
|
||||
enum PasswordEntryMode {
|
||||
set,
|
||||
update,
|
||||
reset,
|
||||
}
|
||||
|
||||
class PasswordEntryPage extends StatefulWidget {
|
||||
final PasswordEntryMode mode;
|
||||
|
||||
const PasswordEntryPage({required this.mode, super.key});
|
||||
|
||||
@override
|
||||
State<PasswordEntryPage> createState() => _PasswordEntryPageState();
|
||||
}
|
||||
|
||||
class _PasswordEntryPageState extends State<PasswordEntryPage> {
|
||||
static const kMildPasswordStrengthThreshold = 0.4;
|
||||
static const kStrongPasswordStrengthThreshold = 0.7;
|
||||
|
||||
final _logger = Logger((_PasswordEntryPageState).toString());
|
||||
final _passwordController1 = TextEditingController(),
|
||||
_passwordController2 = TextEditingController();
|
||||
final Color _validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2);
|
||||
String? _volatilePassword;
|
||||
String _passwordInInputBox = '';
|
||||
String _passwordInInputConfirmationBox = '';
|
||||
double _passwordStrength = 0.0;
|
||||
bool _password1Visible = false;
|
||||
bool _password2Visible = false;
|
||||
final _password1FocusNode = FocusNode();
|
||||
final _password2FocusNode = FocusNode();
|
||||
bool _password1InFocus = false;
|
||||
bool _password2InFocus = false;
|
||||
|
||||
bool _passwordsMatch = false;
|
||||
bool _isPasswordValid = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_volatilePassword = Configuration.instance.getVolatilePassword();
|
||||
if (_volatilePassword != null) {
|
||||
Future.delayed(
|
||||
Duration.zero,
|
||||
() => _showRecoveryCodeDialog(
|
||||
_volatilePassword!,
|
||||
usingVolatilePassword: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
_password1FocusNode.addListener(() {
|
||||
setState(() {
|
||||
_password1InFocus = _password1FocusNode.hasFocus;
|
||||
});
|
||||
});
|
||||
_password2FocusNode.addListener(() {
|
||||
setState(() {
|
||||
_password2InFocus = _password2FocusNode.hasFocus;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
|
||||
|
||||
FloatingActionButtonLocation? fabLocation() {
|
||||
if (isKeypadOpen) {
|
||||
return null;
|
||||
} else {
|
||||
return FloatingActionButtonLocation.centerFloat;
|
||||
}
|
||||
}
|
||||
|
||||
String title = context.l10n.setPasswordTitle;
|
||||
if (widget.mode == PasswordEntryMode.update) {
|
||||
title = context.l10n.changePasswordTitle;
|
||||
} else if (widget.mode == PasswordEntryMode.reset) {
|
||||
title = context.l10n.resetPasswordTitle;
|
||||
} else if (_volatilePassword != null) {
|
||||
title = context.l10n.encryptionKeys;
|
||||
}
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: isKeypadOpen,
|
||||
appBar: AppBar(
|
||||
leading: widget.mode == PasswordEntryMode.reset
|
||||
? Container()
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
body: _getBody(title),
|
||||
floatingActionButton: DynamicFAB(
|
||||
isKeypadOpen: isKeypadOpen,
|
||||
isFormValid: _passwordsMatch && _isPasswordValid,
|
||||
buttonText: title,
|
||||
onPressedFunction: () {
|
||||
if (widget.mode == PasswordEntryMode.set) {
|
||||
_showRecoveryCodeDialog(_passwordController1.text);
|
||||
} else {
|
||||
_updatePassword();
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
),
|
||||
floatingActionButtonLocation: fabLocation(),
|
||||
floatingActionButtonAnimator: NoScalingAnimation(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody(String buttonTextAndHeading) {
|
||||
final email = Configuration.instance.getEmail();
|
||||
var passwordStrengthText = context.l10n.weakStrength;
|
||||
var passwordStrengthColor = Colors.redAccent;
|
||||
if (_passwordStrength > kStrongPasswordStrengthThreshold) {
|
||||
passwordStrengthText = context.l10n.strongStrength;
|
||||
passwordStrengthColor = Colors.greenAccent;
|
||||
} else if (_passwordStrength > kMildPasswordStrengthThreshold) {
|
||||
passwordStrengthText = context.l10n.moderateStrength;
|
||||
passwordStrengthColor = Colors.orangeAccent;
|
||||
}
|
||||
if (_volatilePassword != null) {
|
||||
return Container();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AutofillGroup(
|
||||
child: FocusTraversalGroup(
|
||||
policy: OrderedTraversalPolicy(),
|
||||
child: ListView(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 30,
|
||||
horizontal: 20,
|
||||
),
|
||||
child: Text(
|
||||
buttonTextAndHeading,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
widget.mode == PasswordEntryMode.set
|
||||
? context.l10n.enterPasswordToEncrypt
|
||||
: context.l10n.enterNewPasswordToEncrypt,
|
||||
textAlign: TextAlign.start,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: StyledText(
|
||||
text: context.l10n.passwordWarning,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontSize: 14),
|
||||
tags: {
|
||||
'underline': StyledTextTag(
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(12)),
|
||||
Visibility(
|
||||
// hidden textForm for suggesting auto-fill service for saving
|
||||
// password
|
||||
visible: false,
|
||||
child: TextFormField(
|
||||
autofillHints: const [
|
||||
AutofillHints.email,
|
||||
],
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
initialValue: email,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: TextFormField(
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
onFieldSubmitted: (_) {
|
||||
do {
|
||||
FocusScope.of(context).nextFocus();
|
||||
} while (FocusScope.of(context).focusedChild!.context ==
|
||||
null);
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
fillColor:
|
||||
_isPasswordValid ? _validFieldValueColor : null,
|
||||
filled: true,
|
||||
hintText: context.l10n.password,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
suffixIcon: _password1InFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_password1Visible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_password1Visible = !_password1Visible;
|
||||
});
|
||||
},
|
||||
)
|
||||
: _isPasswordValid
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder!
|
||||
.borderSide
|
||||
.color,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
obscureText: !_password1Visible,
|
||||
controller: _passwordController1,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
onChanged: (password) {
|
||||
setState(() {
|
||||
_passwordInInputBox = password;
|
||||
_passwordStrength =
|
||||
estimatePasswordStrength(password);
|
||||
_isPasswordValid = _passwordStrength >=
|
||||
kMildPasswordStrengthThreshold;
|
||||
_passwordsMatch = _passwordInInputBox ==
|
||||
_passwordInInputConfirmationBox;
|
||||
});
|
||||
},
|
||||
textInputAction: TextInputAction.next,
|
||||
focusNode: _password1FocusNode,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: TextFormField(
|
||||
keyboardType: TextInputType.visiblePassword,
|
||||
controller: _passwordController2,
|
||||
obscureText: !_password2Visible,
|
||||
autofillHints: const [AutofillHints.newPassword],
|
||||
onEditingComplete: () =>
|
||||
TextInput.finishAutofillContext(),
|
||||
decoration: InputDecoration(
|
||||
fillColor:
|
||||
_passwordsMatch ? _validFieldValueColor : null,
|
||||
filled: true,
|
||||
hintText: context.l10n.confirmPassword,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 20,
|
||||
),
|
||||
suffixIcon: _password2InFocus
|
||||
? IconButton(
|
||||
icon: Icon(
|
||||
_password2Visible
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_password2Visible = !_password2Visible;
|
||||
});
|
||||
},
|
||||
)
|
||||
: _passwordsMatch
|
||||
? Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder!
|
||||
.borderSide
|
||||
.color,
|
||||
)
|
||||
: null,
|
||||
border: UnderlineInputBorder(
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
focusNode: _password2FocusNode,
|
||||
onChanged: (cnfPassword) {
|
||||
setState(() {
|
||||
_passwordInInputConfirmationBox = cnfPassword;
|
||||
if (_passwordInInputBox != '') {
|
||||
_passwordsMatch = _passwordInInputBox ==
|
||||
_passwordInInputConfirmationBox;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Opacity(
|
||||
opacity: (_passwordInInputBox != '') && _password1InFocus
|
||||
? 1
|
||||
: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Text(
|
||||
context.l10n.passwordStrength(passwordStrengthText),
|
||||
style: TextStyle(
|
||||
color: passwordStrengthColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
PlatformUtil.openWebView(
|
||||
context,
|
||||
context.l10n.howItWorks,
|
||||
"https://ente.io/architecture",
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: context.l10n.howItWorks,
|
||||
style:
|
||||
Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(20)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _updatePassword() async {
|
||||
final logOutFromOthers = await logOutFromOtherDevices(context);
|
||||
final dialog =
|
||||
createProgressDialog(context, context.l10n.generatingEncryptionKeys);
|
||||
await dialog.show();
|
||||
try {
|
||||
final result = await Configuration.instance
|
||||
.getAttributesForNewPassword(_passwordController1.text);
|
||||
await UserService.instance.updateKeyAttributes(
|
||||
result.item1,
|
||||
result.item2,
|
||||
logoutOtherDevices: logOutFromOthers,
|
||||
);
|
||||
await dialog.hide();
|
||||
showShortToast(context, context.l10n.passwordChangedSuccessfully);
|
||||
Navigator.of(context).pop();
|
||||
if (widget.mode == PasswordEntryMode.reset) {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> logOutFromOtherDevices(BuildContext context) async {
|
||||
bool logOutFromOther = true;
|
||||
await showChoiceDialog(
|
||||
context,
|
||||
title: context.l10n.signOutFromOtherDevices,
|
||||
body: context.l10n.signOutOtherBody,
|
||||
isDismissible: false,
|
||||
firstButtonLabel: context.l10n.signOutOtherDevices,
|
||||
firstButtonType: ButtonType.critical,
|
||||
firstButtonOnTap: () async {
|
||||
logOutFromOther = true;
|
||||
},
|
||||
secondButtonLabel: context.l10n.doNotSignOut,
|
||||
secondButtonOnTap: () async {
|
||||
logOutFromOther = false;
|
||||
},
|
||||
);
|
||||
return logOutFromOther;
|
||||
}
|
||||
|
||||
Future<void> _showRecoveryCodeDialog(
|
||||
String password, {
|
||||
bool usingVolatilePassword = false,
|
||||
}) async {
|
||||
final l10n = context.l10n;
|
||||
final dialog =
|
||||
createProgressDialog(context, l10n.generatingEncryptionKeysTitle);
|
||||
await dialog.show();
|
||||
try {
|
||||
if (usingVolatilePassword) {
|
||||
_logger.info('Using volatile password');
|
||||
}
|
||||
final result =
|
||||
await Configuration.instance.generateKey(password);
|
||||
Configuration.instance.resetVolatilePassword();
|
||||
await dialog.hide();
|
||||
onDone() async {
|
||||
final dialog = createProgressDialog(context, l10n.pleaseWait);
|
||||
await dialog.show();
|
||||
try {
|
||||
await UserService.instance.setAttributes(result);
|
||||
await dialog.hide();
|
||||
Configuration.instance.resetVolatilePassword();
|
||||
// ignore: unawaited_futures
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const HomePage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
await dialog.hide();
|
||||
// ignore: unawaited_futures
|
||||
showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unawaited_futures
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
result.privateKeyAttributes.recoveryKey,
|
||||
context.l10n.continueLabel,
|
||||
showAppBar: false,
|
||||
isDismissible: false,
|
||||
onDone: onDone,
|
||||
showProgressBar: true,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
await dialog.hide();
|
||||
if (e is UnsupportedError) {
|
||||
// ignore: unawaited_futures
|
||||
showErrorDialog(
|
||||
context,
|
||||
context.l10n.insecureDevice,
|
||||
context.l10n.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease,
|
||||
);
|
||||
} else {
|
||||
// ignore: unawaited_futures
|
||||
showGenericErrorDialog(
|
||||
context: context,
|
||||
error: e,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:ente_accounts/pages/password_entry_page.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/common/dynamic_fab.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -27,10 +28,12 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
||||
await Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PopScope(
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: PasswordEntryPage(
|
||||
mode: PasswordEntryMode.reset,
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.reset,
|
||||
const HomePage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,235 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:ente_accounts/models/two_factor.dart';
|
||||
import 'package:ente_accounts/pages/two_factor_authentication_page.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
|
||||
import 'package:ente_auth/ui/components/models/button_type.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class PasskeyPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
final String totp2FASessionID;
|
||||
final String accountsUrl;
|
||||
|
||||
const PasskeyPage(
|
||||
this.sessionID, {
|
||||
required this.totp2FASessionID,
|
||||
required this.accountsUrl,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PasskeyPage> createState() => _PasskeyPageState();
|
||||
}
|
||||
|
||||
class _PasskeyPageState extends State<PasskeyPage> {
|
||||
final Logger _logger = Logger("PasskeyPage");
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
launchPasskey();
|
||||
_initDeepLinks();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> launchPasskey() async {
|
||||
await launchUrlString(
|
||||
"${widget.accountsUrl}/passkeys/verify?"
|
||||
"passkeySessionID=${widget.sessionID}"
|
||||
"&redirect=enteauth://passkey"
|
||||
"&clientPackage=io.ente.auth",
|
||||
mode: LaunchMode.externalApplication,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> checkStatus() async {
|
||||
late dynamic response;
|
||||
try {
|
||||
response = await UserService.instance
|
||||
.getTokenForPasskeySession(widget.sessionID);
|
||||
} on PassKeySessionNotVerifiedError {
|
||||
showToast(context, context.l10n.passKeyPendingVerification);
|
||||
return;
|
||||
} on PassKeySessionExpiredError {
|
||||
await showErrorDialog(
|
||||
context,
|
||||
context.l10n.loginSessionExpired,
|
||||
context.l10n.loginSessionExpiredDetails,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
} catch (e, s) {
|
||||
_logger.severe("failed to check status", e, s);
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
return;
|
||||
}
|
||||
await UserService.instance.onPassKeyVerified(context, response);
|
||||
}
|
||||
|
||||
Future<void> _handleDeeplink(String? link) async {
|
||||
if (!context.mounted ||
|
||||
Configuration.instance.hasConfiguredAccount() ||
|
||||
link == null) {
|
||||
_logger.warning(
|
||||
'ignored deeplink: contextMounted ${context.mounted} hasConfiguredAccount ${Configuration.instance.hasConfiguredAccount()}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) {
|
||||
if (Configuration.instance.isLoggedIn()) {
|
||||
_logger.info('ignored deeplink: already configured');
|
||||
showToast(context, 'Account is already configured.');
|
||||
return;
|
||||
}
|
||||
final parsedUri = Uri.parse(link);
|
||||
final sessionID = parsedUri.queryParameters['passkeySessionID'];
|
||||
if (sessionID != widget.sessionID) {
|
||||
showToast(context, "Session ID mismatch");
|
||||
_logger.warning('ignored deeplink: sessionID mismatch');
|
||||
return;
|
||||
}
|
||||
final String? authResponse = parsedUri.queryParameters['response'];
|
||||
String base64String = authResponse!.toString();
|
||||
while (base64String.length % 4 != 0) {
|
||||
base64String += '=';
|
||||
}
|
||||
final res = utf8.decode(base64.decode(base64String));
|
||||
final json = jsonDecode(res) as Map<String, dynamic>;
|
||||
await UserService.instance.onPassKeyVerified(context, json);
|
||||
} else {
|
||||
_logger.info('ignored deeplink: $link mounted $mounted');
|
||||
}
|
||||
} catch (e, s) {
|
||||
_logger.severe('passKey: failed to handle deeplink', e, s);
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> _initDeepLinks() async {
|
||||
final appLinks = AppLinks();
|
||||
// Attach a listener to the stream
|
||||
appLinks.stringLinkStream.listen(
|
||||
_handleDeeplink,
|
||||
onError: (err) {
|
||||
_logger.severe(err);
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
l10n.passkeyAuthTitle,
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.waitingForVerification,
|
||||
style: const TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.primary,
|
||||
labelText: context.l10n.tryAgain,
|
||||
onTap: () => launchPasskey(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ButtonWidget(
|
||||
buttonType: ButtonType.secondary,
|
||||
labelText: context.l10n.checkStatus,
|
||||
onTap: () async {
|
||||
try {
|
||||
await checkStatus();
|
||||
} catch (e) {
|
||||
debugPrint('failed to check status %e');
|
||||
showGenericErrorDialog(context: context, error: e).ignore();
|
||||
}
|
||||
},
|
||||
shouldSurfaceExecutionStates: true,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
if (widget.totp2FASessionID.isNotEmpty)
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
routeToPage(
|
||||
context,
|
||||
TwoFactorAuthenticationPage(
|
||||
widget.totp2FASessionID,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.l10n.loginWithTOTP,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
UserService.instance.recoverTwoFactor(
|
||||
context,
|
||||
widget.sessionID,
|
||||
TwoFactorType.passkey,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: Text(
|
||||
context.l10n.recoverAccount,
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'package:ente_accounts/pages/change_email_dialog.dart';
|
||||
import 'package:ente_accounts/pages/delete_account_page.dart';
|
||||
import 'package:ente_accounts/pages/password_entry_page.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/l10n/l10n.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
import 'package:ente_auth/ui/account/recovery_key_page.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
@@ -81,8 +82,10 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordEntryPage(
|
||||
mode: PasswordEntryMode.update,
|
||||
return PasswordEntryPage(
|
||||
Configuration.instance,
|
||||
PasswordEntryMode.update,
|
||||
const HomePage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user