mirror of
				https://gitlab.com/famedly/fluffychat.git
				synced 2025-10-31 12:07:24 +01:00 
			
		
		
		
	feat: Implement password recovery
This commit is contained in:
		
							parent
							
								
									ba66d2a00a
								
							
						
					
					
						commit
						4b2fef548c
					
				| @ -2,7 +2,7 @@ abstract class AppConfig { | ||||
|   static String get applicationName => _applicationName; | ||||
|   static String _applicationName = 'FluffyChat'; | ||||
|   static String get defaultHomeserver => _defaultHomeserver; | ||||
|   static String _defaultHomeserver = 'matrix.tchncs.de'; | ||||
|   static String _defaultHomeserver = 'matrix-client.matrix.org'; | ||||
|   static String get privacyUrl => _privacyUrl; | ||||
|   static String _privacyUrl = 'https://fluffychat.im/en/privacy.html'; | ||||
|   static String get sourceCodeUrl => _sourceCodeUrl; | ||||
|  | ||||
| @ -534,6 +534,11 @@ | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "enterAnEmailAddress": "Enter an email address", | ||||
|   "@enterAnEmailAddress": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "emoteExists": "Emote already exists!", | ||||
|   "@emoteExists": { | ||||
|     "type": "text", | ||||
| @ -789,6 +794,11 @@ | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "iHaveClickedOnLink": "I have clicked on the link", | ||||
|   "@iHaveClickedOnLink": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "editJitsiInstance": "Edit Jitsi instance", | ||||
|   "@editJitsiInstance": { | ||||
|     "type": "text", | ||||
| @ -965,6 +975,11 @@ | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "noPasswordRecoveryDescription": "You have added no way to recover you password yet.", | ||||
|   "@noPasswordRecoveryDescription": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "noCrossSignBootstrap": "Fluffychat currently does not support enabling Cross-Signing. Please enable it from within Element.", | ||||
|   "@noCrossSignBootstrap": { | ||||
|     "type": "text", | ||||
| @ -1082,6 +1097,16 @@ | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "passwordRecovery": "Password recovery", | ||||
|   "@passwordRecovery": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "passwordForgotten": "Password forgotten", | ||||
|   "@passwordForgotten": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "pickImage": "Pick image", | ||||
|   "@pickImage": { | ||||
|     "type": "text", | ||||
| @ -1104,6 +1129,11 @@ | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "pleaseClickOnLink": "Please click on the link in the email and then proceed.", | ||||
|   "@pleaseClickOnLink": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "pleaseEnterAMatrixIdentifier": "Please enter a matrix identifier", | ||||
|   "@pleaseEnterAMatrixIdentifier": { | ||||
|     "type": "text", | ||||
| @ -1718,11 +1748,21 @@ | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "weSentYouAnEmail": "We sent you an email", | ||||
|   "@weSentYouAnEmail": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "welcomeText": "Welcome to the cutest instant messenger in the Matrix network.", | ||||
|   "@welcomeText": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "withTheseAddressesRecoveryDescription": "With these addresses you can recover you password if you need.", | ||||
|   "@withTheseAddressesRecoveryDescription": { | ||||
|     "type": "text", | ||||
|     "placeholders": {} | ||||
|   }, | ||||
|   "whoIsAllowedToJoinThisGroup": "Who is allowed to join this group", | ||||
|   "@whoIsAllowedToJoinThisGroup": { | ||||
|     "type": "text", | ||||
|  | ||||
| @ -1,11 +1,13 @@ | ||||
| import 'dart:async'; | ||||
| import 'dart:math'; | ||||
| 
 | ||||
| import 'package:adaptive_dialog/adaptive_dialog.dart'; | ||||
| import 'package:famedlysdk/famedlysdk.dart'; | ||||
| import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; | ||||
| import 'package:fluffychat/components/matrix.dart'; | ||||
| import 'package:fluffychat/utils/app_route.dart'; | ||||
| import 'package:fluffychat/utils/firebase_controller.dart'; | ||||
| import 'package:flushbar/flushbar_helper.dart'; | ||||
| import 'package:flutter/foundation.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/l10n.dart'; | ||||
| @ -102,6 +104,64 @@ class _LoginState extends State<Login> { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   void _passwordForgotten(BuildContext context) async { | ||||
|     final input = await showTextInputDialog( | ||||
|       context: context, | ||||
|       title: L10n.of(context).enterAnEmailAddress, | ||||
|       textFields: [ | ||||
|         DialogTextField( | ||||
|           hintText: L10n.of(context).enterAnEmailAddress, | ||||
|           keyboardType: TextInputType.emailAddress, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|     if (input == null) return; | ||||
|     final clientSecret = DateTime.now().millisecondsSinceEpoch.toString(); | ||||
|     final response = await SimpleDialogs(context).tryRequestWithLoadingDialog( | ||||
|       Matrix.of(context).client.resetPasswordUsingEmail( | ||||
|             input.single, | ||||
|             clientSecret, | ||||
|             sendAttempt++, | ||||
|           ), | ||||
|     ); | ||||
|     if (response == false) return; | ||||
|     final ok = await showOkAlertDialog( | ||||
|       context: context, | ||||
|       title: L10n.of(context).weSentYouAnEmail, | ||||
|       message: L10n.of(context).pleaseClickOnLink, | ||||
|       okLabel: L10n.of(context).iHaveClickedOnLink, | ||||
|     ); | ||||
|     if (ok == null) return; | ||||
|     final password = await showTextInputDialog( | ||||
|       context: context, | ||||
|       title: L10n.of(context).chooseAStrongPassword, | ||||
|       textFields: [ | ||||
|         DialogTextField( | ||||
|           hintText: '******', | ||||
|           obscureText: true, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|     if (password == null) return; | ||||
|     final threepidCreds = { | ||||
|       'client_secret': clientSecret, | ||||
|       'sid': (response as RequestTokenResponse).sid, | ||||
|     }; | ||||
|     final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( | ||||
|       Matrix.of(context).client.changePassword(password.single, auth: { | ||||
|         'type': 'm.login.email.identity', | ||||
|         'threepidCreds': threepidCreds, // Don't ask... >.< | ||||
|         'threepid_creds': threepidCreds, | ||||
|       }), | ||||
|     ); | ||||
|     if (success != false) { | ||||
|       FlushbarHelper.createSuccess( | ||||
|           message: L10n.of(context).passwordHasBeenChanged); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   static int sendAttempt = 0; | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Scaffold( | ||||
| @ -188,6 +248,18 @@ class _LoginState extends State<Login> { | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             Center( | ||||
|               child: FlatButton( | ||||
|                 child: Text( | ||||
|                   L10n.of(context).passwordForgotten, | ||||
|                   style: TextStyle( | ||||
|                     color: Colors.blue, | ||||
|                     decoration: TextDecoration.underline, | ||||
|                   ), | ||||
|                 ), | ||||
|                 onPressed: () => _passwordForgotten(context), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }), | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import 'dart:io'; | ||||
| 
 | ||||
| import 'package:adaptive_dialog/adaptive_dialog.dart'; | ||||
| import 'package:fluffychat/views/settings_3pid.dart'; | ||||
| import 'package:flushbar/flushbar_helper.dart'; | ||||
| import 'package:famedlysdk/famedlysdk.dart'; | ||||
| import 'package:file_picker_cross/file_picker_cross.dart'; | ||||
| @ -488,6 +489,16 @@ class _SettingsState extends State<Settings> { | ||||
|               ), | ||||
|               onTap: () => _changePasswordAccountAction(context), | ||||
|             ), | ||||
|             ListTile( | ||||
|               trailing: Icon(Icons.email), | ||||
|               title: Text(L10n.of(context).passwordRecovery), | ||||
|               onTap: () => Navigator.of(context).push( | ||||
|                 AppRoute.defaultRoute( | ||||
|                   context, | ||||
|                   Settings3PidView(), | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|             ListTile( | ||||
|               trailing: Icon(Icons.exit_to_app), | ||||
|               title: Text(L10n.of(context).logout), | ||||
|  | ||||
							
								
								
									
										204
									
								
								lib/views/settings_3pid.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								lib/views/settings_3pid.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,204 @@ | ||||
| import 'package:adaptive_dialog/adaptive_dialog.dart'; | ||||
| import 'package:famedlysdk/famedlysdk.dart'; | ||||
| import 'package:fluffychat/components/adaptive_page_layout.dart'; | ||||
| import 'package:fluffychat/components/dialogs/simple_dialogs.dart'; | ||||
| import 'package:fluffychat/components/matrix.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/l10n.dart'; | ||||
| 
 | ||||
| import 'chat_list.dart'; | ||||
| 
 | ||||
| class Settings3PidView extends StatelessWidget { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return AdaptivePageLayout( | ||||
|       primaryPage: FocusPage.SECOND, | ||||
|       firstScaffold: ChatList(), | ||||
|       secondScaffold: Settings3Pid(), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| class Settings3Pid extends StatefulWidget { | ||||
|   static int sendAttempt = 0; | ||||
| 
 | ||||
|   @override | ||||
|   _Settings3PidState createState() => _Settings3PidState(); | ||||
| } | ||||
| 
 | ||||
| class _Settings3PidState extends State<Settings3Pid> { | ||||
|   void _add3PidAction(BuildContext context) async { | ||||
|     final input = await showTextInputDialog( | ||||
|       context: context, | ||||
|       title: L10n.of(context).enterAnEmailAddress, | ||||
|       textFields: [ | ||||
|         DialogTextField( | ||||
|           hintText: L10n.of(context).enterAnEmailAddress, | ||||
|           keyboardType: TextInputType.emailAddress, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|     if (input == null) return; | ||||
|     final clientSecret = DateTime.now().millisecondsSinceEpoch.toString(); | ||||
|     final response = await SimpleDialogs(context).tryRequestWithLoadingDialog( | ||||
|       Matrix.of(context).client.requestEmailToken( | ||||
|             input.single, | ||||
|             clientSecret, | ||||
|             Settings3Pid.sendAttempt++, | ||||
|           ), | ||||
|     ); | ||||
|     if (response == false) return; | ||||
|     final ok = await showOkAlertDialog( | ||||
|       context: context, | ||||
|       title: L10n.of(context).weSentYouAnEmail, | ||||
|       message: L10n.of(context).pleaseClickOnLink, | ||||
|       okLabel: L10n.of(context).iHaveClickedOnLink, | ||||
|     ); | ||||
|     if (ok == null) return; | ||||
|     final password = await showTextInputDialog( | ||||
|       context: context, | ||||
|       title: L10n.of(context).pleaseEnterYourPassword, | ||||
|       textFields: [ | ||||
|         DialogTextField( | ||||
|           hintText: '******', | ||||
|           obscureText: true, | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|     if (password == null) return; | ||||
|     final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( | ||||
|       Future.microtask(() async { | ||||
|         final Function request = ({Map<String, dynamic> auth}) async => | ||||
|             Matrix.of(context).client.addThirdPartyIdentifier( | ||||
|                   clientSecret, | ||||
|                   (response as RequestTokenResponse).sid, | ||||
|                   auth: auth, | ||||
|                 ); | ||||
|         try { | ||||
|           await request(); | ||||
|         } on MatrixException catch (exception) { | ||||
|           if (!exception.requireAdditionalAuthentication) rethrow; | ||||
|           await request( | ||||
|             auth: { | ||||
|               'type': 'm.login.password', | ||||
|               'identifier': { | ||||
|                 'type': 'm.id.user', | ||||
|                 'user': Matrix.of(context).client.userID, | ||||
|               }, | ||||
|               'user': Matrix.of(context).client.userID, | ||||
|               'password': password.single, | ||||
|               'session': exception.session, | ||||
|             }, | ||||
|           ); | ||||
|         } | ||||
|         return; | ||||
|       }), | ||||
|     ); | ||||
|     if (success == false) return; | ||||
|     setState(() => _request = null); | ||||
|   } | ||||
| 
 | ||||
|   Future<List<ThirdPartyIdentifier>> _request; | ||||
| 
 | ||||
|   void _delete3Pid( | ||||
|       BuildContext context, ThirdPartyIdentifier identifier) async { | ||||
|     if (await showOkCancelAlertDialog( | ||||
|           context: context, | ||||
|           title: L10n.of(context).areYouSure, | ||||
|         ) != | ||||
|         OkCancelResult.ok) { | ||||
|       return; | ||||
|     } | ||||
|     final success = await SimpleDialogs(context).tryRequestWithLoadingDialog( | ||||
|         Matrix.of(context).client.deleteThirdPartyIdentifier( | ||||
|               identifier.address, | ||||
|               identifier.medium, | ||||
|             )); | ||||
|     if (success == false) return; | ||||
|     setState(() => _request = null); | ||||
|   } | ||||
| 
 | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     _request ??= Matrix.of(context).client.requestThirdPartyIdentifiers(); | ||||
|     return Scaffold( | ||||
|       appBar: AppBar( | ||||
|         title: Text(L10n.of(context).passwordRecovery), | ||||
|         actions: [ | ||||
|           IconButton( | ||||
|             icon: Icon(Icons.add), | ||||
|             onPressed: () => _add3PidAction(context), | ||||
|           ) | ||||
|         ], | ||||
|       ), | ||||
|       body: FutureBuilder<List<ThirdPartyIdentifier>>( | ||||
|         future: _request, | ||||
|         builder: (BuildContext context, | ||||
|             AsyncSnapshot<List<ThirdPartyIdentifier>> snapshot) { | ||||
|           if (snapshot.hasError) { | ||||
|             return Center( | ||||
|               child: Text( | ||||
|                 snapshot.error.toString(), | ||||
|                 textAlign: TextAlign.center, | ||||
|               ), | ||||
|             ); | ||||
|           } | ||||
|           if (!snapshot.hasData) { | ||||
|             return Center(child: CircularProgressIndicator()); | ||||
|           } | ||||
|           final identifier = snapshot.data; | ||||
|           return Column( | ||||
|             children: [ | ||||
|               ListTile( | ||||
|                 leading: CircleAvatar( | ||||
|                   backgroundColor: Theme.of(context).scaffoldBackgroundColor, | ||||
|                   foregroundColor: | ||||
|                       identifier.isEmpty ? Colors.orange : Colors.grey, | ||||
|                   child: Icon( | ||||
|                     identifier.isEmpty ? Icons.warning : Icons.info, | ||||
|                   ), | ||||
|                 ), | ||||
|                 title: Text( | ||||
|                   identifier.isEmpty | ||||
|                       ? L10n.of(context).noPasswordRecoveryDescription | ||||
|                       : L10n.of(context).withTheseAddressesRecoveryDescription, | ||||
|                 ), | ||||
|               ), | ||||
|               Divider(height: 1), | ||||
|               Expanded( | ||||
|                 child: ListView.builder( | ||||
|                   itemCount: identifier.length, | ||||
|                   itemBuilder: (BuildContext context, int i) => ListTile( | ||||
|                     leading: CircleAvatar( | ||||
|                         backgroundColor: | ||||
|                             Theme.of(context).scaffoldBackgroundColor, | ||||
|                         foregroundColor: Colors.grey, | ||||
|                         child: Icon(identifier[i].iconData)), | ||||
|                     title: Text(identifier[i].address), | ||||
|                     trailing: IconButton( | ||||
|                       icon: Icon(Icons.delete_forever), | ||||
|                       color: Colors.red, | ||||
|                       onPressed: () => _delete3Pid(context, identifier[i]), | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|               ), | ||||
|             ], | ||||
|           ); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| extension on ThirdPartyIdentifier { | ||||
|   IconData get iconData { | ||||
|     switch (medium) { | ||||
|       case ThirdPartyIdentifierMedium.email: | ||||
|         return Icons.mail_outline_rounded; | ||||
|       case ThirdPartyIdentifierMedium.msisdn: | ||||
|         return Icons.phone_android_outlined; | ||||
|     } | ||||
|     return Icons.device_unknown_outlined; | ||||
|   } | ||||
| } | ||||
| @ -208,8 +208,8 @@ packages: | ||||
|     dependency: "direct main" | ||||
|     description: | ||||
|       path: "." | ||||
|       ref: ab8eb71fee228be8a6ead96d03daf46c3d0c053c | ||||
|       resolved-ref: ab8eb71fee228be8a6ead96d03daf46c3d0c053c | ||||
|       ref: "01ce832aaa738d4e4432e1f0912b0427ce8d134c" | ||||
|       resolved-ref: "01ce832aaa738d4e4432e1f0912b0427ce8d134c" | ||||
|       url: "https://gitlab.com/famedly/famedlysdk.git" | ||||
|     source: git | ||||
|     version: "0.0.1" | ||||
|  | ||||
| @ -13,7 +13,7 @@ dependencies: | ||||
|   famedlysdk: | ||||
|     git: | ||||
|       url: https://gitlab.com/famedly/famedlysdk.git | ||||
|       ref: ab8eb71fee228be8a6ead96d03daf46c3d0c053c | ||||
|       ref: 01ce832aaa738d4e4432e1f0912b0427ce8d134c | ||||
| 
 | ||||
|   localstorage: ^3.0.3+6 | ||||
|   file_picker_cross: 4.2.2 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Christian Pauly
						Christian Pauly