diff --git a/auth0_flutter/lib/auth0_flutter_web.dart b/auth0_flutter/lib/auth0_flutter_web.dart index e259fbc38..ee8e77e43 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' @@ -115,6 +116,8 @@ class Auth0Web { /// * [scopes] defaults to `openid profile email`. You can override these /// scopes, but `openid` is always requested regardless of this setting. /// * If you want to log into a specific organization, provide the + /// * 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. /// [organizationId]. Provide [invitationUrl] if a user has been invited /// to join an organization. /// * Arbitrary [parameters] can be specified and then picked up in a custom @@ -133,6 +136,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( @@ -143,6 +147,7 @@ class Auth0Web { organizationId: organizationId, invitationUrl: invitationUrl, scopes: scopes ?? {}, + openUrl: openUrl, idTokenValidationConfig: IdTokenValidationConfig(maxAge: maxAge), parameters: parameters, ), @@ -220,9 +225,20 @@ 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 6c2e5b9c1..8236cebda 100644 --- a/auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart +++ b/auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart @@ -83,10 +83,13 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform { : null), options?.parameters ?? {})); - final loginOptions = interop.RedirectLoginOptions( + final openUrl = options?.openUrl; + + final loginOptions = JsInteropUtils.stripNulls(interop.RedirectLoginOptions( appState: options?.appState.jsify(), authorizationParams: authParams, - ); + openUrl: openUrl, + )); 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 c87132d28..02d21dfc8 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 8b5f81328..a9f227c0d 100644 --- a/auth0_flutter/lib/src/web/js_interop.dart +++ b/auth0_flutter/lib/src/web/js_interop.dart @@ -46,11 +46,13 @@ extension type RedirectLoginOptions._(JSObject _) implements JSObject { external JSAny? get appState; external AuthorizationParams? get authorizationParams; external String? get fragment; + external Future Function(String url)? openUrl; external factory RedirectLoginOptions({ final JSAny? appState, final AuthorizationParams authorizationParams, final String fragment, + final Future Function(String url)? openUrl, }); } @@ -169,8 +171,12 @@ extension type LogoutParams._(JSObject _) implements JSObject { @anonymous extension type LogoutOptions._(JSObject _) implements JSObject { 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() @@ -213,7 +219,7 @@ extension type Auth0Client._(JSObject _) implements JSObject { final GetTokenSilentlyOptions? options, ]); external JSPromise isAuthenticated(); - external JSPromise logout([final LogoutOptions? logoutParams]); + external JSPromise logout([final LogoutOptions? logoutOptions]); } // TODO: remove this extension when updating to Dart 3.6.0 diff --git a/auth0_flutter/test/web/auth0_flutter_web_test.dart b/auth0_flutter/test/web/auth0_flutter_web_test.dart index b855d4399..ca87aa92d 100644 --- a/auth0_flutter/test/web/auth0_flutter_web_test.dart +++ b/auth0_flutter/test/web/auth0_flutter_web_test.dart @@ -15,7 +15,11 @@ 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(); @@ -231,6 +235,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)); @@ -327,6 +350,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 64c2514ca..6c9390356 100644 --- a/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart +++ b/auth0_flutter/test/web/auth0_flutter_web_test.mocks.dart @@ -12,6 +12,8 @@ import 'package:auth0_flutter/src/web/js_interop.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; import 'auth0_extension_type_mocks.dart'; +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 @@ -172,3 +174,19 @@ 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 43d7b0170..876952441 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 6bcce02ee..c68befc9c 100644 --- a/auth0_flutter_platform_interface/lib/src/login_options.dart +++ b/auth0_flutter_platform_interface/lib/src/login_options.dart @@ -9,6 +9,7 @@ class LoginOptions implements RequestOptions { final String? redirectUrl; final String? organizationId; final String? invitationUrl; + final Future Function(String url)? openUrl; final Map parameters; LoginOptions({ @@ -19,6 +20,7 @@ class LoginOptions implements RequestOptions { this.redirectUrl, this.organizationId, this.invitationUrl, + this.openUrl, this.parameters = const {}, }); @@ -32,6 +34,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 a41c0a9a7..caf6e5928 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}); }