feat: Nicer registration form

This commit is contained in:
Krille Fear 2021-10-30 14:06:10 +02:00
parent bc78647fb6
commit b48cf2ecdc
6 changed files with 243 additions and 121 deletions

View File

@ -220,7 +220,10 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} }
} }
void signUpAction() => VRouter.of(context).to('signup'); void signUpAction() => VRouter.of(context).to(
'signup',
queryParameters: {'domain': domain},
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.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/pages/views/signup_view.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
@ -17,29 +18,78 @@ class SignupPage extends StatefulWidget {
class SignupPageController extends State<SignupPage> { class SignupPageController extends State<SignupPage> {
final TextEditingController usernameController = TextEditingController(); final TextEditingController usernameController = TextEditingController();
final TextEditingController passwordController = TextEditingController(); final TextEditingController passwordController = TextEditingController();
String usernameError; final TextEditingController passwordController2 = TextEditingController();
String passwordError; final TextEditingController emailController = TextEditingController();
String error;
bool loading = false; bool loading = false;
bool showPassword = true; bool showPassword = false;
void toggleShowPassword() => setState(() => showPassword = !showPassword); void toggleShowPassword() => setState(() => showPassword = !showPassword);
String get domain => VRouter.of(context).queryParameters['domain'];
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
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 { void signup([_]) async {
usernameError = passwordError = null; setState(() {
error = null;
});
if (!formKey.currentState.validate()) return;
if (usernameController.text.isEmpty) { setState(() {
return setState( loading = true;
() => usernameError = L10n.of(context).pleaseChooseAUsername); });
}
if (passwordController.text.isEmpty) {
return setState(
() => passwordError = L10n.of(context).chooseAStrongPassword);
}
setState(() => loading = true);
try { try {
final client = Matrix.of(context).getLoginClient(); 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( await client.uiaRequestBackground(
(auth) => client.register( (auth) => client.register(
username: usernameController.text, username: usernameController.text,
@ -49,7 +99,7 @@ class SignupPageController extends State<SignupPage> {
), ),
); );
} catch (e) { } catch (e) {
passwordError = (e as Object).toLocalizedString(context); error = (e as Object).toLocalizedString(context);
} finally { } finally {
setState(() => loading = false); setState(() => loading = false);
} }

View File

@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/widgets/layouts/one_page_card.dart'; import 'package:fluffychat/widgets/layouts/one_page_card.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../signup.dart'; import '../signup.dart';
class SignupPageView extends StatelessWidget { class SignupPageView extends StatelessWidget {
@ -17,76 +16,116 @@ class SignupPageView extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text(L10n.of(context).signUp), title: Text(L10n.of(context).signUp),
), ),
body: ListView( body: Form(
children: [ key: controller.formKey,
ListTile( child: ListView(
title: Text(L10n.of(context).pleaseChooseAUsername), children: [
subtitle: Text(L10n.of(context).newUsernameDescription), Padding(
), padding: const EdgeInsets.all(12.0),
Padding( child: TextFormField(
padding: const EdgeInsets.all(12.0), readOnly: controller.loading,
child: TextField( autocorrect: false,
readOnly: controller.loading, controller: controller.usernameController,
autocorrect: false, autofillHints:
autofocus: true, controller.loading ? null : [AutofillHints.username],
controller: controller.usernameController, validator: controller.usernameTextFieldValidator,
autofillHints: decoration: InputDecoration(
controller.loading ? null : [AutofillHints.username], prefixIcon: const Icon(Icons.account_circle_outlined),
decoration: InputDecoration( hintText: L10n.of(context).username,
prefixIcon: const Icon(Icons.account_box_outlined), labelText: L10n.of(context).username,
hintText: L10n.of(context).username, prefixText: '@',
errorText: controller.usernameError, prefixStyle: const TextStyle(fontWeight: FontWeight.bold),
labelText: L10n.of(context).username, suffixStyle: const TextStyle(fontWeight: FontWeight.w200),
prefixText: '@', suffixText: ':${controller.domain}'),
suffixText: ),
':${Matrix.of(context).getLoginClient().homeserver.host}'),
), ),
), Padding(
const Divider(), padding: const EdgeInsets.all(12.0),
ListTile( child: TextFormField(
title: Text(L10n.of(context).chooseAStrongPassword), readOnly: controller.loading,
subtitle: Text(L10n.of(context).newPasswordDescription), autocorrect: false,
), autofillHints:
Padding( controller.loading ? null : [AutofillHints.password],
padding: const EdgeInsets.all(12.0), controller: controller.passwordController,
child: TextField( obscureText: !controller.showPassword,
readOnly: controller.loading, validator: controller.password1TextFieldValidator,
autocorrect: false, decoration: InputDecoration(
autofillHints: prefixIcon: const Icon(Icons.vpn_key_outlined),
controller.loading ? null : [AutofillHints.password], hintText: '****',
controller: controller.passwordController, suffixIcon: IconButton(
obscureText: !controller.showPassword, tooltip: L10n.of(context).showPassword,
onSubmitted: controller.signup, icon: Icon(controller.showPassword
decoration: InputDecoration( ? Icons.visibility_off_outlined
prefixIcon: const Icon(Icons.lock_outlined), : Icons.visibility_outlined),
hintText: '****', onPressed: controller.toggleShowPassword,
errorText: controller.passwordError, ),
suffixIcon: IconButton( labelText: L10n.of(context).password,
tooltip: L10n.of(context).showPassword,
icon: Icon(controller.showPassword
? Icons.visibility_off_outlined
: Icons.visibility_outlined),
onPressed: controller.toggleShowPassword,
), ),
labelText: L10n.of(context).password,
), ),
), ),
), Padding(
const Divider(), padding: const EdgeInsets.all(12.0),
const SizedBox(height: 12), child: TextFormField(
Hero( readOnly: controller.loading,
tag: 'loginButton', autocorrect: false,
child: Padding( autofillHints:
padding: const EdgeInsets.symmetric(horizontal: 12), controller.loading ? null : [AutofillHints.password],
child: ElevatedButton( controller: controller.passwordController2,
onPressed: controller.loading ? null : controller.signup, obscureText: true,
child: controller.loading validator: controller.password2TextFieldValidator,
? const LinearProgressIndicator() decoration: const InputDecoration(
: Text(L10n.of(context).signUp), 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),
),
),
),
],
),
), ),
), ),
); );

View File

@ -5,6 +5,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'uia_request_manager.dart';
extension LocalizedExceptionExtension on Object { extension LocalizedExceptionExtension on Object {
String toLocalizedString(BuildContext context) { String toLocalizedString(BuildContext context) {
if (this is MatrixException) { if (this is MatrixException) {
@ -48,6 +50,7 @@ extension LocalizedExceptionExtension on Object {
if (this is MatrixConnectionException || this is SocketException) { if (this is MatrixConnectionException || this is SocketException) {
return L10n.of(context).noConnectionToTheServer; return L10n.of(context).noConnectionToTheServer;
} }
if (this is UiaException) return toString();
Logs().w('Something went wrong: ', this); Logs().w('Something went wrong: ', this);
return L10n.of(context).oopsSomethingWentWrong; return L10n.of(context).oopsSomethingWentWrong;
} }

View File

@ -1,18 +1,24 @@
import 'package:flutter/material.dart'; import 'dart:async';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
extension UiaRequestManager on MatrixState { extension UiaRequestManager on MatrixState {
Future uiaRequestHandler(UiaRequest uiaRequest) async { Future uiaRequestHandler(UiaRequest uiaRequest) async {
try { try {
if (uiaRequest.state != UiaRequestState.waitForUser || 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; final stage = uiaRequest.nextStages.first;
Logs().d('Uia Request Stage: $stage');
switch (stage) { switch (stage) {
case AuthenticationTypes.password: case AuthenticationTypes.password:
final input = cachedPassword ?? final input = cachedPassword ??
@ -31,7 +37,9 @@ extension UiaRequestManager on MatrixState {
], ],
)) ))
?.single; ?.single;
if (input?.isEmpty ?? true) return; if (input?.isEmpty ?? true) {
return uiaRequest.cancel();
}
return uiaRequest.completeStage( return uiaRequest.completeStage(
AuthenticationPassword( AuthenticationPassword(
session: uiaRequest.session, session: uiaRequest.session,
@ -40,35 +48,18 @@ extension UiaRequestManager on MatrixState {
), ),
); );
case AuthenticationTypes.emailIdentity: case AuthenticationTypes.emailIdentity:
final emailInput = await showTextInputDialog( if (currentThreepidCreds == null || currentClientSecret == null) {
context: navigatorContext, return uiaRequest.cancel(
message: L10n.of(context).serverRequiresEmail, UiaException(L10n.of(widget.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));
} }
final clientSecret = DateTime.now().millisecondsSinceEpoch.toString();
final currentThreepidCreds = await client.requestTokenToRegisterEmail(
clientSecret,
emailInput.single,
0,
);
final auth = AuthenticationThreePidCreds( final auth = AuthenticationThreePidCreds(
session: uiaRequest.session, session: uiaRequest.session,
type: AuthenticationTypes.emailIdentity, type: AuthenticationTypes.emailIdentity,
threepidCreds: [ threepidCreds: [
ThreepidCreds( ThreepidCreds(
sid: currentThreepidCreds.sid, sid: currentThreepidCreds.sid,
clientSecret: clientSecret, clientSecret: currentClientSecret,
), ),
], ],
); );
@ -92,24 +83,41 @@ extension UiaRequestManager on MatrixState {
), ),
); );
default: default:
await launch( final url = Uri.parse(client.homeserver.toString() +
client.homeserver.toString() + '/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}');
'/_matrix/client/r0/auth/$stage/fallback/web?session=${uiaRequest.session}', if (PlatformInfos.isMobile) {
); final browser = UiaFallbackBrowser();
if (OkCancelResult.ok == browser.addMenuItem(
await showOkCancelAlertDialog( ChromeSafariBrowserMenuItem(
useRootNavigator: false, action: (_, __) {
message: L10n.of(context).pleaseFollowInstructionsOnWeb, uiaRequest.cancel();
context: navigatorContext, },
okLabel: L10n.of(context).next, label: L10n.of(context).cancel,
cancelLabel: L10n.of(context).cancel, id: 0,
)) { ),
return uiaRequest.completeStage(
AuthenticationData(session: uiaRequest.session),
); );
await browser.open(url: url);
await browser.whenClosed.stream.first;
} else { } 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) { } catch (e, s) {
Logs().e('Error while background UIA', 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<bool> whenClosed = StreamController<bool>.broadcast();
@override
onClosed() => whenClosed.add(true);
}

View File

@ -75,6 +75,9 @@ class MatrixState extends State<Matrix> with WidgetsBindingObserver {
int getClientIndexByMatrixId(String matrixId) => int getClientIndexByMatrixId(String matrixId) =>
widget.clients.indexWhere((client) => client.userID == matrixId); widget.clients.indexWhere((client) => client.userID == matrixId);
String currentClientSecret;
RequestTokenResponse currentThreepidCreds;
int get _safeActiveClient { int get _safeActiveClient {
if (widget.clients.isEmpty) { if (widget.clients.isEmpty) {
widget.clients.add(getLoginClient()); widget.clients.add(getLoginClient());