diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index e635e96cd..1cd090c61 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -31,7 +31,7 @@ abstract class AppConfig { static const String howDoIGetStickersTutorial = 'https://fluffy.chat/faq/#how_do_i_get_stickers'; static const String appId = 'im.fluffychat.FluffyChat'; - static const String appOpenUrlScheme = 'im.fluffychat'; + static const String appOpenUrlScheme = 'chat.fluffy'; static const String sourceCodeUrl = 'https://github.com/krille-chan/fluffychat'; diff --git a/lib/pages/settings/settings_view.dart b/lib/pages/settings/settings_view.dart index 3b25f30b0..20b841466 100644 --- a/lib/pages/settings/settings_view.dart +++ b/lib/pages/settings/settings_view.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -119,13 +118,9 @@ class SettingsView extends StatelessWidget { }, ), FutureBuilder( - future: Matrix.of(context).client.getWellknown(), + future: Matrix.of(context).client.getAuthMetadata(), builder: (context, snapshot) { - final accountManageUrl = snapshot.data?.additionalProperties - .tryGetMap( - 'org.matrix.msc2965.authentication', - ) - ?.tryGet('account'); + final accountManageUrl = snapshot.data?.issuer; if (accountManageUrl == null) { return const SizedBox.shrink(); } @@ -133,7 +128,7 @@ class SettingsView extends StatelessWidget { leading: const Icon(Icons.account_circle_outlined), title: Text(L10n.of(context).manageAccount), trailing: const Icon(Icons.open_in_new_outlined), - onTap: () => launchUrlString( + onTap: () => launchUrl( accountManageUrl, mode: LaunchMode.inAppBrowserView, ), diff --git a/lib/pages/sign_in/sign_in_page.dart b/lib/pages/sign_in/sign_in_page.dart index 8dd4390a2..0b353065f 100644 --- a/lib/pages/sign_in/sign_in_page.dart +++ b/lib/pages/sign_in/sign_in_page.dart @@ -57,6 +57,8 @@ class SignInPage extends StatelessWidget { state.publicHomeservers.connectionState == ConnectionState.waiting, controller: viewModel.filterTextController, + autocorrect: false, + keyboardType: TextInputType.url, decoration: InputDecoration( filled: true, fillColor: theme.colorScheme.secondaryContainer, @@ -91,37 +93,27 @@ class SignInPage extends StatelessWidget { final server = publicHomeservers[i]; return RadioListTile.adaptive( value: server, - radioScaleFactor: 2, - secondary: IconButton( - icon: const Icon(Icons.link_outlined), - onPressed: () => launchUrlString( - server.homepage ?? 'https://${server.name}', - ), - ), + radioScaleFactor: + FluffyThemes.isColumnMode(context) || + { + TargetPlatform.iOS, + TargetPlatform.macOS, + }.contains(theme.platform) + ? 2 + : 1, title: Row( - spacing: 4, children: [ Expanded(child: Text(server.name ?? 'Unknown')), - ...?server.languages?.map( - (language) => Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + SizedBox.square( + dimension: 32, + child: IconButton( + icon: const Icon( + Icons.open_in_new_outlined, + size: 16, ), - color: theme.colorScheme.tertiaryContainer, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 3.0, - ), - child: Text( - language, - style: TextStyle( - fontSize: 10, - color: theme - .colorScheme - .onTertiaryContainer, - ), - ), + onPressed: () => launchUrlString( + server.homepage ?? + 'https://${server.name}', ), ), ), @@ -133,36 +125,61 @@ class SignInPage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (server.features?.isNotEmpty == true) - Row( + Wrap( spacing: 4.0, - children: server.features! - .map( - (feature) => Material( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + runSpacing: 4.0, + children: [ + ...?server.languages?.map( + (language) => Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: theme + .colorScheme + .tertiaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 3.0, ), - color: theme - .colorScheme - .secondaryContainer, - child: Padding( - padding: - const EdgeInsets.symmetric( - horizontal: 6.0, - vertical: 3.0, - ), - child: Text( - feature, - style: TextStyle( - fontSize: 10, - color: theme - .colorScheme - .onSecondaryContainer, - ), + child: Text( + language, + style: TextStyle( + fontSize: 10, + color: theme + .colorScheme + .onTertiaryContainer, ), ), ), - ) - .toList(), + ), + ), + ...server.features!.map( + (feature) => Material( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + color: theme + .colorScheme + .secondaryContainer, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 6.0, + vertical: 3.0, + ), + child: Text( + feature, + style: TextStyle( + fontSize: 10, + color: theme + .colorScheme + .onSecondaryContainer, + ), + ), + ), + ), + ), + ], ), Text( server.description ?? 'A matrix homeserver', @@ -188,22 +205,24 @@ class SignInPage extends StatelessWidget { shadowColor: theme.appBarTheme.shadowColor, child: Padding( padding: const EdgeInsets.all(16.0), - child: ElevatedButton( - onPressed: - state.loginLoading.connectionState == - ConnectionState.waiting - ? null - : () => connectToHomeserverFlow( - selectedHomserver, - context, - viewModel.setLoginLoading, - signUp, - ), - child: - state.loginLoading.connectionState == - ConnectionState.waiting - ? const CircularProgressIndicator.adaptive() - : Text(L10n.of(context).continueText), + child: SafeArea( + child: ElevatedButton( + onPressed: + state.loginLoading.connectionState == + ConnectionState.waiting + ? null + : () => connectToHomeserverFlow( + selectedHomserver, + context, + viewModel.setLoginLoading, + signUp, + ), + child: + state.loginLoading.connectionState == + ConnectionState.waiting + ? const CircularProgressIndicator.adaptive() + : Text(L10n.of(context).continueText), + ), ), ), ), diff --git a/lib/pages/sign_in/view_model/flows/check_homeserver.dart b/lib/pages/sign_in/view_model/flows/check_homeserver.dart index 895a168b3..a7ddb206b 100644 --- a/lib/pages/sign_in/view_model/flows/check_homeserver.dart +++ b/lib/pages/sign_in/view_model/flows/check_homeserver.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pages/sign_in/view_model/flows/oidc_login.dart'; import 'package:fluffychat/pages/sign_in/view_model/flows/sso_login.dart'; import 'package:fluffychat/pages/sign_in/view_model/model/public_homeserver_data.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; @@ -27,16 +28,33 @@ void connectToHomeserverFlow( } final l10n = L10n.of(context); final client = await Matrix.of(context).getLoginClient(); - final (_, _, loginFlows, _) = await client.checkHomeserver(homeserver); + final (_, _, loginFlows, authMetadata) = await client.checkHomeserver( + homeserver, + fetchAuthMetadata: true, + ); + final regLink = homeserverData.regLink; final supportsSso = loginFlows.any((flow) => flow.type == 'm.login.sso'); - if (!supportsSso) { - final regLink = homeserverData.regLink; + if ((kIsWeb || PlatformInfos.isLinux) && + (supportsSso || authMetadata != null || (signUp && regLink != null))) { + final consent = await showOkCancelAlertDialog( + context: context, + title: l10n.appWantsToUseForLogin(homeserverInput), + message: l10n.appWantsToUseForLoginDescription, + okLabel: l10n.continueText, + ); + if (consent != OkCancelResult.ok) return; + } + + if (authMetadata != null) { + await oidcLoginFlow(client, context, signUp); + } else if (supportsSso) { + await ssoLoginFlow(client, context, signUp); + } else { if (signUp && regLink != null) { await launchUrlString(regLink); } - final pathSegments = List.of( GoRouter.of(context).routeInformationProvider.value.uri.pathSegments, ); @@ -46,18 +64,10 @@ void connectToHomeserverFlow( setState(AsyncSnapshot.withData(ConnectionState.done, true)); return; } - if (kIsWeb || PlatformInfos.isLinux) { - final consent = await showOkCancelAlertDialog( - context: context, - title: l10n.appWantsToUseForLogin(homeserverInput), - message: l10n.appWantsToUseForLoginDescription, - okLabel: l10n.continueText, - ); - if (consent != OkCancelResult.ok) return; - } - await ssoLoginFlow(client, context, signUp); - setState(AsyncSnapshot.withData(ConnectionState.done, true)); + if (context.mounted) { + setState(AsyncSnapshot.withData(ConnectionState.done, true)); + } } catch (e, s) { setState(AsyncSnapshot.withError(ConnectionState.done, e, s)); if (!context.mounted) return; diff --git a/lib/pages/sign_in/view_model/flows/oidc_login.dart b/lib/pages/sign_in/view_model/flows/oidc_login.dart new file mode 100644 index 000000000..38ffd4b37 --- /dev/null +++ b/lib/pages/sign_in/view_model/flows/oidc_login.dart @@ -0,0 +1,91 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_web_auth_2/flutter_web_auth_2.dart'; +import 'package:matrix/matrix.dart'; +import 'package:universal_html/html.dart' as html; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/setting_keys.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; + +Future oidcLoginFlow( + Client client, + BuildContext context, + bool signUp, +) async { + Logs().i('Starting Matrix Native OIDC Flow...'); + final redirectUrl = kIsWeb + ? Uri.parse( + html.window.location.href, + ).resolveUri(Uri(pathSegments: ['auth.html'])) + : (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS) + ? Uri.parse('${AppConfig.appOpenUrlScheme.toLowerCase()}:/login') + : Uri.parse('http://localhost:3001/login'); + + final urlScheme = + (PlatformInfos.isMobile || PlatformInfos.isWeb || PlatformInfos.isMacOS) + ? redirectUrl.scheme + : 'http://localhost:3001'; + + final clientUri = Uri.parse(AppConfig.website); + final supportWebPlatform = + kIsWeb && + kReleaseMode && + redirectUrl.scheme == 'https' && + redirectUrl.host.contains(clientUri.host); + if (!supportWebPlatform) { + Logs().w( + 'OIDC Application Type web is not supported. Using native now. Please use this instance not in production!', + ); + } + + final oidcClientData = await client.registerOidcClient( + redirectUris: [redirectUrl], + applicationType: supportWebPlatform + ? OidcApplicationType.web + : OidcApplicationType.native, + clientInformation: OidcClientInformation( + clientName: AppSettings.applicationName.value, + clientUri: clientUri, + logoUri: Uri.parse('https://fluffy.chat/assets/favicon.png'), + tosUri: null, + policyUri: AppConfig.privacyUrl, + ), + ); + + final session = await client.initOidcLoginSession( + oidcClientData: oidcClientData, + redirectUri: redirectUrl, + prompt: signUp ? 'create' : null, + ); + + if (!PlatformInfos.isMobile && !PlatformInfos.isMacOS) { + final consent = await showOkCancelAlertDialog( + context: context, + title: L10n.of( + context, + ).appWantsToUseForLogin(client.homeserver!.toString()), + message: L10n.of(context).appWantsToUseForLoginDescription, + okLabel: L10n.of(context).continueText, + ); + if (consent != OkCancelResult.ok) return; + } + if (!context.mounted) return; + + final returnUrlString = await FlutterWebAuth2.authenticate( + url: session.authenticationUri.toString(), + callbackUrlScheme: urlScheme, + options: FlutterWebAuth2Options(useWebview: PlatformInfos.isMobile), + ); + final returnUrl = Uri.parse(returnUrlString); + final queryParameters = returnUrl.hasFragment + ? Uri.parse(returnUrl.fragment).queryParameters + : returnUrl.queryParameters; + final code = queryParameters['code'] as String; + final state = queryParameters['state'] as String; + + await client.oidcLogin(session: session, code: code, state: state); +} diff --git a/lib/pages/sign_in/view_model/flows/sso_login.dart b/lib/pages/sign_in/view_model/flows/sso_login.dart index 5eb5c3ccb..325793c16 100644 --- a/lib/pages/sign_in/view_model/flows/sso_login.dart +++ b/lib/pages/sign_in/view_model/flows/sso_login.dart @@ -13,6 +13,7 @@ Future ssoLoginFlow( BuildContext context, bool signUp, ) async { + Logs().i('Starting legacy SSO Flow...'); final redirectUrl = kIsWeb ? Uri.parse( html.window.location.href,