diff --git a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart index cdcaed49a..40928a0af 100644 --- a/lib/pages_desktop_specific/home/settings_menu/security_settings.dart +++ b/lib/pages_desktop_specific/home/settings_menu/security_settings.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/src/widgets/framework.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/home/settings_menu/backup_and_restore/enable_backup_dialog.dart'; import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:zxcvbn/zxcvbn.dart'; class SecuritySettings extends ConsumerStatefulWidget { const SecuritySettings({Key? key}) : super(key: key); @@ -18,21 +21,55 @@ class SecuritySettings extends ConsumerStatefulWidget { } class _SecuritySettings extends ConsumerState { - Future enableAutoBackup() async { - // wait for keyboard to disappear - FocusScope.of(context).unfocus(); - await Future.delayed( - const Duration(milliseconds: 100), - ); + late bool changePassword = false; - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return EnableBackupDialog(); - }, - ); + late final TextEditingController passwordCurrentController; + late final TextEditingController passwordController; + late final TextEditingController passwordRepeatController; + + late final FocusNode passwordCurrentFocusNode; + late final FocusNode passwordFocusNode; + late final FocusNode passwordRepeatFocusNode; + final zxcvbn = Zxcvbn(); + + bool hidePassword = true; + bool shouldShowPasswordHint = true; + + double passwordStrength = 0.0; + + bool get shouldEnableSave { + return passwordCurrentController.text.isNotEmpty && + passwordController.text.isNotEmpty && + passwordRepeatController.text.isNotEmpty; + } + + String passwordFeedback = + "Add another word or two. Uncommon words are better. Use a few words, avoid common phrases. No need for symbols, digits, or uppercase letters."; + + @override + void initState() { + passwordCurrentController = TextEditingController(); + passwordController = TextEditingController(); + passwordRepeatController = TextEditingController(); + + passwordCurrentFocusNode = FocusNode(); + passwordFocusNode = FocusNode(); + passwordRepeatFocusNode = FocusNode(); + + super.initState(); + } + + @override + void dispose() { + passwordCurrentController.dispose(); + passwordController.dispose(); + passwordRepeatController.dispose(); + + passwordCurrentFocusNode.dispose(); + passwordFocusNode.dispose(); + passwordRepeatFocusNode.dispose(); + + super.dispose(); } @override @@ -78,12 +115,290 @@ class _SecuritySettings extends ConsumerState { ), Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Padding( padding: EdgeInsets.all( 10, ), - child: NewPasswordButton(), + child: changePassword + ? SizedBox( + width: 512, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldKey"), + focusNode: passwordFocusNode, + controller: passwordController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter current password", + passwordFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityRestoreFromFilePasswordFieldShowPasswordButtonKey"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) {}, + ), + ), + const SizedBox(height: 16), + Text( + "New password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey1"), + focusNode: passwordCurrentFocusNode, + controller: passwordCurrentController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Enter new password", + passwordCurrentFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey1"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + if (newValue.isEmpty) { + setState(() { + passwordFeedback = ""; + }); + return; + } + final result = + zxcvbn.evaluate(newValue); + String suggestionsAndTips = ""; + for (var sug in result + .feedback.suggestions! + .toSet()) { + suggestionsAndTips += "$sug\n"; + } + suggestionsAndTips += + result.feedback.warning!; + String feedback = + // "Password Strength: ${((result.score! / 4.0) * 100).toInt()}%\n" + suggestionsAndTips; + + passwordStrength = result.score! / 4; + + // hack fix to format back string returned from zxcvbn + if (feedback + .contains("phrasesNo need")) { + feedback = feedback.replaceFirst( + "phrasesNo need", + "phrases\nNo need"); + } + + if (feedback.endsWith("\n")) { + feedback = feedback.substring( + 0, feedback.length - 2); + } + + setState(() { + passwordFeedback = feedback; + }); + }, + ), + ), + const SizedBox(height: 16), + Text( + "Confirm new password", + style: + STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark3), + textAlign: TextAlign.left, + ), + const SizedBox(height: 10), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key( + "desktopSecurityCreateNewPasswordFieldKey2"), + focusNode: passwordRepeatFocusNode, + controller: passwordRepeatController, + style: STextStyles.field(context), + obscureText: hidePassword, + enableSuggestions: false, + autocorrect: false, + decoration: standardInputDecoration( + "Confirm new password", + passwordRepeatFocusNode, + context, + ).copyWith( + labelStyle: + STextStyles.fieldLabel(context), + suffixIcon: UnconstrainedBox( + child: Row( + children: [ + const SizedBox( + width: 16, + ), + GestureDetector( + key: const Key( + "desktopSecurityCreateNewPasswordButtonKey2"), + onTap: () async { + setState(() { + hidePassword = + !hidePassword; + }); + }, + child: SvgPicture.asset( + hidePassword + ? Assets.svg.eye + : Assets.svg.eyeSlash, + color: Theme.of(context) + .extension()! + .textDark3, + width: 16, + height: 16, + ), + ), + const SizedBox( + width: 12, + ), + ], + ), + ), + ), + onChanged: (newValue) { + setState(() {}); + }, + ), + ), + const SizedBox(height: 20), + PrimaryButton( + width: 142, + desktopMed: true, + enabled: shouldEnableSave, + label: "Save changes", + onPressed: () { + setState(() { + changePassword = false; + }); + }, + ) + ], + ), + ) + : PrimaryButton( + width: 192, + desktopMed: true, + enabled: true, + label: "Set up new password", + onPressed: () { + setState(() { + changePassword = true; + }); + }, + ), ), ], ), @@ -110,29 +425,7 @@ class NewPasswordButton extends ConsumerWidget { style: Theme.of(context) .extension()! .getPrimaryEnabledButtonColor(context), - onPressed: () { - // Expandable( - // header: Row( - // mainAxisAlignment: MainAxisAlignment.start, - // children: [ - // NewPasswordButton(), - // ], - // ), - // body: Column( - // mainAxisAlignment: MainAxisAlignment.start, - // children: [ - // Text( - // "Current Password", - // style: STextStyles.desktopTextExtraSmall(context).copyWith( - // color: - // Theme.of(context).extension()!.textDark3, - // ), - // textAlign: TextAlign.left, - // ), - // ], - // ), - // ); - }, + onPressed: () {}, child: Text( "Set up new password", style: STextStyles.button(context),