password_entry_page.dart 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:logging/logging.dart';
  4. import 'package:password_strength/password_strength.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/core/event_bus.dart';
  7. import 'package:photos/events/account_configured_event.dart';
  8. import 'package:photos/events/subscription_purchased_event.dart';
  9. import 'package:photos/services/user_service.dart';
  10. import 'package:photos/ui/common/dynamicFAB.dart';
  11. import 'package:photos/ui/payment/subscription.dart';
  12. import 'package:photos/ui/recovery_key_page.dart';
  13. import 'package:photos/ui/web_page.dart';
  14. import 'package:photos/utils/dialog_util.dart';
  15. import 'package:photos/utils/navigation_util.dart';
  16. import 'package:photos/utils/toast_util.dart';
  17. enum PasswordEntryMode {
  18. set,
  19. update,
  20. reset,
  21. }
  22. class PasswordEntryPage extends StatefulWidget {
  23. final PasswordEntryMode mode;
  24. PasswordEntryPage({this.mode = PasswordEntryMode.set, Key key})
  25. : super(key: key);
  26. @override
  27. _PasswordEntryPageState createState() => _PasswordEntryPageState();
  28. }
  29. class _PasswordEntryPageState extends State<PasswordEntryPage> {
  30. static const kMildPasswordStrengthThreshold = 0.4;
  31. static const kStrongPasswordStrengthThreshold = 0.7;
  32. final _logger = Logger((_PasswordEntryPageState).toString());
  33. final _passwordController1 = TextEditingController(),
  34. _passwordController2 = TextEditingController();
  35. final Color _validFieldValueColor = Color.fromRGBO(45, 194, 98, 0.2);
  36. String _volatilePassword;
  37. String _passwordInInputBox = '';
  38. String _passwordInInputConfirmationBox = '';
  39. double _passwordStrength = 0.0;
  40. bool _password1Visible = false;
  41. bool _password2Visible = false;
  42. final _password1FocusNode = FocusNode();
  43. final _password2FocusNode = FocusNode();
  44. bool _password1InFocus = false;
  45. bool _password2InFocus = false;
  46. bool _passwordsMatch = false;
  47. bool _isPasswordValid = false;
  48. @override
  49. void initState() {
  50. super.initState();
  51. _volatilePassword = Configuration.instance.getVolatilePassword();
  52. if (_volatilePassword != null) {
  53. Future.delayed(
  54. Duration.zero,
  55. () => _showRecoveryCodeDialog(_volatilePassword),
  56. );
  57. }
  58. _password1FocusNode.addListener(() {
  59. setState(() {
  60. _password1InFocus = _password1FocusNode.hasFocus;
  61. });
  62. });
  63. _password2FocusNode.addListener(() {
  64. setState(() {
  65. _password2InFocus = _password2FocusNode.hasFocus;
  66. });
  67. });
  68. }
  69. @override
  70. Widget build(BuildContext context) {
  71. final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 125;
  72. FloatingActionButtonLocation fabLocation() {
  73. if (isKeypadOpen) {
  74. return null;
  75. } else {
  76. return FloatingActionButtonLocation.centerFloat;
  77. }
  78. }
  79. String title = "Set password";
  80. if (widget.mode == PasswordEntryMode.update) {
  81. title = "Change password";
  82. } else if (widget.mode == PasswordEntryMode.reset) {
  83. title = "Reset password";
  84. } else if (_volatilePassword != null) {
  85. title = "Encryption keys";
  86. }
  87. return Scaffold(
  88. appBar: AppBar(
  89. leading: widget.mode == PasswordEntryMode.reset
  90. ? Container()
  91. : IconButton(
  92. icon: Icon(Icons.arrow_back),
  93. color: Theme.of(context).iconTheme.color,
  94. onPressed: () {
  95. Navigator.of(context).pop();
  96. },
  97. ),
  98. elevation: 0,
  99. ),
  100. body: _getBody(title),
  101. floatingActionButton: DynamicFAB(
  102. isKeypadOpen: isKeypadOpen,
  103. isFormValid: _passwordsMatch,
  104. buttonText: title,
  105. onPressedFunction: () {
  106. if (widget.mode == PasswordEntryMode.set) {
  107. _showRecoveryCodeDialog(_passwordController1.text);
  108. } else {
  109. _updatePassword();
  110. }
  111. },
  112. ),
  113. floatingActionButtonLocation: fabLocation(),
  114. floatingActionButtonAnimator: NoScalingAnimation(),
  115. );
  116. }
  117. Widget _getBody(String buttonTextAndHeading) {
  118. final email = Configuration.instance.getEmail();
  119. var passwordStrengthText = 'Weak';
  120. var passwordStrengthColor = Colors.redAccent;
  121. if (_passwordStrength > kStrongPasswordStrengthThreshold) {
  122. passwordStrengthText = 'Strong';
  123. passwordStrengthColor = Colors.greenAccent;
  124. } else if (_passwordStrength > kMildPasswordStrengthThreshold) {
  125. passwordStrengthText = 'Moderate';
  126. passwordStrengthColor = Colors.orangeAccent;
  127. }
  128. if (_volatilePassword != null) {
  129. return Container();
  130. }
  131. return Column(
  132. children: [
  133. Expanded(
  134. child: AutofillGroup(
  135. child: ListView(
  136. children: [
  137. Padding(
  138. padding:
  139. const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
  140. child: Text(
  141. buttonTextAndHeading,
  142. style: Theme.of(context).textTheme.headline4,
  143. ),
  144. ),
  145. Padding(
  146. padding: const EdgeInsets.symmetric(horizontal: 20),
  147. child: Text(
  148. "Enter a" +
  149. (widget.mode != PasswordEntryMode.set ? " new " : " ") +
  150. "password we can use to encrypt your data",
  151. textAlign: TextAlign.start,
  152. style: Theme.of(context)
  153. .textTheme
  154. .subtitle1
  155. .copyWith(fontSize: 14),
  156. ),
  157. ),
  158. Padding(padding: EdgeInsets.all(8)),
  159. Padding(
  160. padding: const EdgeInsets.symmetric(horizontal: 20),
  161. child: RichText(
  162. text: TextSpan(
  163. style: Theme.of(context)
  164. .textTheme
  165. .subtitle1
  166. .copyWith(fontSize: 14),
  167. children: [
  168. TextSpan(
  169. text:
  170. "We don't store this password, so if you forget, ",
  171. ),
  172. TextSpan(
  173. text: "we cannot decrypt your data",
  174. style: Theme.of(context).textTheme.subtitle1.copyWith(
  175. fontSize: 14,
  176. decoration: TextDecoration.underline,
  177. ),
  178. ),
  179. ],
  180. ),
  181. ),
  182. ),
  183. Padding(padding: EdgeInsets.all(12)),
  184. Visibility(
  185. // hidden textForm for suggesting auto-fill service for saving
  186. // password
  187. visible: false,
  188. child: TextFormField(
  189. autofillHints: const [
  190. AutofillHints.email,
  191. ],
  192. autocorrect: false,
  193. keyboardType: TextInputType.emailAddress,
  194. initialValue: email,
  195. textInputAction: TextInputAction.next,
  196. ),
  197. ),
  198. Padding(
  199. padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
  200. child: TextFormField(
  201. autofillHints: const [AutofillHints.newPassword],
  202. decoration: InputDecoration(
  203. fillColor:
  204. _isPasswordValid ? _validFieldValueColor : null,
  205. filled: true,
  206. hintText: "Password",
  207. contentPadding: EdgeInsets.all(20),
  208. border: UnderlineInputBorder(
  209. borderSide: BorderSide.none,
  210. borderRadius: BorderRadius.circular(6),
  211. ),
  212. suffixIcon: _password1InFocus
  213. ? IconButton(
  214. icon: Icon(
  215. _password1Visible
  216. ? Icons.visibility
  217. : Icons.visibility_off,
  218. color: Theme.of(context).iconTheme.color,
  219. size: 20,
  220. ),
  221. onPressed: () {
  222. setState(() {
  223. _password1Visible = !_password1Visible;
  224. });
  225. },
  226. )
  227. : _isPasswordValid
  228. ? Icon(
  229. Icons.check,
  230. color: Theme.of(context)
  231. .inputDecorationTheme
  232. .focusedBorder
  233. .borderSide
  234. .color,
  235. )
  236. : null,
  237. ),
  238. obscureText: !_password1Visible,
  239. controller: _passwordController1,
  240. autofocus: false,
  241. autocorrect: false,
  242. keyboardType: TextInputType.visiblePassword,
  243. onChanged: (password) {
  244. setState(() {
  245. _passwordInInputBox = password;
  246. _passwordStrength = estimatePasswordStrength(password);
  247. _isPasswordValid =
  248. _passwordStrength >= kMildPasswordStrengthThreshold;
  249. _passwordsMatch = _passwordInInputBox ==
  250. _passwordInInputConfirmationBox;
  251. });
  252. },
  253. textInputAction: TextInputAction.next,
  254. focusNode: _password1FocusNode,
  255. ),
  256. ),
  257. const SizedBox(height: 8),
  258. Padding(
  259. padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
  260. child: TextFormField(
  261. keyboardType: TextInputType.visiblePassword,
  262. controller: _passwordController2,
  263. obscureText: !_password2Visible,
  264. autofillHints: const [AutofillHints.newPassword],
  265. onEditingComplete: () => TextInput.finishAutofillContext(),
  266. decoration: InputDecoration(
  267. fillColor: _passwordsMatch ? _validFieldValueColor : null,
  268. filled: true,
  269. hintText: "Confirm password",
  270. contentPadding: EdgeInsets.symmetric(
  271. horizontal: 20,
  272. vertical: 20,
  273. ),
  274. suffixIcon: _password2InFocus
  275. ? IconButton(
  276. icon: Icon(
  277. _password2Visible
  278. ? Icons.visibility
  279. : Icons.visibility_off,
  280. color: Theme.of(context).iconTheme.color,
  281. size: 20,
  282. ),
  283. onPressed: () {
  284. setState(() {
  285. _password2Visible = !_password2Visible;
  286. });
  287. },
  288. )
  289. : _passwordsMatch
  290. ? Icon(
  291. Icons.check,
  292. color: Theme.of(context)
  293. .inputDecorationTheme
  294. .focusedBorder
  295. .borderSide
  296. .color,
  297. )
  298. : null,
  299. border: UnderlineInputBorder(
  300. borderSide: BorderSide.none,
  301. borderRadius: BorderRadius.circular(6),
  302. ),
  303. ),
  304. focusNode: _password2FocusNode,
  305. onChanged: (cnfPassword) {
  306. setState(() {
  307. _passwordInInputConfirmationBox = cnfPassword;
  308. if (_passwordInInputBox != null ||
  309. _passwordInInputBox != '') {
  310. _passwordsMatch = _passwordInInputBox ==
  311. _passwordInInputConfirmationBox;
  312. }
  313. });
  314. },
  315. ),
  316. ),
  317. Opacity(
  318. opacity:
  319. (_passwordInInputBox != '') && _password1InFocus ? 1 : 0,
  320. child: Padding(
  321. padding:
  322. const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
  323. child: Text(
  324. 'Password Strength: $passwordStrengthText',
  325. style: TextStyle(
  326. color: passwordStrengthColor,
  327. ),
  328. ),
  329. ),
  330. ),
  331. const SizedBox(height: 8),
  332. GestureDetector(
  333. behavior: HitTestBehavior.translucent,
  334. onTap: () {
  335. Navigator.of(context).push(
  336. MaterialPageRoute(
  337. builder: (BuildContext context) {
  338. return WebPage(
  339. "How it works",
  340. "https://ente.io/architecture",
  341. );
  342. },
  343. ),
  344. );
  345. },
  346. child: Container(
  347. padding: EdgeInsets.symmetric(horizontal: 20),
  348. child: RichText(
  349. text: TextSpan(
  350. text: "How it works",
  351. style: Theme.of(context).textTheme.subtitle1.copyWith(
  352. fontSize: 14,
  353. decoration: TextDecoration.underline,
  354. ),
  355. ),
  356. ),
  357. ),
  358. ),
  359. Padding(padding: EdgeInsets.all(20)),
  360. ],
  361. ),
  362. ),
  363. ),
  364. ],
  365. );
  366. }
  367. void _updatePassword() async {
  368. final dialog =
  369. createProgressDialog(context, "Generating encryption keys...");
  370. await dialog.show();
  371. try {
  372. final keyAttributes = await Configuration.instance
  373. .updatePassword(_passwordController1.text);
  374. await UserService.instance.updateKeyAttributes(keyAttributes);
  375. await dialog.hide();
  376. showShortToast(context, "Password changed successfully");
  377. Navigator.of(context).pop();
  378. if (widget.mode == PasswordEntryMode.reset) {
  379. Bus.instance.fire(SubscriptionPurchasedEvent());
  380. Navigator.of(context).popUntil((route) => route.isFirst);
  381. }
  382. } catch (e, s) {
  383. _logger.severe(e, s);
  384. await dialog.hide();
  385. showGenericErrorDialog(context);
  386. }
  387. }
  388. Future<void> _showRecoveryCodeDialog(String password) async {
  389. final dialog =
  390. createProgressDialog(context, "Generating encryption keys...");
  391. await dialog.show();
  392. try {
  393. final result = await Configuration.instance.generateKey(password);
  394. Configuration.instance.setVolatilePassword(null);
  395. await dialog.hide();
  396. onDone() async {
  397. final dialog = createProgressDialog(context, "Please wait...");
  398. await dialog.show();
  399. try {
  400. await UserService.instance.setAttributes(result);
  401. await dialog.hide();
  402. Bus.instance.fire(AccountConfiguredEvent());
  403. Navigator.of(context).pushAndRemoveUntil(
  404. MaterialPageRoute(
  405. builder: (BuildContext context) {
  406. return getSubscriptionPage(isOnBoarding: true);
  407. },
  408. ),
  409. (route) => route.isFirst,
  410. );
  411. } catch (e, s) {
  412. _logger.severe(e, s);
  413. await dialog.hide();
  414. showGenericErrorDialog(context);
  415. }
  416. }
  417. routeToPage(
  418. context,
  419. RecoveryKeyPage(
  420. result.privateKeyAttributes.recoveryKey,
  421. "Continue",
  422. showAppBar: false,
  423. isDismissible: false,
  424. onDone: onDone,
  425. showProgressBar: true,
  426. ),
  427. );
  428. } catch (e) {
  429. _logger.severe(e);
  430. await dialog.hide();
  431. if (e is UnsupportedError) {
  432. showErrorDialog(
  433. context,
  434. "Insecure device",
  435. "Sorry, we could not generate secure keys on this device.\n\nplease sign up from a different device.",
  436. );
  437. } else {
  438. showGenericErrorDialog(context);
  439. }
  440. }
  441. }
  442. }