feat: Redesign SSO login

This commit is contained in:
Christian Pauly 2021-05-22 10:25:37 +02:00
parent 574b2e4d6e
commit 8e1948b12e
4 changed files with 249 additions and 153 deletions

View File

@ -1908,6 +1908,21 @@
"type": "text",
"placeholders": {}
},
"or": "Or",
"@or": {
"type": "text",
"placeholders": {}
},
"login": "Login",
"@login": {
"type": "text",
"placeholders": {}
},
"useSSO": "Use single sign on",
"@useSSO": {
"type": "text",
"placeholders": {}
},
"sourceCode": "Source code",
"@sourceCode": {
"type": "text",

View File

@ -1,22 +1,12 @@
import 'dart:async';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:fluffychat/pages/views/homeserver_picker_view.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/setting_keys.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:uni_links/uni_links.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter/material.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:url_launcher/url_launcher.dart';
import '../main.dart';
import '../utils/localized_exception_extension.dart';
import 'package:universal_html/html.dart' as html;
class HomeserverPicker extends StatefulWidget {
@override
@ -28,55 +18,6 @@ class HomeserverPickerController extends State<HomeserverPicker> {
String domain = AppConfig.defaultHomeserver;
final TextEditingController homeserverController =
TextEditingController(text: AppConfig.defaultHomeserver);
StreamSubscription _intentDataStreamSubscription;
void _loginWithToken(String token) {
if (token?.isEmpty ?? true) return;
showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.login(
type: AuthenticationTypes.token,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
),
);
}
void _processIncomingUris(String text) async {
if (text == null || !text.startsWith(AppConfig.appOpenUrlScheme)) return;
AdaptivePageLayout.of(context).popUntilIsFirst();
final token = Uri.parse(text).queryParameters['loginToken'];
if (token != null) _loginWithToken(token);
}
void _initReceiveUri() {
if (!PlatformInfos.isMobile) return;
// For receiving shared Uris
_intentDataStreamSubscription = linkStream.listen(_processIncomingUris);
if (FluffyChatApp.gotInitialLink == false) {
FluffyChatApp.gotInitialLink = true;
getInitialLink().then(_processIncomingUris);
}
}
@override
void initState() {
super.initState();
_initReceiveUri();
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final token =
Uri.parse(html.window.location.href).queryParameters['loginToken'];
_loginWithToken(token);
});
}
}
@override
void dispose() {
super.dispose();
_intentDataStreamSubscription?.cancel();
}
/// Starts an analysis of the given homeserver. It uses the current domain and
/// makes sure that it is prefixed with https. Then it searches for the
@ -110,19 +51,8 @@ class HomeserverPickerController extends State<HomeserverPicker> {
AppConfig.jitsiInstance = jitsi;
}
final loginTypes = await Matrix.of(context).client.getLoginFlows();
if (loginTypes.flows
.any((flow) => flow.type == AuthenticationTypes.password)) {
await AdaptivePageLayout.of(context)
.pushNamed(AppConfig.enableRegistration ? '/signup' : '/login');
} else if (loginTypes.flows
.any((flow) => flow.type == AuthenticationTypes.sso)) {
final redirectUrl = kIsWeb
? html.window.location.href
: AppConfig.appOpenUrlScheme.toLowerCase() + '://sso';
await launch(
'${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}');
}
await AdaptivePageLayout.of(context)
.pushNamed(AppConfig.enableRegistration ? '/signup' : '/login');
} catch (e) {
AdaptivePageLayout.of(context).showSnackBar(
SnackBar(content: Text((e as Object).toLocalizedString(context))));

View File

@ -1,12 +1,24 @@
import 'dart:async';
import 'dart:io';
import 'package:adaptive_page_layout/adaptive_page_layout.dart';
import 'package:famedlysdk/famedlysdk.dart';
import 'package:file_picker_cross/file_picker_cross.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/views/sign_up_view.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:uni_links/uni_links.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:universal_html/html.dart' as html;
import '../main.dart';
class SignUp extends StatefulWidget {
@override
@ -19,6 +31,85 @@ class SignUpController extends State<SignUp> {
bool loading = false;
MatrixFile avatar;
LoginTypes _loginTypes;
StreamSubscription _intentDataStreamSubscription;
void _loginWithToken(String token) {
if (token?.isEmpty ?? true) return;
showFutureLoadingDialog(
context: context,
future: () => Matrix.of(context).client.login(
type: AuthenticationTypes.token,
token: token,
initialDeviceDisplayName: PlatformInfos.clientName,
),
);
}
void _processIncomingUris(String text) async {
if (text == null || !text.startsWith(AppConfig.appOpenUrlScheme)) return;
AdaptivePageLayout.of(context).popUntilIsFirst();
final token = Uri.parse(text).queryParameters['loginToken'];
if (token != null) _loginWithToken(token);
}
void _initReceiveUri() {
if (!PlatformInfos.isMobile) return;
// For receiving shared Uris
_intentDataStreamSubscription = linkStream.listen(_processIncomingUris);
if (FluffyChatApp.gotInitialLink == false) {
FluffyChatApp.gotInitialLink = true;
getInitialLink().then(_processIncomingUris);
}
}
@override
void initState() {
super.initState();
_initReceiveUri();
if (kIsWeb) {
WidgetsBinding.instance.addPostFrameCallback((_) {
final token =
Uri.parse(html.window.location.href).queryParameters['loginToken'];
_loginWithToken(token);
});
}
}
@override
void dispose() {
super.dispose();
_intentDataStreamSubscription?.cancel();
}
bool get passwordLoginSupported => _loginTypes.flows
.any((flow) => flow.type == AuthenticationTypes.password);
bool get ssoLoginSupported =>
_loginTypes.flows.any((flow) => flow.type == AuthenticationTypes.sso);
Future<LoginTypes> getLoginTypes() async {
_loginTypes ??= await Matrix.of(context).client.getLoginFlows();
return _loginTypes;
}
void ssoLoginAction() {
if (!kIsWeb && !PlatformInfos.isMobile) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Single sign on is not suppored on ${Platform.operatingSystem}'),
),
);
return;
}
final redirectUrl = kIsWeb
? html.window.location.href
: AppConfig.appOpenUrlScheme.toLowerCase() + '://sso';
launch(
'${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=${Uri.encodeQueryComponent(redirectUrl)}');
}
void setAvatarAction() async {
final file =
await FilePickerCross.importFromStorage(type: FileTypeCross.image);

View File

@ -7,6 +7,7 @@ import 'package:fluffychat/widgets/layouts/one_page_card.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import '../../utils/localized_exception_extension.dart';
class SignUpView extends StatelessWidget {
final SignUpController controller;
@ -28,89 +29,148 @@ class SignUpView extends StatelessWidget {
.replaceFirst('https://', ''),
),
),
body: ListView(children: <Widget>[
Hero(
tag: 'loginBanner',
child: FluffyBanner(),
),
SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TextField(
readOnly: controller.loading,
autocorrect: false,
controller: controller.usernameController,
onSubmitted: controller.signUpAction,
autofillHints:
controller.loading ? null : [AutofillHints.newUsername],
decoration: InputDecoration(
prefixIcon: Icon(Icons.account_circle_outlined),
hintText: L10n.of(context).username,
errorText: controller.usernameError,
labelText: L10n.of(context).chooseAUsername,
),
),
),
SizedBox(height: 8),
ListTile(
leading: CircleAvatar(
backgroundImage: controller.avatar == null
? null
: MemoryImage(controller.avatar.bytes),
backgroundColor: controller.avatar == null
? Theme.of(context).brightness == Brightness.dark
? Color(0xff121212)
: Colors.white
: Theme.of(context).secondaryHeaderColor,
child: controller.avatar == null
? Icon(Icons.camera_alt_outlined,
color: Theme.of(context).primaryColor)
: null,
),
trailing: controller.avatar == null
? null
: Icon(
Icons.close,
color: Colors.red,
body: FutureBuilder(
future: controller.getLoginTypes(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toLocalizedString(context),
textAlign: TextAlign.center,
),
title: Text(controller.avatar == null
? L10n.of(context).setAProfilePicture
: L10n.of(context).discardPicture),
onTap: controller.avatar == null
? controller.setAvatarAction
: controller.resetAvatarAction,
),
SizedBox(height: 16),
Hero(
tag: 'loginButton',
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: ElevatedButton(
onPressed: controller.loading ? null : controller.signUpAction,
child: controller.loading
? LinearProgressIndicator()
: Text(
L10n.of(context).signUp.toUpperCase(),
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
),
Center(
child: TextButton(
onPressed: () =>
AdaptivePageLayout.of(context).pushNamed('/login'),
child: Text(
L10n.of(context).alreadyHaveAnAccount,
style: TextStyle(
decoration: TextDecoration.underline,
color: Colors.blue,
fontSize: 16,
);
}
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
return ListView(children: <Widget>[
Hero(
tag: 'loginBanner',
child: FluffyBanner(),
),
),
),
),
]),
SizedBox(height: 16),
if (controller.passwordLoginSupported) ...{
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: TextField(
readOnly: controller.loading,
autocorrect: false,
controller: controller.usernameController,
onSubmitted: controller.signUpAction,
autofillHints: controller.loading
? null
: [AutofillHints.newUsername],
decoration: InputDecoration(
prefixIcon: Icon(Icons.account_circle_outlined),
hintText: L10n.of(context).username,
errorText: controller.usernameError,
labelText: L10n.of(context).chooseAUsername,
),
),
),
SizedBox(height: 8),
ListTile(
leading: CircleAvatar(
backgroundImage: controller.avatar == null
? null
: MemoryImage(controller.avatar.bytes),
backgroundColor: controller.avatar == null
? Theme.of(context).brightness == Brightness.dark
? Color(0xff121212)
: Colors.white
: Theme.of(context).secondaryHeaderColor,
child: controller.avatar == null
? Icon(Icons.camera_alt_outlined,
color: Theme.of(context).primaryColor)
: null,
),
trailing: controller.avatar == null
? null
: Icon(
Icons.close,
color: Colors.red,
),
title: Text(controller.avatar == null
? L10n.of(context).setAProfilePicture
: L10n.of(context).discardPicture),
onTap: controller.avatar == null
? controller.setAvatarAction
: controller.resetAvatarAction,
),
SizedBox(height: 16),
Hero(
tag: 'loginButton',
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: ElevatedButton(
onPressed:
controller.loading ? null : controller.signUpAction,
child: controller.loading
? LinearProgressIndicator()
: Text(
L10n.of(context).signUp.toUpperCase(),
style: TextStyle(
color: Colors.white, fontSize: 16),
),
),
),
),
Row(
children: [
Expanded(
child: Container(
height: 1,
color: Theme.of(context).dividerColor,
)),
Padding(
padding: const EdgeInsets.all(12.0),
child: Text(L10n.of(context).or),
),
Expanded(
child: Container(
height: 1,
color: Theme.of(context).dividerColor,
)),
],
),
},
Padding(
padding: EdgeInsets.symmetric(horizontal: 12),
child: Row(children: [
if (controller.passwordLoginSupported)
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary:
Theme.of(context).textTheme.bodyText1.color,
elevation: 2,
),
onPressed: () => AdaptivePageLayout.of(context)
.pushNamed('/login'),
child: Text(L10n.of(context).login),
),
),
if (controller.passwordLoginSupported &&
controller.ssoLoginSupported)
SizedBox(width: 12),
if (controller.ssoLoginSupported)
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
primary: Theme.of(context).secondaryHeaderColor,
onPrimary:
Theme.of(context).textTheme.bodyText1.color,
elevation: 2,
),
onPressed: controller.ssoLoginAction,
child: Text(L10n.of(context).useSSO),
),
),
]),
),
]);
}),
),
);
}