diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index a9c0a90c..28516f88 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1357,6 +1357,16 @@ "fileName": {} } }, + "invalidEmail": "Invalid email", + "@invalidEmail": { + "type": "text", + "placeholders": {} + }, + "optionalAddEmail": "(Optional) Your email address", + "@optionalAddEmail": { + "type": "text", + "placeholders": {} + }, "pleaseChooseAUsername": "Please choose a username", "@pleaseChooseAUsername": { "type": "text", diff --git a/lib/utils/get_client_secret.dart b/lib/utils/get_client_secret.dart new file mode 100644 index 00000000..d86cd388 --- /dev/null +++ b/lib/utils/get_client_secret.dart @@ -0,0 +1,7 @@ +import 'dart:math'; + +const _chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; +Random _rnd = Random(); + +String getClientSecret(int length) => String.fromCharCodes(Iterable.generate( + length, (_) => _chars.codeUnitAt(_rnd.nextInt(_chars.length)))); diff --git a/lib/views/sign_up_password.dart b/lib/views/sign_up_password.dart index 6489ecda..2add3a1a 100644 --- a/lib/views/sign_up_password.dart +++ b/lib/views/sign_up_password.dart @@ -1,6 +1,9 @@ +import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; +import 'package:email_validator/email_validator.dart'; import 'package:famedlysdk/famedlysdk.dart'; +import 'package:fluffychat/utils/get_client_secret.dart'; import 'package:fluffychat/views/ui/sign_up_password_ui.dart'; import 'package:fluffychat/views/widgets/matrix.dart'; @@ -19,7 +22,9 @@ class SignUpPassword extends StatefulWidget { class SignUpPasswordController extends State { final TextEditingController passwordController = TextEditingController(); + final TextEditingController emailController = TextEditingController(); String passwordError; + String emailError; bool loading = false; bool showPassword = true; @@ -30,7 +35,7 @@ class SignUpPasswordController extends State { if (passwordController.text.isEmpty) { setState(() => passwordError = L10n.of(context).pleaseEnterYourPassword); } else { - setState(() => passwordError = null); + setState(() => passwordError = emailError = null); } if (passwordController.text.isEmpty) { @@ -39,6 +44,31 @@ class SignUpPasswordController extends State { try { setState(() => loading = true); + if (emailController.text.isNotEmpty) { + emailController.text = emailController.text.trim(); + if (!EmailValidator.validate(emailController.text)) { + setState(() => emailError = L10n.of(context).invalidEmail); + return; + } + matrix.currentClientSecret = getClientSecret(30); + Logs().d('Request email token'); + matrix.currentThreepidCreds = await matrix.client.requestEmailToken( + emailController.text, + matrix.currentClientSecret, + 1, + ); + if (OkCancelResult.ok != + await showOkCancelAlertDialog( + context: context, + message: L10n.of(context).weSentYouAnEmail, + okLabel: L10n.of(context).confirm, + cancelLabel: L10n.of(context).cancel, + )) { + matrix.currentClientSecret = matrix.currentThreepidCreds = null; + setState(() => loading = false); + return; + } + } final waitForLogin = matrix.client.onLoginStateChanged.stream.first; await matrix.client.uiaRequestBackground((auth) => matrix.client.register( username: widget.username, @@ -46,13 +76,22 @@ class SignUpPasswordController extends State { initialDeviceDisplayName: PlatformInfos.clientName, auth: auth, )); + if (matrix.currentClientSecret != null && + matrix.currentThreepidCreds != null) { + Logs().d('Add third party identifier'); + await matrix.client.addThirdPartyIdentifier( + matrix.currentClientSecret, + matrix.currentThreepidCreds.sid, + ); + } await waitForLogin; } catch (exception) { - setState(() => passwordError = exception.toString()); + setState(() => emailError = exception.toString()); return setState(() => loading = false); } await matrix.client.onLoginStateChanged.stream .firstWhere((l) => l == LoginState.logged); + // tchncs.de try { await matrix.client .setDisplayname(matrix.client.userID, widget.displayname); diff --git a/lib/views/ui/sign_up_password_ui.dart b/lib/views/ui/sign_up_password_ui.dart index f961ecd3..7699653b 100644 --- a/lib/views/ui/sign_up_password_ui.dart +++ b/lib/views/ui/sign_up_password_ui.dart @@ -47,6 +47,21 @@ class SignUpPasswordUI extends StatelessWidget { labelText: L10n.of(context).password), ), ), + Padding( + padding: const EdgeInsets.all(12.0), + child: TextField( + controller: controller.emailController, + readOnly: controller.loading, + autocorrect: false, + keyboardType: TextInputType.emailAddress, + onSubmitted: (_) => controller.signUpAction, + decoration: InputDecoration( + prefixIcon: Icon(Icons.mail_outline_outlined), + errorText: controller.emailError, + hintText: 'email@example.com', + labelText: L10n.of(context).optionalAddEmail), + ), + ), SizedBox(height: 12), Hero( tag: 'loginButton', diff --git a/lib/views/widgets/matrix.dart b/lib/views/widgets/matrix.dart index dd3ac66c..889c2940 100644 --- a/lib/views/widgets/matrix.dart +++ b/lib/views/widgets/matrix.dart @@ -121,57 +121,89 @@ class MatrixState extends State with WidgetsBindingObserver { set cachedPassword(String p) => _cachedPassword = p; + String currentClientSecret; + RequestTokenResponse currentThreepidCreds; + void _onUiaRequest(UiaRequest uiaRequest) async { - if (uiaRequest.state != UiaRequestState.waitForUser || - uiaRequest.nextStages.isEmpty) return; - final stage = uiaRequest.nextStages.first; - switch (stage) { - case AuthenticationTypes.password: - final input = cachedPassword ?? - (await showTextInputDialog( - context: context, - title: L10n.of(context).pleaseEnterYourPassword, - okLabel: L10n.of(context).ok, - cancelLabel: L10n.of(context).cancel, - useRootNavigator: false, - textFields: [ - DialogTextField( - minLines: 1, - maxLines: 1, - obscureText: true, - hintText: '******', - ) - ], - )) - ?.single; - if (input?.isEmpty ?? true) return; - return uiaRequest.completeStage( - AuthenticationPassword( - session: uiaRequest.session, - user: client.userID, - password: input, - identifier: AuthenticationUserIdentifier(user: client.userID), - ), - ); - default: - await launch( - client.homeserver.toString() + - '/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}', - ); - if (OkCancelResult.ok == - await showOkCancelAlertDialog( - message: L10n.of(context).pleaseFollowInstructionsOnWeb, - context: context, - useRootNavigator: false, - okLabel: L10n.of(context).next, - cancelLabel: L10n.of(context).cancel, - )) { + try { + if (uiaRequest.state != UiaRequestState.waitForUser || + uiaRequest.nextStages.isEmpty) return; + final stage = uiaRequest.nextStages.first; + switch (stage) { + case AuthenticationTypes.password: + final input = cachedPassword ?? + (await showTextInputDialog( + context: context, + title: L10n.of(context).pleaseEnterYourPassword, + okLabel: L10n.of(context).ok, + cancelLabel: L10n.of(context).cancel, + useRootNavigator: false, + textFields: [ + DialogTextField( + minLines: 1, + maxLines: 1, + obscureText: true, + hintText: '******', + ) + ], + )) + ?.single; + if (input?.isEmpty ?? true) return; return uiaRequest.completeStage( - AuthenticationData(session: uiaRequest.session), + AuthenticationPassword( + session: uiaRequest.session, + user: client.userID, + password: input, + identifier: AuthenticationUserIdentifier(user: client.userID), + ), ); - } else { - return uiaRequest.cancel(); - } + case AuthenticationTypes.emailIdentity: + if (currentClientSecret == null || currentThreepidCreds == null) { + return uiaRequest + .cancel(Exception('This server requires an email address')); + } + final auth = AuthenticationThreePidCreds( + session: uiaRequest.session, + type: AuthenticationTypes.emailIdentity, + threepidCreds: [ + ThreepidCreds( + sid: currentThreepidCreds.sid, + clientSecret: currentClientSecret, + ), + ], + ); + currentThreepidCreds = currentClientSecret = null; + return uiaRequest.completeStage(auth); + case AuthenticationTypes.dummy: + return uiaRequest.completeStage( + AuthenticationData( + type: AuthenticationTypes.dummy, + session: uiaRequest.session, + ), + ); + default: + await launch( + client.homeserver.toString() + + '/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}', + ); + if (OkCancelResult.ok == + await showOkCancelAlertDialog( + message: L10n.of(context).pleaseFollowInstructionsOnWeb, + context: context, + useRootNavigator: false, + okLabel: L10n.of(context).next, + cancelLabel: L10n.of(context).cancel, + )) { + return uiaRequest.completeStage( + AuthenticationData(session: uiaRequest.session), + ); + } else { + return uiaRequest.cancel(); + } + } + } catch (e, s) { + Logs().e('Error while background UIA', e, s); + return uiaRequest.cancel(e); } } @@ -422,3 +454,27 @@ class MatrixState extends State with WidgetsBindingObserver { ); } } + +class FixedThreepidCreds extends ThreepidCreds { + FixedThreepidCreds({ + String sid, + String clientSecret, + String idServer, + String idAccessToken, + }) : super( + sid: sid, + clientSecret: clientSecret, + idServer: idServer, + idAccessToken: idAccessToken, + ); + + @override + Map toJson() { + final data = {}; + data['sid'] = sid; + data['client_secret'] = clientSecret; + if (idServer != null) data['id_server'] = idServer; + if (idAccessToken != null) data['id_access_token'] = idAccessToken; + return data; + } +}