From 51128b587c74ff8a5461fc477dbba14d8289e398 Mon Sep 17 00:00:00 2001 From: Simon Lightfoot Date: Thu, 28 Nov 2024 21:28:56 +0000 Subject: [PATCH] fix: improve SSO popup UX #33 (#50) --- packages/clerk_flutter/example/lib/main.dart | 1 + .../clerk_authentication_widget.dart | 20 +++- .../widgets/control/clerk_auth_provider.dart | 101 ++++++++++-------- .../widgets/ui/social_connection_button.dart | 5 +- .../src/widgets/user/clerk_user_button.dart | 37 ++++--- 5 files changed, 97 insertions(+), 67 deletions(-) diff --git a/packages/clerk_flutter/example/lib/main.dart b/packages/clerk_flutter/example/lib/main.dart index 67670b8..55bdc31 100644 --- a/packages/clerk_flutter/example/lib/main.dart +++ b/packages/clerk_flutter/example/lib/main.dart @@ -57,6 +57,7 @@ class _ExampleAppState extends State { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, home: ClerkAuth( publicKey: publicKey, publishableKey: widget.publishableKey, diff --git a/packages/clerk_flutter/lib/src/widgets/authentication/clerk_authentication_widget.dart b/packages/clerk_flutter/lib/src/widgets/authentication/clerk_authentication_widget.dart index 33ba35b..74bbd74 100644 --- a/packages/clerk_flutter/lib/src/widgets/authentication/clerk_authentication_widget.dart +++ b/packages/clerk_flutter/lib/src/widgets/authentication/clerk_authentication_widget.dart @@ -1,4 +1,5 @@ import 'package:clerk_auth/clerk_auth.dart' as clerk; +import 'package:clerk_auth/clerk_auth.dart'; import 'package:clerk_flutter/clerk_flutter.dart'; import 'package:clerk_flutter/src/assets.dart'; import 'package:flutter/gestures.dart'; @@ -50,20 +51,29 @@ class _ClerkAuthenticationWidgetState extends State { ClerkAuthBuilder( builder: (context, auth) { return Closeable( - closed: auth.signIn is clerk.SignIn || - auth.signUp is clerk.SignUp, + closed: (auth.signIn is clerk.SignIn && + auth.signIn!.status.isActive) || + (auth.signUp is clerk.SignUp && + auth.signUp!.status.isActive), child: const ClerkSSOPanel(), ); }, ), Closeable( - open: _state.isSigningIn, child: const ClerkSignInPanel()), + open: _state.isSigningIn, + child: const ClerkSignInPanel(), + ), Closeable( - open: _state.isSigningUp, child: const ClerkSignUpPanel()), + open: _state.isSigningUp, + child: const ClerkSignUpPanel(), + ), const ClerkErrorMessage(), ], ), - bottomPortion: _BottomPortion(state: _state, onChange: _toggle), + bottomPortion: _BottomPortion( + state: _state, + onChange: _toggle, + ), ), ); } diff --git a/packages/clerk_flutter/lib/src/widgets/control/clerk_auth_provider.dart b/packages/clerk_flutter/lib/src/widgets/control/clerk_auth_provider.dart index 8c692df..73354fa 100644 --- a/packages/clerk_flutter/lib/src/widgets/control/clerk_auth_provider.dart +++ b/packages/clerk_flutter/lib/src/widgets/control/clerk_auth_provider.dart @@ -51,10 +51,11 @@ class ClerkAuthProvider extends clerk.Auth with ChangeNotifier { final _errors = StreamController(); final OverlayEntry _loadingOverlay; - OverlayEntry? _ssoOverlay; static const _kRotatingTokenNonce = 'rotating_token_nonce'; + static const _kSsoRouteName = 'clerk_sso_popup'; + @override void update() => notifyListeners(); @@ -71,53 +72,42 @@ class ClerkAuthProvider extends clerk.Auth with ChangeNotifier { void Function(clerk.AuthError)? onError, }) async { final auth = ClerkAuth.of(context); - final overlay = Overlay.of(context); final client = await call( context, () => auth.oauthSignIn(strategy: strategy), onError: onError, ); final url = client?.signIn?.firstFactorVerification?.providerUrl; - if (url case String url) { - _ssoOverlay = OverlayEntry( - builder: (BuildContext context) { - return _SsoWebViewHost( - url: url, - callback: _ssoCallback( - strategy, - onError: onError, - auth: auth, - ), - ); - }, + if (url != null && context.mounted) { + final redirectUrl = await showDialog( + context: context, + useSafeArea: false, + useRootNavigator: true, + routeSettings: const RouteSettings(name: _kSsoRouteName), + builder: (context) => _SsoWebViewOverlay(url: url), ); - overlay.insert(_ssoOverlay!); - } - } - - Function(BuildContext, String) _ssoCallback( - clerk.Strategy strategy, { - void Function(clerk.AuthError)? onError, - required ClerkAuthProvider auth, - }) { - return (BuildContext context, String redirectUrl) async { - final uri = Uri.parse(redirectUrl); - final token = uri.queryParameters[_kRotatingTokenNonce]; - if (token case String token) { - await call( - context, - () => auth.attemptSignIn(strategy: strategy, token: token), - onError: onError, - ); - } else { - await auth.refreshClient(); + if (redirectUrl != null && context.mounted) { + final uri = Uri.parse(redirectUrl); + final token = uri.queryParameters[_kRotatingTokenNonce]; + if (token case String token) { + await call( + context, + () => auth.attemptSignIn(strategy: strategy, token: token), + onError: onError, + ); + } else { + await auth.refreshClient(); + if (context.mounted) { + await call(context, () => auth.transfer(), onError: onError); + } + } if (context.mounted) { - await call(context, () => auth.transfer(), onError: onError); + Navigator.of(context).popUntil( + (route) => route.settings.name != _kSsoRouteName, + ); } } - _ssoOverlay?.remove(); - _ssoOverlay = null; - }; + } } /// Convenience method to make an auth call to the backend via ClerkAuth @@ -198,21 +188,20 @@ class ClerkAuthProvider extends clerk.Auth with ChangeNotifier { _errors.add(clerk.AuthError(message: message)); } -class _SsoWebViewHost extends StatefulWidget { - const _SsoWebViewHost({ +class _SsoWebViewOverlay extends StatefulWidget { + const _SsoWebViewOverlay({ required this.url, - required this.callback, }); final String url; - final Function(BuildContext context, String redirectUrl) callback; @override - State<_SsoWebViewHost> createState() => _SsoWebViewHostState(); + State<_SsoWebViewOverlay> createState() => _SsoWebViewOverlayState(); } -class _SsoWebViewHostState extends State<_SsoWebViewHost> { +class _SsoWebViewOverlayState extends State<_SsoWebViewOverlay> { late final WebViewController controller; + var _title = Future.value('Loading…'); @override void initState() { @@ -220,11 +209,17 @@ class _SsoWebViewHostState extends State<_SsoWebViewHost> { controller = WebViewController() ..setUserAgent('Clerk Flutter SDK v${clerk.Auth.jsVersion}') ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.white) ..setNavigationDelegate( NavigationDelegate( + onPageFinished: (_) => _updateTitle(), onNavigationRequest: (NavigationRequest request) async { if (request.url.startsWith(clerk.Auth.oauthRedirect)) { - widget.callback(context, request.url); + scheduleMicrotask(() { + if (mounted) { + Navigator.of(context).pop(request.url); + } + }); return NavigationDecision.prevent; } return NavigationDecision.navigate; @@ -234,9 +229,25 @@ class _SsoWebViewHostState extends State<_SsoWebViewHost> { controller.loadRequest(Uri.parse(widget.url)); } + void _updateTitle() { + setState(() { + _title = controller.getTitle(); + }); + } + @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: FutureBuilder( + future: _title, + builder: (context, snapshot) { + return Text(snapshot.data ?? ''); + }, + ), + actions: const [CloseButton()], + ), body: WebViewWidget(controller: controller), ); } diff --git a/packages/clerk_flutter/lib/src/widgets/ui/social_connection_button.dart b/packages/clerk_flutter/lib/src/widgets/ui/social_connection_button.dart index ddce4e1..1c15019 100644 --- a/packages/clerk_flutter/lib/src/widgets/ui/social_connection_button.dart +++ b/packages/clerk_flutter/lib/src/widgets/ui/social_connection_button.dart @@ -20,8 +20,9 @@ class SocialConnectionButton extends StatelessWidget { return SizedBox( height: 30.0, child: MaterialButton( - onPressed: () => - ClerkAuth.above(context).sso(context, connection.strategy), + onPressed: () { + ClerkAuth.above(context).sso(context, connection.strategy); + }, elevation: 2.0, shape: RoundedRectangleBorder( borderRadius: borderRadius4, diff --git a/packages/clerk_flutter/lib/src/widgets/user/clerk_user_button.dart b/packages/clerk_flutter/lib/src/widgets/user/clerk_user_button.dart index c340d38..fa6dbfa 100644 --- a/packages/clerk_flutter/lib/src/widgets/user/clerk_user_button.dart +++ b/packages/clerk_flutter/lib/src/widgets/user/clerk_user_button.dart @@ -356,27 +356,34 @@ class _SessionRow extends StatelessWidget { auth.call(context, () => auth.signOut()); } else { auth.call( - context, () => auth.signOutOf(session)); + context, + () => auth.signOutOf(session), + ); } }, - label: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Icon(Icons.logout, - color: ClerkColors.charcoalGrey, size: 11), - horizontalMargin8, - Text( - translator.translate('Sign Out'), - style: ClerkTextStyle.buttonSubtitle.copyWith( - fontSize: 8, + label: Padding( + padding: verticalPadding4, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + Icons.logout, color: ClerkColors.charcoalGrey, + size: 20, ), - ), - ], + horizontalMargin8, + Text( + translator.translate('Sign Out'), + style: ClerkTextStyle.buttonSubtitle.copyWith( + fontSize: 12, + color: ClerkColors.charcoalGrey, + ), + ), + ], + ), ), style: ClerkMaterialButtonStyle.light, - height: 16, ), ), ],