diff --git a/auth0_flutter/lib/auth0_flutter_web.dart b/auth0_flutter/lib/auth0_flutter_web.dart index cd9e5085..c0e9d435 100644 --- a/auth0_flutter/lib/auth0_flutter_web.dart +++ b/auth0_flutter/lib/auth0_flutter_web.dart @@ -1,4 +1,5 @@ import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'; + import 'src/version.dart'; export 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart' @@ -101,6 +102,8 @@ class Auth0Web { /// * Arbitrary [parameters] can be specified and then picked up in a custom /// Auth0 [Action](https://auth0.com/docs/customize/actions) or /// [Rule](https://auth0.com/docs/customize/rules). + /// * Use [openUrl] to control the redirect and not rely on the SDK to do the + /// actual redirect. Required *auth0-spa-js* `2.0.1` or later. Future loginWithRedirect( {final String? audience, final String? redirectUrl, @@ -108,6 +111,7 @@ class Auth0Web { final String? invitationUrl, final int? maxAge, final Set? scopes, + final Future Function(String url)? openUrl, final Map parameters = const {}}) => Auth0FlutterWebPlatform.instance.loginWithRedirect(LoginOptions( audience: audience, @@ -115,6 +119,7 @@ class Auth0Web { organizationId: organizationId, invitationUrl: invitationUrl, scopes: scopes ?? {}, + openUrl: openUrl, idTokenValidationConfig: IdTokenValidationConfig(maxAge: maxAge), parameters: parameters)); @@ -190,9 +195,18 @@ class Auth0Web { /// * Use [federated] to log the user out of their identity provider /// (such as Google) as well as Auth0. Only applicable if the user /// authenticated using an identity provider. [Read more about how federated logout works at Auth0](https://auth0.com/docs/logout/guides/logout-idps). - Future logout({final bool? federated, final String? returnToUrl}) => - Auth0FlutterWebPlatform.instance - .logout(LogoutOptions(federated: federated, returnTo: returnToUrl)); + /// * Use [openUrl] to control the redirect and not rely on the SDK to do the + /// actual redirect. Required *auth0-spa-js* `2.0.1` or later. + Future logout({ + final bool? federated, + final String? returnToUrl, + final Future Function(String url)? openUrl, + }) => + Auth0FlutterWebPlatform.instance.logout(LogoutOptions( + federated: federated, + returnTo: returnToUrl, + openUrl: openUrl, + )); /// Retrieves a set of credentials for the user. /// diff --git a/auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart b/auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart index bb94f974..66bbfa27 100644 --- a/auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart +++ b/auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart @@ -3,6 +3,7 @@ import 'dart:html'; import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:js/js.dart'; import 'auth0_flutter_web_platform_proxy.dart'; import 'extensions/client_options_extensions.dart'; @@ -61,8 +62,12 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform { : null), options?.parameters ?? {})); - final loginOptions = - interop.RedirectLoginOptions(authorizationParams: authParams); + final openUrl = options?.openUrl; + + final loginOptions = JsInteropUtils.stripNulls(interop.RedirectLoginOptions( + authorizationParams: authParams, + openUrl: openUrl != null ? allowInterop(openUrl) : null, + )); return client.loginWithRedirect(loginOptions); } diff --git a/auth0_flutter/lib/src/web/extensions/logout_options.extension.dart b/auth0_flutter/lib/src/web/extensions/logout_options.extension.dart index c87132d2..02d21dfc 100644 --- a/auth0_flutter/lib/src/web/extensions/logout_options.extension.dart +++ b/auth0_flutter/lib/src/web/extensions/logout_options.extension.dart @@ -1,10 +1,15 @@ +import 'dart:js'; + import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart'; import '../js_interop.dart' as interop; import '../js_interop_utils.dart'; extension LogoutOptionsExtension on LogoutOptions { - interop.LogoutOptions toClientLogoutOptions() => interop.LogoutOptions( - logoutParams: JsInteropUtils.stripNulls( - interop.LogoutParams(federated: federated, returnTo: returnTo))); + interop.LogoutOptions toClientLogoutOptions() => + JsInteropUtils.stripNulls(interop.LogoutOptions( + openUrl: openUrl != null ? allowInterop(openUrl!) : null, + logoutParams: JsInteropUtils.stripNulls( + interop.LogoutParams(federated: federated, returnTo: returnTo)), + )); } diff --git a/auth0_flutter/lib/src/web/js_interop.dart b/auth0_flutter/lib/src/web/js_interop.dart index 9367eef1..e4e7429c 100644 --- a/auth0_flutter/lib/src/web/js_interop.dart +++ b/auth0_flutter/lib/src/web/js_interop.dart @@ -44,10 +44,14 @@ class AuthorizationParams { @anonymous class RedirectLoginOptions { external AuthorizationParams? get authorizationParams; + external Future Function(String url)? openUrl; external String? get fragment; - external factory RedirectLoginOptions( - {final AuthorizationParams authorizationParams, final String fragment}); + external factory RedirectLoginOptions({ + final AuthorizationParams authorizationParams, + final String fragment, + final Future Function(String url)? openUrl, + }); } @JS() @@ -150,8 +154,12 @@ class LogoutParams { @anonymous class LogoutOptions { external LogoutParams? get logoutParams; + external Future Function(String url)? openUrl; - external factory LogoutOptions({final LogoutParams? logoutParams}); + external factory LogoutOptions({ + final LogoutParams? logoutParams, + final Future Function(String url)? openUrl, + }); } @JS() @@ -184,5 +192,5 @@ class Auth0Client { external Future getTokenSilently( [final GetTokenSilentlyOptions? options]); external Future isAuthenticated(); - external Future logout([final LogoutOptions? logoutParams]); + external Future logout([final LogoutOptions? logoutOptions]); } diff --git a/auth0_flutter/test/web/auth0_flutter_web_test.dart b/auth0_flutter/test/web/auth0_flutter_web_test.dart index e224276e..c322f6e6 100644 --- a/auth0_flutter/test/web/auth0_flutter_web_test.dart +++ b/auth0_flutter/test/web/auth0_flutter_web_test.dart @@ -12,9 +12,14 @@ import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; + import 'auth0_flutter_web_test.mocks.dart'; -@GenerateMocks([Auth0FlutterWebClientProxy]) +abstract class OpenUrl { + Future call(final String url) async {} +} + +@GenerateMocks([Auth0FlutterWebClientProxy, OpenUrl]) void main() { final auth0 = Auth0Web('test-domain', 'test-client-id'); final mockClientProxy = MockAuth0FlutterWebClientProxy(); @@ -140,6 +145,25 @@ void main() { expect(params.screen_hint, 'signup'); }); + test('loginWithRedirect supports openUrl', () async { + when(mockClientProxy.isAuthenticated()) + .thenAnswer((final _) => Future.value(false)); + + final openUrlMock = MockOpenUrl(); + await auth0.loginWithRedirect(openUrl: openUrlMock); + + final openUrl = verify(mockClientProxy.loginWithRedirect(captureAny)) + .captured + .first + .openUrl; + + expect(openUrl, isNotNull); + + await openUrl('http://open.url'); + + verify(openUrlMock('http://open.url')).called(1); + }); + test('loginWithRedirect strips options that are null', () async { when(mockClientProxy.isAuthenticated()) .thenAnswer((final _) => Future.value(false)); @@ -236,6 +260,22 @@ void main() { expect(params.returnTo, 'http://returnto.url'); }); + test('logout support openUrl', () async { + when(mockClientProxy.logout(any)).thenAnswer((final _) => Future.value()); + final openUrlMock = MockOpenUrl(); + + await auth0.logout(openUrl: openUrlMock); + + final openUrl = + verify(mockClientProxy.logout(captureAny)).captured.first.openUrl; + + expect(openUrl, isNotNull); + + await openUrl('http://open.url'); + + verify(openUrlMock('http://open.url')).called(1); + }); + test('loginWithPopup is called and succeeds', () async { when(mockClientProxy.loginWithPopup(any, any)) .thenAnswer((final _) => Future.value()); diff --git a/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart b/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart index e5aadd54..2affab28 100644 --- a/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart +++ b/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.0 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in auth0_flutter/test/web/auth0_flutter_web_test.dart. // Do not manually edit this file. @@ -10,10 +10,14 @@ import 'package:auth0_flutter/src/web/auth0_flutter_web_platform_proxy.dart' import 'package:auth0_flutter/src/web/js_interop.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; +import 'auth0_flutter_web_test.dart' as _i5; + // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values // ignore_for_file: avoid_setters_without_getters // ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member // ignore_for_file: prefer_const_constructors @@ -59,6 +63,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock Invocation.getter(#client), ), ) as _i2.Auth0Client); + @override _i4.Future loginWithRedirect(_i2.RedirectLoginOptions? options) => (super.noSuchMethod( @@ -69,6 +74,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future loginWithPopup([ _i2.PopupLoginOptions? options, @@ -85,6 +91,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future checkSession() => (super.noSuchMethod( Invocation.method( @@ -94,6 +101,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future<_i2.WebCredentials> getTokenSilently( [_i2.GetTokenSilentlyOptions? options]) => @@ -110,6 +118,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock ), )), ) as _i4.Future<_i2.WebCredentials>); + @override _i4.Future handleRedirectCallback() => (super.noSuchMethod( Invocation.method( @@ -119,6 +128,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); + @override _i4.Future isAuthenticated() => (super.noSuchMethod( Invocation.method( @@ -127,6 +137,7 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock ), returnValue: _i4.Future.value(false), ) as _i4.Future); + @override _i4.Future logout(_i2.LogoutOptions? options) => (super.noSuchMethod( Invocation.method( @@ -137,3 +148,22 @@ class MockAuth0FlutterWebClientProxy extends _i1.Mock returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); } + +/// A class which mocks [OpenUrl]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockOpenUrl extends _i1.Mock implements _i5.OpenUrl { + MockOpenUrl() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future call(String? url) => (super.noSuchMethod( + Invocation.method( + #call, + [url], + ), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); +} diff --git a/auth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dart b/auth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dart index 3d7fb656..dad07a52 100644 --- a/auth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dart +++ b/auth0_flutter_platform_interface/lib/src/auth0_flutter_web_platform.dart @@ -1,4 +1,5 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + import '../auth0_flutter_platform_interface.dart'; class StubAuth0FlutterWeb extends Auth0FlutterWebPlatform {} diff --git a/auth0_flutter_platform_interface/lib/src/login_options.dart b/auth0_flutter_platform_interface/lib/src/login_options.dart index ad8a9421..665b4408 100644 --- a/auth0_flutter_platform_interface/lib/src/login_options.dart +++ b/auth0_flutter_platform_interface/lib/src/login_options.dart @@ -8,6 +8,7 @@ class LoginOptions implements RequestOptions { final String? redirectUrl; final String? organizationId; final String? invitationUrl; + final Future Function(String url)? openUrl; final Map parameters; LoginOptions( @@ -17,6 +18,7 @@ class LoginOptions implements RequestOptions { this.redirectUrl, this.organizationId, this.invitationUrl, + this.openUrl, this.parameters = const {}}); @override @@ -29,6 +31,7 @@ class LoginOptions implements RequestOptions { 'redirectUrl': redirectUrl, 'organizationId': organizationId, 'invitationUrl': invitationUrl, + 'openUrl': openUrl, 'parameters': parameters, }; } diff --git a/auth0_flutter_platform_interface/lib/src/web/logout_options.dart b/auth0_flutter_platform_interface/lib/src/web/logout_options.dart index a41c0a9a..caf6e592 100644 --- a/auth0_flutter_platform_interface/lib/src/web/logout_options.dart +++ b/auth0_flutter_platform_interface/lib/src/web/logout_options.dart @@ -1,6 +1,7 @@ class LogoutOptions { final String? returnTo; final bool? federated; + final Future Function(String url)? openUrl; - LogoutOptions({this.returnTo, this.federated}); + LogoutOptions({this.returnTo, this.federated, this.openUrl}); }