From b48cf2ecdc88aaa366be39274c01ac9b50ea058d Mon Sep 17 00:00:00 2001 From: Krille Fear Date: Sat, 30 Oct 2021 14:06:10 +0200 Subject: [PATCH] feat: Nicer registration form --- lib/pages/homeserver_picker.dart | 5 +- lib/pages/signup.dart | 80 +++++++-- lib/pages/views/signup_view.dart | 169 ++++++++++++------- lib/utils/localized_exception_extension.dart | 3 + lib/utils/uia_request_manager.dart | 104 +++++++----- lib/widgets/matrix.dart | 3 + 6 files changed, 243 insertions(+), 121 deletions(-) diff --git a/lib/pages/homeserver_picker.dart b/lib/pages/homeserver_picker.dart index 217e2e3e..37bf0a49 100644 --- a/lib/pages/homeserver_picker.dart +++ b/lib/pages/homeserver_picker.dart @@ -220,7 +220,10 @@ class HomeserverPickerController extends State { } } - void signUpAction() => VRouter.of(context).to('signup'); + void signUpAction() => VRouter.of(context).to( + 'signup', + queryParameters: {'domain': domain}, + ); @override Widget build(BuildContext context) { diff --git a/lib/pages/signup.dart b/lib/pages/signup.dart index c672a7a4..a6c639a3 100644 --- a/lib/pages/signup.dart +++ b/lib/pages/signup.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/pages/views/signup_view.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -17,29 +18,78 @@ class SignupPage extends StatefulWidget { class SignupPageController extends State { final TextEditingController usernameController = TextEditingController(); final TextEditingController passwordController = TextEditingController(); - String usernameError; - String passwordError; + final TextEditingController passwordController2 = TextEditingController(); + final TextEditingController emailController = TextEditingController(); + String error; bool loading = false; - bool showPassword = true; + bool showPassword = false; void toggleShowPassword() => setState(() => showPassword = !showPassword); + String get domain => VRouter.of(context).queryParameters['domain']; + + final GlobalKey formKey = GlobalKey(); + + String usernameTextFieldValidator(String value) { + usernameController.text = + usernameController.text.trim().toLowerCase().replaceAll(' ', '_'); + if (value.isEmpty) { + return L10n.of(context).pleaseChooseAUsername; + } + return null; + } + + String password1TextFieldValidator(String value) { + const minLength = 8; + if (value.isEmpty) { + return L10n.of(context).chooseAStrongPassword; + } + if (value.length < minLength) { + return 'Please choose at least $minLength characters.'; + } + return null; + } + + String password2TextFieldValidator(String value) { + if (value.isEmpty) { + return L10n.of(context).chooseAStrongPassword; + } + if (value != passwordController.text) { + return 'Passwords do not match!'; + } + return null; + } + + String emailTextFieldValidator(String value) { + if (value.isNotEmpty && !value.contains('@')) { + return 'Please enter a valid email address.'; + } + return null; + } + void signup([_]) async { - usernameError = passwordError = null; + setState(() { + error = null; + }); + if (!formKey.currentState.validate()) return; - if (usernameController.text.isEmpty) { - return setState( - () => usernameError = L10n.of(context).pleaseChooseAUsername); - } - if (passwordController.text.isEmpty) { - return setState( - () => passwordError = L10n.of(context).chooseAStrongPassword); - } - - setState(() => loading = true); + setState(() { + loading = true; + }); try { final client = Matrix.of(context).getLoginClient(); + final email = emailController.text; + if (email.isNotEmpty) { + Matrix.of(context).currentClientSecret = + DateTime.now().millisecondsSinceEpoch.toString(); + Matrix.of(context).currentThreepidCreds = + await client.requestTokenToRegisterEmail( + Matrix.of(context).currentClientSecret, + email, + 0, + ); + } await client.uiaRequestBackground( (auth) => client.register( username: usernameController.text, @@ -49,7 +99,7 @@ class SignupPageController extends State { ), ); } catch (e) { - passwordError = (e as Object).toLocalizedString(context); + error = (e as Object).toLocalizedString(context); } finally { setState(() => loading = false); } diff --git a/lib/pages/views/signup_view.dart b/lib/pages/views/signup_view.dart index 678c9cf6..84066932 100644 --- a/lib/pages/views/signup_view.dart +++ b/lib/pages/views/signup_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/widgets/layouts/one_page_card.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../signup.dart'; class SignupPageView extends StatelessWidget { @@ -17,76 +16,116 @@ class SignupPageView extends StatelessWidget { appBar: AppBar( title: Text(L10n.of(context).signUp), ), - body: ListView( - children: [ - ListTile( - title: Text(L10n.of(context).pleaseChooseAUsername), - subtitle: Text(L10n.of(context).newUsernameDescription), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - readOnly: controller.loading, - autocorrect: false, - autofocus: true, - controller: controller.usernameController, - autofillHints: - controller.loading ? null : [AutofillHints.username], - decoration: InputDecoration( - prefixIcon: const Icon(Icons.account_box_outlined), - hintText: L10n.of(context).username, - errorText: controller.usernameError, - labelText: L10n.of(context).username, - prefixText: '@', - suffixText: - ':${Matrix.of(context).getLoginClient().homeserver.host}'), + body: Form( + key: controller.formKey, + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + controller: controller.usernameController, + autofillHints: + controller.loading ? null : [AutofillHints.username], + validator: controller.usernameTextFieldValidator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.account_circle_outlined), + hintText: L10n.of(context).username, + labelText: L10n.of(context).username, + prefixText: '@', + prefixStyle: const TextStyle(fontWeight: FontWeight.bold), + suffixStyle: const TextStyle(fontWeight: FontWeight.w200), + suffixText: ':${controller.domain}'), + ), ), - ), - const Divider(), - ListTile( - title: Text(L10n.of(context).chooseAStrongPassword), - subtitle: Text(L10n.of(context).newPasswordDescription), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - readOnly: controller.loading, - autocorrect: false, - autofillHints: - controller.loading ? null : [AutofillHints.password], - controller: controller.passwordController, - obscureText: !controller.showPassword, - onSubmitted: controller.signup, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.lock_outlined), - hintText: '****', - errorText: controller.passwordError, - suffixIcon: IconButton( - tooltip: L10n.of(context).showPassword, - icon: Icon(controller.showPassword - ? Icons.visibility_off_outlined - : Icons.visibility_outlined), - onPressed: controller.toggleShowPassword, + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + autofillHints: + controller.loading ? null : [AutofillHints.password], + controller: controller.passwordController, + obscureText: !controller.showPassword, + validator: controller.password1TextFieldValidator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.vpn_key_outlined), + hintText: '****', + suffixIcon: IconButton( + tooltip: L10n.of(context).showPassword, + icon: Icon(controller.showPassword + ? Icons.visibility_off_outlined + : Icons.visibility_outlined), + onPressed: controller.toggleShowPassword, + ), + labelText: L10n.of(context).password, ), - labelText: L10n.of(context).password, ), ), - ), - const Divider(), - const SizedBox(height: 12), - Hero( - tag: 'loginButton', - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: ElevatedButton( - onPressed: controller.loading ? null : controller.signup, - child: controller.loading - ? const LinearProgressIndicator() - : Text(L10n.of(context).signUp), + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + autofillHints: + controller.loading ? null : [AutofillHints.password], + controller: controller.passwordController2, + obscureText: true, + validator: controller.password2TextFieldValidator, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.repeat_outlined), + hintText: '****', + labelText: 'Repeat password', + ), ), ), - ), - ], + Padding( + padding: const EdgeInsets.all(12.0), + child: TextFormField( + readOnly: controller.loading, + autocorrect: false, + controller: controller.emailController, + keyboardType: TextInputType.emailAddress, + autofillHints: + controller.loading ? null : [AutofillHints.username], + validator: controller.emailTextFieldValidator, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.mail_outlined), + labelText: L10n.of(context).addEmail, + hintText: 'email@example.abc', + ), + ), + ), + const Divider(), + const SizedBox(height: 12), + if (controller.error != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + controller.error, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 12), + const Divider(), + const SizedBox(height: 12), + ], + Hero( + tag: 'loginButton', + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ElevatedButton( + onPressed: controller.loading ? null : controller.signup, + child: controller.loading + ? const LinearProgressIndicator() + : Text(L10n.of(context).signUp), + ), + ), + ), + ], + ), ), ), ); diff --git a/lib/utils/localized_exception_extension.dart b/lib/utils/localized_exception_extension.dart index 38df9cca..ac767611 100644 --- a/lib/utils/localized_exception_extension.dart +++ b/lib/utils/localized_exception_extension.dart @@ -5,6 +5,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; +import 'uia_request_manager.dart'; + extension LocalizedExceptionExtension on Object { String toLocalizedString(BuildContext context) { if (this is MatrixException) { @@ -48,6 +50,7 @@ extension LocalizedExceptionExtension on Object { if (this is MatrixConnectionException || this is SocketException) { return L10n.of(context).noConnectionToTheServer; } + if (this is UiaException) return toString(); Logs().w('Something went wrong: ', this); return L10n.of(context).oopsSomethingWentWrong; } diff --git a/lib/utils/uia_request_manager.dart b/lib/utils/uia_request_manager.dart index a2c0c779..e9a1765a 100644 --- a/lib/utils/uia_request_manager.dart +++ b/lib/utils/uia_request_manager.dart @@ -1,18 +1,24 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/matrix.dart'; extension UiaRequestManager on MatrixState { Future uiaRequestHandler(UiaRequest uiaRequest) async { try { if (uiaRequest.state != UiaRequestState.waitForUser || - uiaRequest.nextStages.isEmpty) return; + uiaRequest.nextStages.isEmpty) { + Logs().d('Uia Request Stage: ${uiaRequest.state}'); + return; + } final stage = uiaRequest.nextStages.first; + Logs().d('Uia Request Stage: $stage'); switch (stage) { case AuthenticationTypes.password: final input = cachedPassword ?? @@ -31,7 +37,9 @@ extension UiaRequestManager on MatrixState { ], )) ?.single; - if (input?.isEmpty ?? true) return; + if (input?.isEmpty ?? true) { + return uiaRequest.cancel(); + } return uiaRequest.completeStage( AuthenticationPassword( session: uiaRequest.session, @@ -40,35 +48,18 @@ extension UiaRequestManager on MatrixState { ), ); case AuthenticationTypes.emailIdentity: - final emailInput = await showTextInputDialog( - context: navigatorContext, - message: L10n.of(context).serverRequiresEmail, - okLabel: L10n.of(context).next, - cancelLabel: L10n.of(context).cancel, - textFields: [ - DialogTextField( - hintText: L10n.of(context).addEmail, - keyboardType: TextInputType.emailAddress, - ), - ], - ); - if (emailInput == null || emailInput.isEmpty) { - return uiaRequest - .cancel(Exception(L10n.of(context).serverRequiresEmail)); + if (currentThreepidCreds == null || currentClientSecret == null) { + return uiaRequest.cancel( + UiaException(L10n.of(widget.context).serverRequiresEmail), + ); } - final clientSecret = DateTime.now().millisecondsSinceEpoch.toString(); - final currentThreepidCreds = await client.requestTokenToRegisterEmail( - clientSecret, - emailInput.single, - 0, - ); final auth = AuthenticationThreePidCreds( session: uiaRequest.session, type: AuthenticationTypes.emailIdentity, threepidCreds: [ ThreepidCreds( sid: currentThreepidCreds.sid, - clientSecret: clientSecret, + clientSecret: currentClientSecret, ), ], ); @@ -92,24 +83,41 @@ extension UiaRequestManager on MatrixState { ), ); default: - await launch( - client.homeserver.toString() + - '/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}', - ); - if (OkCancelResult.ok == - await showOkCancelAlertDialog( - useRootNavigator: false, - message: L10n.of(context).pleaseFollowInstructionsOnWeb, - context: navigatorContext, - okLabel: L10n.of(context).next, - cancelLabel: L10n.of(context).cancel, - )) { - return uiaRequest.completeStage( - AuthenticationData(session: uiaRequest.session), + final url = Uri.parse(client.homeserver.toString() + + '/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}'); + if (PlatformInfos.isMobile) { + final browser = UiaFallbackBrowser(); + browser.addMenuItem( + ChromeSafariBrowserMenuItem( + action: (_, __) { + uiaRequest.cancel(); + }, + label: L10n.of(context).cancel, + id: 0, + ), ); + await browser.open(url: url); + await browser.whenClosed.stream.first; } else { - return uiaRequest.cancel(); + launch(url.toString()); + if (OkCancelResult.ok == + await showOkCancelAlertDialog( + useRootNavigator: false, + message: L10n.of(context).pleaseFollowInstructionsOnWeb, + context: navigatorContext, + okLabel: L10n.of(context).next, + cancelLabel: L10n.of(context).cancel, + )) { + return uiaRequest.completeStage( + AuthenticationData(session: uiaRequest.session), + ); + } else { + return uiaRequest.cancel(); + } } + await uiaRequest.completeStage( + AuthenticationData(session: uiaRequest.session), + ); } } catch (e, s) { Logs().e('Error while background UIA', e, s); @@ -117,3 +125,19 @@ extension UiaRequestManager on MatrixState { } } } + +class UiaException implements Exception { + final String reason; + + UiaException(this.reason); + + @override + String toString() => reason; +} + +class UiaFallbackBrowser extends ChromeSafariBrowser { + final StreamController whenClosed = StreamController.broadcast(); + + @override + onClosed() => whenClosed.add(true); +} diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index 3492b205..a2e0e678 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -75,6 +75,9 @@ class MatrixState extends State with WidgetsBindingObserver { int getClientIndexByMatrixId(String matrixId) => widget.clients.indexWhere((client) => client.userID == matrixId); + String currentClientSecret; + RequestTokenResponse currentThreepidCreds; + int get _safeActiveClient { if (widget.clients.isEmpty) { widget.clients.add(getLoginClient());