feat: Implement registration with email

This commit is contained in:
Christian Pauly 2021-05-13 12:18:50 +02:00
parent 48bf1169bc
commit 19616f3457
5 changed files with 177 additions and 50 deletions

View File

@ -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",

View File

@ -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))));

View File

@ -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<SignUpPassword> {
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<SignUpPassword> {
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<SignUpPassword> {
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<SignUpPassword> {
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);

View File

@ -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',

View File

@ -121,57 +121,89 @@ class MatrixState extends State<Matrix> 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<Matrix> 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<String, dynamic> toJson() {
final data = <String, dynamic>{};
data['sid'] = sid;
data['client_secret'] = clientSecret;
if (idServer != null) data['id_server'] = idServer;
if (idAccessToken != null) data['id_access_token'] = idAccessToken;
return data;
}
}