password_reminder.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. import 'dart:ui';
  2. import 'package:flutter/material.dart';
  3. import 'package:logging/logging.dart';
  4. import 'package:pedantic/pedantic.dart';
  5. import 'package:photos/core/configuration.dart';
  6. import 'package:photos/ente_theme_data.dart';
  7. import 'package:photos/services/local_authentication_service.dart';
  8. import 'package:photos/services/user_remote_flag_service.dart';
  9. import 'package:photos/theme/colors.dart';
  10. import 'package:photos/theme/ente_theme.dart';
  11. import 'package:photos/ui/account/password_entry_page.dart';
  12. import 'package:photos/ui/common/gradient_button.dart';
  13. import 'package:photos/ui/home_widget.dart';
  14. import 'package:photos/utils/dialog_util.dart';
  15. import 'package:photos/utils/navigation_util.dart';
  16. class PasswordReminder extends StatefulWidget {
  17. const PasswordReminder({Key? key}) : super(key: key);
  18. @override
  19. State<PasswordReminder> createState() => _PasswordReminderState();
  20. }
  21. class _PasswordReminderState extends State<PasswordReminder> {
  22. final _passwordController = TextEditingController();
  23. final Logger _logger = Logger((_PasswordReminderState).toString());
  24. bool _password2Visible = false;
  25. bool _incorrectPassword = false;
  26. Future<void> _verifyRecoveryKey() async {
  27. final dialog = createProgressDialog(context, "Verifying password...");
  28. await dialog.show();
  29. try {
  30. final String inputKey = _passwordController.text;
  31. await Configuration.instance.verifyPassword(inputKey);
  32. await dialog.hide();
  33. UserRemoteFlagService.instance.stopPasswordReminder().ignore();
  34. // todo: change this as per figma once the component is ready
  35. await showErrorDialog(
  36. context,
  37. "Password verified",
  38. "Great! Thank you for verifying.\n"
  39. "\nPlease"
  40. " remember to keep your recovery key safely backed up.",
  41. );
  42. unawaited(
  43. Navigator.of(context).pushAndRemoveUntil(
  44. MaterialPageRoute(
  45. builder: (BuildContext context) {
  46. return const HomeWidget();
  47. },
  48. ),
  49. (route) => false,
  50. ),
  51. );
  52. } catch (e, s) {
  53. _logger.severe("failed to verify password", e, s);
  54. await dialog.hide();
  55. _incorrectPassword = true;
  56. if (mounted) {
  57. setState(() => {});
  58. }
  59. }
  60. }
  61. Future<void> _onChangePasswordClick() async {
  62. try {
  63. final hasAuthenticated =
  64. await LocalAuthenticationService.instance.requestLocalAuthentication(
  65. context,
  66. "Please authenticate to change your password",
  67. );
  68. if (hasAuthenticated) {
  69. UserRemoteFlagService.instance.stopPasswordReminder().ignore();
  70. await routeToPage(
  71. context,
  72. const PasswordEntryPage(
  73. mode: PasswordEntryMode.update,
  74. ),
  75. forceCustomPageRoute: true,
  76. );
  77. unawaited(
  78. Navigator.of(context).pushAndRemoveUntil(
  79. MaterialPageRoute(
  80. builder: (BuildContext context) {
  81. return const HomeWidget();
  82. },
  83. ),
  84. (route) => false,
  85. ),
  86. );
  87. }
  88. } catch (e) {
  89. showGenericErrorDialog(context: context);
  90. return;
  91. }
  92. }
  93. Future<void> _onSkipClick() async {
  94. final enteTextTheme = getEnteTextTheme(context);
  95. final enteColor = getEnteColorScheme(context);
  96. final content = Column(
  97. crossAxisAlignment: CrossAxisAlignment.start,
  98. mainAxisSize: MainAxisSize.min,
  99. children: [
  100. Text(
  101. "You will not be able to access your photos if you forget "
  102. "your password.\n\nIf you do not remember your password, "
  103. "now is a good time to change it.",
  104. style: enteTextTheme.body.copyWith(
  105. color: enteColor.textMuted,
  106. ),
  107. ),
  108. const Padding(padding: EdgeInsets.all(8)),
  109. SizedBox(
  110. width: double.infinity,
  111. height: 52,
  112. child: OutlinedButton(
  113. style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
  114. textStyle: MaterialStateProperty.resolveWith<TextStyle>(
  115. (Set<MaterialState> states) {
  116. return enteTextTheme.bodyBold;
  117. },
  118. ),
  119. ),
  120. onPressed: () async {
  121. Navigator.of(context, rootNavigator: true).pop('dialog');
  122. _onChangePasswordClick();
  123. },
  124. child: const Text(
  125. "Change password",
  126. ),
  127. ),
  128. ),
  129. const Padding(padding: EdgeInsets.all(8)),
  130. SizedBox(
  131. width: double.infinity,
  132. height: 52,
  133. child: OutlinedButton(
  134. style: Theme.of(context).outlinedButtonTheme.style?.copyWith(
  135. textStyle: MaterialStateProperty.resolveWith<TextStyle>(
  136. (Set<MaterialState> states) {
  137. return enteTextTheme.bodyBold;
  138. },
  139. ),
  140. backgroundColor: MaterialStateProperty.resolveWith<Color>(
  141. (Set<MaterialState> states) {
  142. return enteColor.fillFaint;
  143. },
  144. ),
  145. foregroundColor: MaterialStateProperty.resolveWith<Color>(
  146. (Set<MaterialState> states) {
  147. return Theme.of(context).colorScheme.defaultTextColor;
  148. },
  149. ),
  150. ),
  151. onPressed: () async {
  152. Navigator.of(context, rootNavigator: true).pop('dialog');
  153. },
  154. child: Text(
  155. "Cancel",
  156. style: enteTextTheme.bodyBold,
  157. ),
  158. ),
  159. )
  160. ],
  161. );
  162. return showDialog(
  163. context: context,
  164. builder: (BuildContext context) {
  165. return AlertDialog(
  166. backgroundColor: enteColor.backgroundElevated,
  167. title: Column(
  168. crossAxisAlignment: CrossAxisAlignment.start,
  169. children: [
  170. Icon(
  171. Icons.report_outlined,
  172. size: 36,
  173. color: getEnteColorScheme(context).strokeBase,
  174. ),
  175. ],
  176. ),
  177. content: content,
  178. );
  179. },
  180. barrierColor: enteColor.backdropFaint,
  181. );
  182. }
  183. @override
  184. void dispose() {
  185. _passwordController.dispose();
  186. super.dispose();
  187. }
  188. @override
  189. Widget build(BuildContext context) {
  190. final enteTheme = Theme.of(context).colorScheme.enteTheme;
  191. final List<Widget> actions = <Widget>[];
  192. actions.add(
  193. PopupMenuButton(
  194. itemBuilder: (context) {
  195. return [
  196. PopupMenuItem(
  197. value: 1,
  198. child: SizedBox(
  199. width: 120,
  200. height: 32,
  201. child: Row(
  202. mainAxisAlignment: MainAxisAlignment.start,
  203. crossAxisAlignment: CrossAxisAlignment.center,
  204. children: [
  205. const Icon(
  206. Icons.report_outlined,
  207. color: warning500,
  208. size: 20,
  209. ),
  210. const Padding(padding: EdgeInsets.symmetric(horizontal: 6)),
  211. Text(
  212. "Skip",
  213. style: getEnteTextTheme(context)
  214. .bodyBold
  215. .copyWith(color: warning500),
  216. ),
  217. ],
  218. ),
  219. ),
  220. ),
  221. ];
  222. },
  223. onSelected: (value) async {
  224. _onSkipClick();
  225. },
  226. ),
  227. );
  228. return Scaffold(
  229. appBar: AppBar(
  230. elevation: 0,
  231. leading: null,
  232. automaticallyImplyLeading: false,
  233. actions: actions,
  234. ),
  235. body: Padding(
  236. padding: const EdgeInsets.symmetric(horizontal: 20.0),
  237. child: LayoutBuilder(
  238. builder: (context, constraints) {
  239. return SingleChildScrollView(
  240. child: ConstrainedBox(
  241. constraints: BoxConstraints(
  242. minWidth: constraints.maxWidth,
  243. minHeight: constraints.maxHeight,
  244. ),
  245. child: IntrinsicHeight(
  246. child: Column(
  247. mainAxisAlignment: MainAxisAlignment.start,
  248. children: [
  249. SizedBox(
  250. width: double.infinity,
  251. child: Column(
  252. crossAxisAlignment: CrossAxisAlignment.start,
  253. mainAxisAlignment: MainAxisAlignment.start,
  254. children: [
  255. Text(
  256. 'Password reminder',
  257. style: enteTheme.textTheme.h3Bold,
  258. ),
  259. Text(
  260. Configuration.instance.getEmail()!,
  261. style: enteTheme.textTheme.small.copyWith(
  262. color: enteTheme.colorScheme.textMuted,
  263. ),
  264. ),
  265. ],
  266. ),
  267. ),
  268. const SizedBox(height: 18),
  269. Text(
  270. "Enter your password to ensure you remember it."
  271. "\n\nThe developer account we use to publish ente on App Store will change in the next version, so you will need to login again when the next version is released.",
  272. style: enteTheme.textTheme.small
  273. .copyWith(color: enteTheme.colorScheme.textMuted),
  274. ),
  275. const SizedBox(height: 24),
  276. TextFormField(
  277. autofillHints: const [AutofillHints.password],
  278. decoration: InputDecoration(
  279. filled: true,
  280. hintText: "Password",
  281. suffixIcon: IconButton(
  282. icon: Icon(
  283. _password2Visible
  284. ? Icons.visibility
  285. : Icons.visibility_off,
  286. color: Theme.of(context).iconTheme.color,
  287. size: 20,
  288. ),
  289. onPressed: () {
  290. setState(() {
  291. _password2Visible = !_password2Visible;
  292. });
  293. },
  294. ),
  295. contentPadding: const EdgeInsets.all(20),
  296. border: UnderlineInputBorder(
  297. borderSide: BorderSide.none,
  298. borderRadius: BorderRadius.circular(6),
  299. ),
  300. ),
  301. style: const TextStyle(
  302. fontSize: 14,
  303. fontFeatures: [FontFeature.tabularFigures()],
  304. ),
  305. controller: _passwordController,
  306. autofocus: false,
  307. autocorrect: false,
  308. obscureText: !_password2Visible,
  309. keyboardType: TextInputType.visiblePassword,
  310. onChanged: (_) {
  311. _incorrectPassword = false;
  312. setState(() {});
  313. },
  314. ),
  315. _incorrectPassword
  316. ? const SizedBox(height: 2)
  317. : const SizedBox.shrink(),
  318. _incorrectPassword
  319. ? Align(
  320. alignment: Alignment.centerLeft,
  321. child: Text(
  322. "Incorrect password",
  323. style: enteTheme.textTheme.small.copyWith(
  324. color: enteTheme.colorScheme.warning700,
  325. ),
  326. ),
  327. )
  328. : const SizedBox.shrink(),
  329. const SizedBox(height: 12),
  330. Expanded(
  331. child: Container(
  332. alignment: Alignment.bottomCenter,
  333. width: double.infinity,
  334. padding: const EdgeInsets.fromLTRB(0, 12, 0, 40),
  335. child: Column(
  336. mainAxisAlignment: MainAxisAlignment.end,
  337. crossAxisAlignment: CrossAxisAlignment.stretch,
  338. children: [
  339. GradientButton(
  340. onTap: _verifyRecoveryKey,
  341. text: "Verify",
  342. ),
  343. const SizedBox(height: 8),
  344. ],
  345. ),
  346. ),
  347. ),
  348. const SizedBox(height: 20)
  349. ],
  350. ),
  351. ),
  352. ),
  353. );
  354. },
  355. ),
  356. ),
  357. );
  358. }
  359. }