More code refractor in auth/accounts section

This commit is contained in:
AmanRajSinghMourya
2025-08-01 11:48:04 +05:30
parent 57569e79fe
commit 6f94d91afb
10 changed files with 37 additions and 2084 deletions

View File

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

View File

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

View File

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

View File

@@ -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)),
],
);
}
}

View File

@@ -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,
),
),
),
),
],
),
),
],
),
),
),
],
);
}
}

View File

@@ -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,
),
),
),
],
),
),
],
),
],
);
// );
}
}

View File

@@ -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,
);
}
}
}
}

View File

@@ -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(),
),
);
},

View File

@@ -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,
),
),
),
),
),
],
),
),
);
}
}

View File

@@ -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(),
);
},
),