password_entry_page.dart 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import 'dart:ui';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:flutter/widgets.dart';
  5. import 'package:flutter_password_strength/flutter_password_strength.dart';
  6. import 'package:logging/logging.dart';
  7. import 'package:photos/core/configuration.dart';
  8. import 'package:photos/core/event_bus.dart';
  9. import 'package:photos/events/subscription_purchased_event.dart';
  10. import 'package:photos/services/billing_service.dart';
  11. import 'package:photos/services/user_service.dart';
  12. import 'package:photos/ui/common_elements.dart';
  13. import 'package:photos/ui/recovery_key_dialog.dart';
  14. import 'package:photos/ui/subscription_page.dart';
  15. import 'package:photos/ui/web_page.dart';
  16. import 'package:photos/utils/dialog_util.dart';
  17. import 'package:photos/utils/toast_util.dart';
  18. enum PasswordEntryMode {
  19. set,
  20. update,
  21. reset,
  22. }
  23. class PasswordEntryPage extends StatefulWidget {
  24. final PasswordEntryMode mode;
  25. PasswordEntryPage({this.mode = PasswordEntryMode.set, Key key})
  26. : super(key: key);
  27. @override
  28. _PasswordEntryPageState createState() => _PasswordEntryPageState();
  29. }
  30. class _PasswordEntryPageState extends State<PasswordEntryPage> {
  31. static const kPasswordStrengthThreshold = 0.4;
  32. final _logger = Logger("PasswordEntry");
  33. final _passwordController1 = TextEditingController(),
  34. _passwordController2 = TextEditingController();
  35. double _passwordStrength = 0;
  36. @override
  37. Widget build(BuildContext context) {
  38. String title = "set password";
  39. if (widget.mode == PasswordEntryMode.update) {
  40. title = "change password";
  41. } else if (widget.mode == PasswordEntryMode.reset) {
  42. title = "reset password";
  43. }
  44. return Scaffold(
  45. appBar: AppBar(
  46. title: Text(title),
  47. ),
  48. body: _getBody(title),
  49. resizeToAvoidBottomInset: false,
  50. );
  51. }
  52. Widget _getBody(String buttonText) {
  53. return Column(
  54. children: [
  55. FlutterPasswordStrength(
  56. password: _passwordController1.text,
  57. backgroundColor: Colors.grey[850],
  58. strengthCallback: (strength) {
  59. _passwordStrength = strength;
  60. },
  61. ),
  62. SingleChildScrollView(
  63. child: Container(
  64. padding: EdgeInsets.fromLTRB(16, 36, 16, 16),
  65. child: Column(
  66. children: [
  67. Padding(padding: EdgeInsets.all(12)),
  68. Text(
  69. "enter a" +
  70. (widget.mode != PasswordEntryMode.set ? " new " : " ") +
  71. "password we can use to encrypt your data",
  72. textAlign: TextAlign.center,
  73. style: TextStyle(
  74. height: 1.3,
  75. ),
  76. ),
  77. Padding(padding: EdgeInsets.all(8)),
  78. Text("we don't store this password, so if you forget, "),
  79. Text.rich(
  80. TextSpan(
  81. text: "we cannot decrypt your data",
  82. style: TextStyle(
  83. decoration: TextDecoration.underline,
  84. fontWeight: FontWeight.bold,
  85. )),
  86. style: TextStyle(
  87. height: 1.3,
  88. ),
  89. textAlign: TextAlign.center,
  90. ),
  91. Padding(padding: EdgeInsets.all(12)),
  92. Padding(
  93. padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
  94. child: TextFormField(
  95. decoration: InputDecoration(
  96. hintText: "password",
  97. contentPadding: EdgeInsets.all(20),
  98. ),
  99. controller: _passwordController1,
  100. autofocus: false,
  101. autocorrect: false,
  102. keyboardType: TextInputType.visiblePassword,
  103. onChanged: (_) {
  104. setState(() {});
  105. },
  106. ),
  107. ),
  108. Padding(padding: EdgeInsets.all(8)),
  109. Padding(
  110. padding: const EdgeInsets.fromLTRB(32, 0, 32, 0),
  111. child: TextFormField(
  112. decoration: InputDecoration(
  113. hintText: "password again",
  114. contentPadding: EdgeInsets.all(20),
  115. ),
  116. controller: _passwordController2,
  117. autofocus: false,
  118. autocorrect: false,
  119. obscureText: true,
  120. keyboardType: TextInputType.visiblePassword,
  121. onChanged: (_) {
  122. setState(() {});
  123. },
  124. ),
  125. ),
  126. Padding(padding: EdgeInsets.all(20)),
  127. Container(
  128. width: double.infinity,
  129. height: 64,
  130. padding: EdgeInsets.fromLTRB(40, 0, 40, 0),
  131. child: button(
  132. buttonText,
  133. fontSize: 18,
  134. onPressed: _passwordController1.text.isNotEmpty &&
  135. _passwordController2.text.isNotEmpty
  136. ? _onButtonPress
  137. : null,
  138. ),
  139. ),
  140. ],
  141. ),
  142. ),
  143. ),
  144. Expanded(child: Container()),
  145. GestureDetector(
  146. behavior: HitTestBehavior.translucent,
  147. onTap: () {
  148. Navigator.of(context).push(
  149. MaterialPageRoute(
  150. builder: (BuildContext context) {
  151. return WebPage("how it works", "https://ente.io/encryption");
  152. },
  153. ),
  154. );
  155. },
  156. child: Container(
  157. padding: EdgeInsets.all(40),
  158. child: RichText(
  159. text: TextSpan(
  160. text: "how it works",
  161. style: TextStyle(
  162. color: Colors.blue,
  163. fontFamily: 'Ubuntu',
  164. ),
  165. ),
  166. ),
  167. ),
  168. ),
  169. ],
  170. );
  171. }
  172. void _onButtonPress() {
  173. if (_passwordController1.text != _passwordController2.text) {
  174. showErrorDialog(
  175. context, "uhm...", "the passwords you entered don't match");
  176. } else if (_passwordStrength < kPasswordStrengthThreshold) {
  177. showErrorDialog(context, "weak password",
  178. "the password you have chosen is too simple, please choose another one");
  179. } else {
  180. if (widget.mode == PasswordEntryMode.set) {
  181. _showRecoveryCodeDialog();
  182. } else {
  183. _updatePassword();
  184. }
  185. }
  186. }
  187. void _updatePassword() async {
  188. final dialog = createProgressDialog(context, "please wait...");
  189. await dialog.show();
  190. try {
  191. final keyAttributes = await Configuration.instance
  192. .updatePassword(_passwordController1.text);
  193. await UserService.instance.updateKeyAttributes(keyAttributes);
  194. await dialog.hide();
  195. showToast("password changed successfully");
  196. Navigator.of(context).pop();
  197. if (widget.mode == PasswordEntryMode.reset) {
  198. if (!BillingService.instance.hasActiveSubscription()) {
  199. Navigator.of(context).pushReplacement(
  200. MaterialPageRoute(
  201. builder: (BuildContext context) {
  202. return SubscriptionPage(isOnboarding: true);
  203. },
  204. ),
  205. );
  206. } else {
  207. Bus.instance.fire(SubscriptionPurchasedEvent());
  208. Navigator.of(context).popUntil((route) => route.isFirst);
  209. }
  210. }
  211. } catch (e, s) {
  212. _logger.severe(e, s);
  213. await dialog.hide();
  214. showGenericErrorDialog(context);
  215. }
  216. }
  217. Future<void> _showRecoveryCodeDialog() async {
  218. final dialog =
  219. createProgressDialog(context, "generating encryption keys...");
  220. await dialog.show();
  221. try {
  222. final result =
  223. await Configuration.instance.generateKey(_passwordController1.text);
  224. await dialog.hide();
  225. final onDone = () async {
  226. final dialog = createProgressDialog(context, "please wait...");
  227. await dialog.show();
  228. try {
  229. await UserService.instance.setAttributes(result);
  230. await dialog.hide();
  231. Navigator.of(context).pushAndRemoveUntil(
  232. MaterialPageRoute(
  233. builder: (BuildContext context) {
  234. return SubscriptionPage(isOnboarding: true);
  235. },
  236. ),
  237. (route) => route.isFirst,
  238. );
  239. } catch (e, s) {
  240. Logger("PEP").severe(e, s);
  241. await dialog.hide();
  242. showGenericErrorDialog(context);
  243. }
  244. };
  245. showDialog(
  246. context: context,
  247. builder: (BuildContext context) {
  248. return RecoveryKeyDialog(
  249. result.privateKeyAttributes.recoveryKey, "continue", onDone);
  250. },
  251. barrierColor: Colors.black.withOpacity(0.85),
  252. barrierDismissible: false,
  253. );
  254. } catch (e) {
  255. await dialog.hide();
  256. showGenericErrorDialog(context);
  257. }
  258. }
  259. }