diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 07443e05..f948b741 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -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; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 0e5f3b1b..98cf5b0a 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -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", diff --git a/lib/views/login.dart b/lib/views/login.dart index 14e3e954..e1d8345f 100644 --- a/lib/views/login.dart +++ b/lib/views/login.dart @@ -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 { } } + 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 { ), ), ), + Center( + child: FlatButton( + child: Text( + L10n.of(context).passwordForgotten, + style: TextStyle( + color: Colors.blue, + decoration: TextDecoration.underline, + ), + ), + onPressed: () => _passwordForgotten(context), + ), + ), ], ); }), diff --git a/lib/views/settings.dart b/lib/views/settings.dart index da259751..60615103 100644 --- a/lib/views/settings.dart +++ b/lib/views/settings.dart @@ -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 { ), 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), diff --git a/lib/views/settings_3pid.dart b/lib/views/settings_3pid.dart new file mode 100644 index 00000000..4e2ed00c --- /dev/null +++ b/lib/views/settings_3pid.dart @@ -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 { + 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 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> _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>( + future: _request, + builder: (BuildContext context, + AsyncSnapshot> 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; + } +} diff --git a/pubspec.lock b/pubspec.lock index 607ec432..7a8128c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index f6a19a0b..ad0e5ba5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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