diff --git a/lib/screens/account/me/settings.dart b/lib/screens/account/me/settings.dart index d86d2c7..6095469 100644 --- a/lib/screens/account/me/settings.dart +++ b/lib/screens/account/me/settings.dart @@ -156,22 +156,8 @@ class AccountSettingsScreen extends HookConsumerWidget { getLocalizedProviderName(connection.provider), ).tr(), subtitle: - connection.meta.isNotEmpty - ? Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: [ - for (final meta - in connection.meta.entries) - Text( - '${meta.key.split('_').map((word) => word[0].toUpperCase() + word.substring(1)).join(' ')}: ${meta.value}', - style: const TextStyle( - fontSize: 12, - ), - ), - ], - ) + connection.meta['email'] != null + ? Text(connection.meta['email']) : Text(connection.providedIdentifier), leading: CircleAvatar( child: getProviderIcon( @@ -184,7 +170,6 @@ class AccountSettingsScreen extends HookConsumerWidget { ), ).padding(top: 4), trailing: const Icon(Symbols.chevron_right), - isThreeLine: true, onTap: () { showModalBottomSheet( context: context, diff --git a/lib/screens/account/me/settings_connections.dart b/lib/screens/account/me/settings_connections.dart index 6dd5e5c..0605837 100644 --- a/lib/screens/account/me/settings_connections.dart +++ b/lib/screens/account/me/settings_connections.dart @@ -8,6 +8,7 @@ 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/text.dart'; import 'package:island/services/time.dart'; import 'package:island/widgets/alert.dart'; import 'package:island/widgets/content/sheet.dart'; @@ -96,6 +97,18 @@ class AccountConnectionSheet extends HookConsumerWidget { const Gap(8), Text(getLocalizedProviderName(connection.provider)).tr(), const Gap(4), + if (connection.meta.isNotEmpty) + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + for (final meta in connection.meta.entries) + Text( + '${meta.key.replaceAll('_', ' ').capitalizeEachWord()}: ${meta.value}', + style: const TextStyle(fontSize: 12), + ), + ], + ), Text( connection.providedIdentifier, style: Theme.of(context).textTheme.bodySmall, @@ -174,10 +187,12 @@ class AccountConnectionNewSheet extends HookConsumerWidget { builder: (context) => OidcScreen( provider: selectedProvider.value.toLowerCase(), - title: 'Connect with ${selectedProvider.value}', + title: + 'Connect with ${selectedProvider.value.capitalizeEachWord()}', ), ), ); + if (context.mounted) Navigator.pop(context, true); break; default: showSnackBar(context, 'accountConnectionAddError'.tr()); @@ -326,7 +341,10 @@ class AccountConnectionsSheet extends HookConsumerWidget { connection.provider, ), ).tr(), - subtitle: Text(connection.providedIdentifier), + subtitle: + connection.meta['email'] != null + ? Text(connection.meta['email']) + : Text(connection.providedIdentifier), trailing: Text( DateFormat.yMd().format( connection.lastUsedAt.toLocal(), diff --git a/lib/screens/auth/oidc.native.dart b/lib/screens/auth/oidc.native.dart index c499873..36bb70b 100644 --- a/lib/screens/auth/oidc.native.dart +++ b/lib/screens/auth/oidc.native.dart @@ -2,12 +2,15 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:gap/gap.dart'; import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/widgets/app_scaffold.dart'; +import 'package:styled_widget/styled_widget.dart'; class OidcScreen extends ConsumerStatefulWidget { final String provider; @@ -21,6 +24,15 @@ class OidcScreen extends ConsumerStatefulWidget { class _OidcScreenState extends ConsumerState { String? authToken; + String? currentUrl; + final TextEditingController _urlController = TextEditingController(); + bool _isLoading = true; + + @override + void dispose() { + _urlController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -31,74 +43,155 @@ class _OidcScreenState extends ConsumerState { appBar: AppBar( title: widget.title != null ? Text(widget.title!) : Text('login').tr(), ), - body: InAppWebView( - initialSettings: InAppWebViewSettings( - userAgent: - kIsWeb - ? null - : Platform.isIOS - ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' - : Platform.isAndroid - ? 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' - : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', - ), - initialUrlRequest: URLRequest( - url: WebUri( - (token?.token.isNotEmpty ?? false) - ? '$serverUrl/auth/login/${widget.provider}?tk=${token!.token}' - : '$serverUrl/auth/login/${widget.provider}', - ), - ), - onWebViewCreated: (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) { + body: Column( + children: [ + Expanded( + child: InAppWebView( + initialSettings: InAppWebViewSettings( + userAgent: + kIsWeb + ? null + : Platform.isIOS + ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1' + : Platform.isAndroid + ? 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36' + : 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + ), + initialUrlRequest: URLRequest( + url: WebUri('$serverUrl/auth/login/${widget.provider}'), + headers: { + if (token?.token.isNotEmpty ?? false) + 'Authorization': 'AtField ${token!.token}', + }, + ), + onWebViewCreated: (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) { + setState(() { + currentUrl = url.toString(); + _urlController.text = currentUrl ?? ''; + _isLoading = true; + }); + + final path = url.path; + final queryParams = url.queryParameters; + + // Check if we're on the token page + if (path.contains('/auth/callback')) { + // Extract token from URL + final token = queryParams['token'] ?? true; + // Return the token and close the webview + Navigator.of(context).pop(token); + return NavigationActionPolicy.CANCEL; + } + } + return NavigationActionPolicy.ALLOW; + }, + onUpdateVisitedHistory: (controller, url, androidIsReload) { + if (url != null) { + setState(() { + currentUrl = url.toString(); + _urlController.text = currentUrl ?? ''; + }); + } + }, + onLoadStop: (controller, url) { setState(() { - authToken = args[0]; + _isLoading = false; }); - - // 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); - } - ''', - ); - } - }, + }, + onLoadStart: (controller, url) { + setState(() { + _isLoading = true; + }); + }, + onLoadError: (controller, url, code, message) { + setState(() { + _isLoading = false; + }); + }, + ), + ), + // Loading progress indicator + if (_isLoading) + LinearProgressIndicator( + color: Theme.of(context).colorScheme.primary, + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.zero, + stopIndicatorRadius: 0, + minHeight: 2, + ) + else + ColoredBox( + color: Theme.of(context).colorScheme.surfaceVariant, + ).height(2), + // Debug location bar (only visible in debug mode) + Container( + padding: EdgeInsets.only( + left: 16, + right: 0, + bottom: MediaQuery.of(context).padding.bottom + 8, + top: 8, + ), + color: Theme.of(context).colorScheme.surface, + child: Row( + children: [ + Expanded( + child: TextField( + controller: _urlController, + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 8, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(4), + ), + hintText: 'URL', + ), + style: const TextStyle(fontSize: 12), + readOnly: true, + ), + ), + const Gap(4), + IconButton( + icon: const Icon(Icons.copy, size: 20), + padding: const EdgeInsets.all(4), + constraints: const BoxConstraints(), + onPressed: () { + if (currentUrl != null) { + Clipboard.setData(ClipboardData(text: currentUrl!)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('copyToClipboard').tr(), + duration: const Duration(seconds: 1), + ), + ); + } + }, + ), + ], + ), + ), + ], ), ); } diff --git a/lib/services/text.dart b/lib/services/text.dart new file mode 100644 index 0000000..71d8bc5 --- /dev/null +++ b/lib/services/text.dart @@ -0,0 +1,14 @@ +extension StringExtension on String { + String capitalizeEachWord() { + if (isEmpty) return this; + + return split(' ') + .map( + (word) => + word.isNotEmpty + ? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}' + : '', + ) + .join(' '); + } +}