diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0f17bb45..a26dd12c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,6 +30,13 @@ + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 359e46fc..595592c6 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -59,6 +59,17 @@ LaunchScreen UIMainStoryboardFile Main + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + im.fluffychat + + + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/lib/app_config.dart b/lib/app_config.dart index 2d214ee3..74628efc 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -15,6 +15,7 @@ abstract class AppConfig { static String _privacyUrl = 'https://fluffychat.im/en/privacy.html'; static String get privacyUrl => _privacyUrl; static const String appId = 'im.fluffychat.FluffyChat'; + static const String appOpenUrlScheme = 'im.fluffychat'; static const String sourceCodeUrl = 'https://gitlab.com/famedly/fluffychat'; static const String supportUrl = 'https://gitlab.com/famedly/fluffychat/issues'; diff --git a/lib/config/routes.dart b/lib/config/routes.dart index ab1e971b..1002989b 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -26,7 +26,6 @@ import 'package:fluffychat/views/settings_notifications.dart'; import 'package:fluffychat/views/settings_style.dart'; import 'package:fluffychat/views/sign_up.dart'; import 'package:fluffychat/views/sign_up_password.dart'; -import 'package:fluffychat/views/sso_web_view.dart'; import 'package:flutter/material.dart'; class FluffyRoutes { @@ -47,8 +46,6 @@ class FluffyRoutes { return ViewData(mainView: (_) => HomeserverPicker()); case 'login': return ViewData(mainView: (_) => Login()); - case 'sso': - return ViewData(mainView: (_) => SsoWebView()); case 'signup': if (parts.length == 5 && parts[2] == 'password') { return ViewData( diff --git a/lib/views/homeserver_picker.dart b/lib/views/homeserver_picker.dart index 553a4be7..7c2be369 100644 --- a/lib/views/homeserver_picker.dart +++ b/lib/views/homeserver_picker.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:adaptive_page_layout/adaptive_page_layout.dart'; @@ -7,9 +8,14 @@ import 'package:fluffychat/components/matrix.dart'; import 'package:fluffychat/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flushbar/flushbar_helper.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:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../utils/localized_exception_extension.dart'; +import 'package:universal_html/prefer_universal/html.dart' as html; class HomeserverPicker extends StatefulWidget { @override @@ -21,6 +27,46 @@ class _HomeserverPickerState extends State { String _domain = AppConfig.defaultHomeserver; final TextEditingController _controller = 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, + userIdentifierType: null, + token: token, + initialDeviceDisplayName: Matrix.of(context).clientName, + ), + ); + } + + void _processIncomingSharedText(String text) async { + if (text == null || !text.startsWith(AppConfig.appOpenUrlScheme)) return; + AdaptivePageLayout.of(context).popUntilIsFirst(); + final token = Uri.parse(text).queryParameters['loginToken']; + _loginWithToken(token); + } + + void _initReceiveSharingContent() { + if (!PlatformInfos.isMobile) return; + _intentDataStreamSubscription = + ReceiveSharingIntent.getTextStream().listen(_processIncomingSharedText); + ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); + } + + @override + void initState() { + super.initState(); + _initReceiveSharingContent(); + } + + @override + void dispose() { + super.dispose(); + _intentDataStreamSubscription?.cancel(); + } void _checkHomeserverAction(BuildContext context) async { try { @@ -40,7 +86,11 @@ class _HomeserverPickerState extends State { .pushNamed(AppConfig.enableRegistration ? '/signup' : '/login'); } else if (loginTypes.flows .any((flow) => flow.type == AuthenticationTypes.sso)) { - await AdaptivePageLayout.of(context).pushNamed('/sso'); + final redirectUrl = kIsWeb + ? html.window.location.href + : '${Uri.encodeQueryComponent(AppConfig.appOpenUrlScheme.toLowerCase() + '://sso')}'; + await launch( + '${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=$redirectUrl'); } } on String catch (e) { // ignore: unawaited_futures @@ -62,6 +112,13 @@ class _HomeserverPickerState extends State { final padding = EdgeInsets.symmetric( horizontal: max((MediaQuery.of(context).size.width - 600) / 2, 0), ); + if (kIsWeb) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final token = + Uri.parse(html.window.location.href).queryParameters['loginToken']; + _loginWithToken(token); + }); + } return Scaffold( appBar: AppBar( title: DefaultAppBarSearchField( diff --git a/lib/views/sso_web_view.dart b/lib/views/sso_web_view.dart deleted file mode 100644 index 1d5f25a7..00000000 --- a/lib/views/sso_web_view.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:famedlysdk/famedlysdk.dart'; -import 'package:fluffychat/components/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -import '../app_config.dart'; - -class SsoWebView extends StatefulWidget { - @override - _SsoWebViewState createState() => _SsoWebViewState(); -} - -class _SsoWebViewState extends State { - bool _loading = false; - String _error; - - void _login(BuildContext context, String token) async { - setState(() => _loading = true); - try { - await Matrix.of(context).client.login( - type: AuthenticationTypes.token, - userIdentifierType: null, - token: token, - initialDeviceDisplayName: Matrix.of(context).clientName, - ); - } catch (e, s) { - Logs().e('Login with token failed', e, s); - setState(() => _error = e.toString()); - } - } - - @override - Widget build(BuildContext context) { - final url = - '${Matrix.of(context).client.homeserver?.toString()}/_matrix/client/r0/login/sso/redirect?redirectUrl=${Uri.encodeQueryComponent(AppConfig.appId.toLowerCase() + '://sso')}'; - return Scaffold( - appBar: AppBar( - title: Text( - L10n.of(context).logInTo(Matrix.of(context).client.homeserver?.host ?? - L10n.of(context).oopsSomethingWentWrong), - ), - ), - body: _error != null - ? Center(child: Text(_error)) - : _loading - ? Center(child: CircularProgressIndicator()) - : WebView( - initialUrl: url, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (url) { - if (url.startsWith(AppConfig.appId.toLowerCase())) { - _login(context, - Uri.parse(url).queryParameters['loginToken']); - } - }, - ), - ); - } -} diff --git a/pubspec.lock b/pubspec.lock index e8ab3569..05fab1ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1271,13 +1271,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.7.3" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.7" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f78354e5..14cd4d35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,6 @@ dependencies: path_provider: ^1.6.27 android_path_provider: ^0.1.1 permission_handler: ^5.0.1+1 - webview_flutter: ^1.0.7 share: ^0.6.5+4 flutter_secure_storage: ^3.3.5 http: ^0.12.2