From 3d04aaeac5f3b33f7a92d2347b9610332446a6ad Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Sat, 22 May 2021 10:25:37 +0200 Subject: [PATCH] feat: Redesign SSO login --- assets/l10n/intl_en.arb | 15 ++ lib/pages/homeserver_picker.dart | 74 +--------- lib/pages/sign_up.dart | 91 ++++++++++++ lib/pages/views/sign_up_view.dart | 222 +++++++++++++++++++----------- 4 files changed, 249 insertions(+), 153 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f1ef6f49..7c0de10f 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -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", diff --git a/lib/pages/homeserver_picker.dart b/lib/pages/homeserver_picker.dart index f33c3af6..ecf01b2d 100644 --- a/lib/pages/homeserver_picker.dart +++ b/lib/pages/homeserver_picker.dart @@ -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 { 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 { 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)))); diff --git a/lib/pages/sign_up.dart b/lib/pages/sign_up.dart index 0d186974..db0451b6 100644 --- a/lib/pages/sign_up.dart +++ b/lib/pages/sign_up.dart @@ -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 { 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 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); diff --git a/lib/pages/views/sign_up_view.dart b/lib/pages/views/sign_up_view.dart index e9aaa46a..5f6bb25a 100644 --- a/lib/pages/views/sign_up_view.dart +++ b/lib/pages/views/sign_up_view.dart @@ -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: [ - 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: [ + 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), + ), + ), + ]), + ), + ]); + }), ), ); }