diff --git a/lib/screens/account/me/settings_connections.dart b/lib/screens/account/me/settings_connections.dart index f07f241..34dc2e3 100644 --- a/lib/screens/account/me/settings_connections.dart +++ b/lib/screens/account/me/settings_connections.dart @@ -1,3 +1,4 @@ +import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -6,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:island/models/auth.dart'; import 'package:island/pods/network.dart'; import 'package:island/screens/account/me/settings.dart'; +import 'package:island/screens/auth/oidc.native.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; @@ -159,6 +161,15 @@ class AccountConnectionNewSheet extends HookConsumerWidget { case 'google': case 'github': case 'discord': + final token = await Navigator.of(context).push( + MaterialPageRoute( + builder: + (context) => OidcScreen( + provider: selectedProvider.value.toLowerCase(), + ), + ), + ); + print(token); break; default: showSnackBar(context, 'accountConnectionAddError'.tr()); diff --git a/lib/screens/auth/oidc.dart b/lib/screens/auth/oidc.dart new file mode 100644 index 0000000..600d0d9 --- /dev/null +++ b/lib/screens/auth/oidc.dart @@ -0,0 +1 @@ +export 'oidc.native.dart' if (dart.library.html) 'oidc.web.dart'; \ No newline at end of file diff --git a/lib/screens/auth/oidc.native.dart b/lib/screens/auth/oidc.native.dart new file mode 100644 index 0000000..57d27d6 --- /dev/null +++ b/lib/screens/auth/oidc.native.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/widgets/app_scaffold.dart'; + +class OidcScreen extends ConsumerStatefulWidget { + final String provider; + + const OidcScreen({super.key, required this.provider}); + + @override + ConsumerState createState() => _OIDCScreenState(); +} + +class _OIDCScreenState extends ConsumerState { + InAppWebViewController? _webViewController; + String? authToken; + + @override + Widget build(BuildContext context) { + final serverUrl = ref.watch(serverUrlProvider); + + return AppScaffold( + appBar: AppBar(title: Text('login').tr()), + body: InAppWebView( + initialUrlRequest: URLRequest( + url: WebUri('$serverUrl/auth/login/${widget.provider}'), + ), + onWebViewCreated: (controller) { + _webViewController = controller; + + // Register a handler to receive the token from JavaScript + controller.addJavaScriptHandler( + handlerName: 'tokenHandler', + callback: (args) { + // args[0] will be the token string + if (args.isNotEmpty && args[0] is String) { + setState(() { + authToken = args[0]; + }); + + // Return the token and close the webview + Navigator.of(context).pop(authToken); + } + }, + ); + }, + shouldOverrideUrlLoading: (controller, navigationAction) async { + final url = navigationAction.request.url; + if (url != null) { + final path = url.path; + final queryParams = url.queryParameters; + + // Check if we're on the token page + if (path.contains('/auth/token') && + queryParams.containsKey('token')) { + // Extract token from URL + final token = queryParams['token']!; + + // Return the token and close the webview + Navigator.of(context).pop(token); + return NavigationActionPolicy.CANCEL; + } + } + return NavigationActionPolicy.ALLOW; + }, + onLoadStop: (controller, url) async { + if (url != null && url.path.contains('/auth/token')) { + // Inject JavaScript to call our handler with the token + await controller.evaluateJavascript( + source: ''' + const urlParams = new URLSearchParams(window.location.search); + const token = urlParams.get('token'); + if (token) { + window.flutter_inappwebview.callHandler('tokenHandler', token); + } + ''', + ); + } + }, + ), + ); + } +} diff --git a/lib/screens/auth/oidc.web.dart b/lib/screens/auth/oidc.web.dart new file mode 100644 index 0000000..fa3ad21 --- /dev/null +++ b/lib/screens/auth/oidc.web.dart @@ -0,0 +1,78 @@ +// ignore_for_file: invalid_runtime_check_with_js_interop_types + +import 'dart:ui_web' as ui; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:island/pods/config.dart'; +import 'package:island/widgets/app_scaffold.dart'; +import 'package:web/web.dart' as web; +import 'package:flutter/material.dart'; + +class OidcScreen extends ConsumerStatefulWidget { + final String provider; + + const OidcScreen({super.key, required this.provider}); + + @override + ConsumerState createState() => _OIDCScreenState(); +} + +class _OIDCScreenState extends ConsumerState { + bool _isInitialized = false; + final String _viewType = 'oidc-iframe'; + + void _setupWebListener(String serverUrl) { + // Listen for messages from the iframe + web.window.onMessage.listen((event) { + if (event.data != null && event.data is String) { + final message = event.data as String; + if (message.startsWith("token=")) { + String token = message.replaceFirst("token=", ""); + // Return the token and close the screen + if (context.mounted) Navigator.pop(context, token); + } + } + }); + + // Create the iframe for the OIDC login + final iframe = + web.HTMLIFrameElement() + ..src = '$serverUrl/auth/login/${widget.provider}' + ..style.border = 'none' + ..width = '100%' + ..height = '100%'; + + // Add the iframe to the document body + web.document.body!.append(iframe); + + // Register the iframe as a platform view + ui.platformViewRegistry.registerViewFactory( + _viewType, + (int viewId) => iframe, + ); + + setState(() { + _isInitialized = true; + }); + } + + @override + void initState() { + super.initState(); + Future.delayed(Duration.zero, () { + final serverUrl = ref.watch(serverUrlProvider); + _setupWebListener(serverUrl); + }); + } + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar(title: Text('login').tr()), + body: + _isInitialized + ? HtmlElementView(viewType: _viewType) + : Center(child: CircularProgressIndicator()), + ); + } +}