From 648f68d6a6798bee1494ee25f7684c0e5e1ed75f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Mon, 14 Aug 2023 09:46:12 +0530 Subject: [PATCH] Page for password verification --- .../request_pwd_verification_page.dart | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 lib/ui/account/request_pwd_verification_page.dart diff --git a/lib/ui/account/request_pwd_verification_page.dart b/lib/ui/account/request_pwd_verification_page.dart new file mode 100644 index 0000000000..a4064ae125 --- /dev/null +++ b/lib/ui/account/request_pwd_verification_page.dart @@ -0,0 +1,272 @@ +import "dart:convert"; +import "dart:typed_data"; + +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/common/dynamic_fab.dart'; +import "package:ente_auth/utils/crypto_util.dart"; +import "package:ente_auth/utils/dialog_util.dart"; +import 'package:flutter/material.dart'; +import "package:flutter_sodium/flutter_sodium.dart"; +import "package:logging/logging.dart"; + +typedef OnPasswordVerifiedFn = Future Function(Uint8List bytes); + +class RequestPasswordVerificationPage extends StatefulWidget { + final OnPasswordVerifiedFn onPasswordVerified; + final Function? onPasswordError; + + const RequestPasswordVerificationPage( + {super.key, required this.onPasswordVerified, this.onPasswordError,}); + + @override + State createState() => + _RequestPasswordVerificationPageState(); +} + +class _RequestPasswordVerificationPageState + extends State { + final _logger = Logger((_RequestPasswordVerificationPageState).toString()); + final _passwordController = TextEditingController(); + final FocusNode _passwordFocusNode = FocusNode(); + String? email; + bool _passwordInFocus = false; + bool _passwordVisible = false; + + @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: () async { + FocusScope.of(context).unfocus(); + final dialog = createProgressDialog(context, context.l10n.pleaseWait); + dialog.show(); + try { + final attributes = Configuration.instance.getKeyAttributes()!; + final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( + utf8.encode(_passwordController.text) as Uint8List, + Sodium.base642bin(attributes.kekSalt), + attributes.memLimit, + attributes.opsLimit, + ); + CryptoUtil.decryptSync( + Sodium.base642bin(attributes.encryptedKey), + keyEncryptionKey, + Sodium.base642bin(attributes.keyDecryptionNonce), + ); + dialog.show(); + // pop + await widget.onPasswordVerified(keyEncryptionKey); + Navigator.of(context).pop(true); + } catch (e, s) { + _logger.severe("Error while verifying password", e, s); + dialog.hide(); + if (widget.onPasswordError != null) { + widget.onPasswordError!(); + } else { + showErrorDialog( + context, + context.l10n.incorrectPasswordTitle, + context.l10n.pleaseTryAgain, + ); + } + } + }, + ), + floatingActionButtonLocation: fabLocation(), + floatingActionButtonAnimator: NoScalingAnimation(), + ); + } + + 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( + 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, + ), + ), + ), + ), + ], + ), + ) + ], + ), + ), + ), + ], + ); + } +}