Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for app state argument #485

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
40 changes: 30 additions & 10 deletions auth0_flutter/lib/auth0_flutter_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ class Auth0Web {
Auth0Web(final String domain, final String clientId)
: _account = Account(domain, clientId);

/// Get the app state that was provided during a previous call
/// to [loginWithRedirect].
///
/// This method should be called after calling [onLoad].
///
/// This getter can only be called once,
/// after which it will return `null` for subsequent invocations.
Future<Object?> get appState => Auth0FlutterWebPlatform.instance.appState;

/// Initializes the client.
///
/// This should be called during the loading phase of your application. If
Expand Down Expand Up @@ -101,22 +110,33 @@ 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).
Future<void> loginWithRedirect(
{final String? audience,
final String? redirectUrl,
final String? organizationId,
final String? invitationUrl,
final int? maxAge,
final Set<String>? scopes,
final Map<String, String> parameters = const {}}) =>
Auth0FlutterWebPlatform.instance.loginWithRedirect(LoginOptions(
/// * The [appState] can be used to pass custom state to the callback URL.
/// This app state can be any value,
/// as long as it can be converted into a Javascript literal.
///
/// See https://api.dart.dev/dart-js_interop/NullableObjectUtilExtension/jsify.html
Future<void> loginWithRedirect({
final Object? appState,
final String? audience,
final String? redirectUrl,
final String? organizationId,
final String? invitationUrl,
final int? maxAge,
final Set<String>? scopes,
final Map<String, String> parameters = const {},
}) =>
Auth0FlutterWebPlatform.instance.loginWithRedirect(
LoginOptions(
appState: appState,
audience: audience,
redirectUrl: redirectUrl,
organizationId: organizationId,
invitationUrl: invitationUrl,
scopes: scopes ?? {},
idTokenValidationConfig: IdTokenValidationConfig(maxAge: maxAge),
parameters: parameters));
parameters: parameters,
),
);

/// Opens a popup with the `/authorize` URL using the parameters provided as
/// parameters.
Expand Down
48 changes: 42 additions & 6 deletions auth0_flutter/lib/src/web/auth0_flutter_plugin_real.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,54 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
Auth0FlutterWebClientProxy? clientProxy;
UrlSearchProvider urlSearchProvider = () => window.location.search;

/// The app state that was passed through [loginWithRedirect]
/// and retrieved in [initialize].
///
/// This object is always a Dart object, never a JS object.
///
/// When the login completes with the redirect, the page is reloaded.
/// Thus clearing this object is not needed,
/// as the actual state is managed across reloads,
/// using the transaction manager.
Object? _appState;
Copy link
Author

@navaronbracke navaronbracke Nov 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This property & getter are new. We provide these now, so that:
a) users can retrieve the app state
b) we do not need to change the API for initialize/onLoad (the app state is web only anyway? Or so I think?)

In the future, we might want to move the redirect state to the result of onLoad() / initialize() directly. That would make the API cleaner, but that does involve a breaking change, so the plugin would have to do this in version 2.0.0


@override
Future<Object?> get appState {
if (_appState != null) {
final Object? appState = _appState;
_appState = null;

return Future<Object?>.value(appState);
}

return Future<Object?>.value();
}

@override
Future<void> initialize(
final ClientOptions clientOptions, final UserAgent userAgent) async {
final ClientOptions clientOptions,
final UserAgent userAgent,
) async {
clientProxy ??= Auth0FlutterWebClientProxy(
client: interop.Auth0Client(JsInteropUtils.stripNulls(
clientOptions.toAuth0ClientOptions(userAgent))));
client: interop.Auth0Client(
JsInteropUtils.stripNulls(
clientOptions.toAuth0ClientOptions(userAgent),
),
),
);

final search = urlSearchProvider();

if (search?.contains('state=') == true &&
(search?.contains('code=') == true ||
search?.contains('error=') == true)) {
try {
return await clientProxy!.handleRedirectCallback();
final interop.RedirectLoginResult result =
await clientProxy!.handleRedirectCallback();

_appState = JsInteropUtils.dartifyObject(result.appState);

return;
} catch (e) {
throw WebExceptionExtension.fromJsObject(e);
}
Expand All @@ -61,8 +95,10 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {
: null),
options?.parameters ?? {}));

final loginOptions =
interop.RedirectLoginOptions(authorizationParams: authParams);
final loginOptions = interop.RedirectLoginOptions(
appState: JsInteropUtils.jsifyObject(options?.appState),
authorizationParams: authParams,
);

return client.loginWithRedirect(loginOptions);
}
Expand Down
5 changes: 5 additions & 0 deletions auth0_flutter/lib/src/web/auth0_flutter_plugin_stub.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ class Auth0FlutterPlugin extends Auth0FlutterWebPlatform {

static void registerWith(final Registrar registrar) {}

@override
Future<Object?> get appState {
throw UnsupportedError('appState is only supported on the web platform');
}

@override
Future<void> initialize(
final ClientOptions clientOptions, final UserAgent userAgent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ class Auth0FlutterWebClientProxy {
[final GetTokenSilentlyOptions? options]) =>
promiseToFuture(client.getTokenSilently(options));

Future<void> handleRedirectCallback() =>
promiseToFuture(client.handleRedirectCallback());
Future<RedirectLoginResult> handleRedirectCallback([final String? url]) {
// Omit the url if it is not provided, so that the default argument is used.
if (url == null) {
return promiseToFuture(client.handleRedirectCallback());
} else {
return promiseToFuture(client.handleRedirectCallback(url));
}
}

Future<bool> isAuthenticated() => promiseToFuture(client.isAuthenticated());

Expand Down
33 changes: 26 additions & 7 deletions auth0_flutter/lib/src/web/js_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,25 @@ class AuthorizationParams {
@JS()
@anonymous
class RedirectLoginOptions {
external Object? get appState; // TODO: use `JSAny?` when migrating to WASM
external AuthorizationParams? get authorizationParams;
external String? get fragment;

external factory RedirectLoginOptions(
{final AuthorizationParams authorizationParams, final String fragment});
external factory RedirectLoginOptions({
final Object? appState,
final AuthorizationParams authorizationParams,
final String fragment,
});
}

@JS()
@anonymous
class RedirectLoginResult {
external Object? get appState; // TODO: use `JSAny?` when migrating to WASM

external factory RedirectLoginResult({
final Object? appState,
});
}

@JS()
Expand Down Expand Up @@ -177,12 +191,17 @@ class PopupConfigOptions {
class Auth0Client {
external Auth0Client(final Auth0ClientOptions options);
external Future<void> loginWithRedirect([final RedirectLoginOptions options]);
external Future<void> loginWithPopup(
[final PopupLoginOptions? options, final PopupConfigOptions? config]);
external Future<void> handleRedirectCallback([final String? url]);
external Future<void> loginWithPopup([
final PopupLoginOptions? options,
final PopupConfigOptions? config,
]);
external Future<RedirectLoginResult> handleRedirectCallback([
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The app state is defined in the RedirectLoginResult, so we need it here too.

final String url,
]);
external Future<void> checkSession();
external Future<WebCredentials> getTokenSilently(
[final GetTokenSilentlyOptions? options]);
external Future<WebCredentials> getTokenSilently([
final GetTokenSilentlyOptions? options,
]);
external Future<bool> isAuthenticated();
external Future<void> logout([final LogoutOptions? logoutParams]);
}
18 changes: 18 additions & 0 deletions auth0_flutter/lib/src/web/js_interop_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,22 @@ class JsInteropUtils {

return obj;
}

// TODO: replace with `dartify` from `dart:js_interop_unsafe` when migrating to WASM
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added these here, to make the migration to WASM easier. Then we only need to change these two, instead of all callsites

/// Convert the Javascript object [obj] to a Dart object.
///
/// This method should only be used to convert objects
/// that do not fit into a static interop definition.
///
/// See https://api.dart.dev/dart-js_interop/JSAnyUtilityExtension/dartify.html
static Object? dartifyObject(final Object? obj) => dartify(obj);

// TODO: replace with `jsify` from `dart:js_interop_unsafe` when migrating to WASM
/// Convert the Dart object [obj] to a plain Javascript object.
///
/// This method should only be used to convert objects
/// that do not fit into a static interop definition.
///
/// See https://api.dart.dev/dart-js_interop/NullableObjectUtilExtension/jsify.html
static Object? jsifyObject(final Object? obj) => jsify(obj);
}
1 change: 1 addition & 0 deletions auth0_flutter/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependency_overrides:

dev_dependencies:
build_runner: ^2.1.8
collection: ^1.18.0
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the MapEquality in the test, hence a dev dependency

dart_jsonwebtoken: ^2.7.1
flutter_lints: ^2.0.1
flutter_test:
Expand Down
10 changes: 9 additions & 1 deletion auth0_flutter/test/mobile/authentication_api_test.mocks.dart
Original file line number Diff line number Diff line change
@@ -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/mobile/authentication_api_test.dart.
// Do not manually edit this file.

Expand All @@ -16,6 +16,8 @@ import 'authentication_api_test.dart' as _i4;
// 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
Expand Down Expand Up @@ -87,6 +89,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform {
),
)),
) as _i5.Future<_i2.Credentials>);

@override
_i5.Future<_i2.Credentials> loginWithOtp(
_i3.ApiRequest<_i3.AuthLoginWithOtpOptions>? request) =>
Expand All @@ -103,6 +106,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform {
),
)),
) as _i5.Future<_i2.Credentials>);

@override
_i5.Future<_i3.Challenge> multifactorChallenge(
_i3.ApiRequest<_i3.AuthMultifactorChallengeOptions>? request) =>
Expand All @@ -119,6 +123,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform {
),
)),
) as _i5.Future<_i3.Challenge>);

@override
_i5.Future<_i2.UserProfile> userInfo(
_i3.ApiRequest<_i3.AuthUserInfoOptions>? request) =>
Expand All @@ -135,6 +140,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform {
),
)),
) as _i5.Future<_i2.UserProfile>);

@override
_i5.Future<_i2.DatabaseUser> signup(
_i3.ApiRequest<_i3.AuthSignupOptions>? request) =>
Expand All @@ -151,6 +157,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform {
),
)),
) as _i5.Future<_i2.DatabaseUser>);

@override
_i5.Future<_i2.Credentials> renew(
_i3.ApiRequest<_i3.AuthRenewOptions>? request) =>
Expand All @@ -167,6 +174,7 @@ class MockTestPlatform extends _i1.Mock implements _i4.TestPlatform {
),
)),
) as _i5.Future<_i2.Credentials>);

@override
_i5.Future<void> resetPassword(
_i3.ApiRequest<_i3.AuthResetPasswordOptions>? request) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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/mobile/credentials_manager_test.dart.
// Do not manually edit this file.

Expand All @@ -16,6 +16,8 @@ import 'credentials_manager_test.dart' as _i3;
// 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
Expand Down Expand Up @@ -57,6 +59,7 @@ class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform {
),
)),
) as _i4.Future<_i2.Credentials>);

@override
_i4.Future<bool> clearCredentials(
_i5.CredentialsManagerRequest<_i5.RequestOptions?>? request) =>
Expand All @@ -67,6 +70,7 @@ class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform {
),
returnValue: _i4.Future<bool>.value(false),
) as _i4.Future<bool>);

@override
_i4.Future<bool> saveCredentials(
_i5.CredentialsManagerRequest<_i5.SaveCredentialsOptions>? request) =>
Expand All @@ -77,6 +81,7 @@ class MockTestPlatform extends _i1.Mock implements _i3.TestPlatform {
),
returnValue: _i4.Future<bool>.value(false),
) as _i4.Future<bool>);

@override
_i4.Future<bool> hasValidCredentials(
_i5.CredentialsManagerRequest<_i5.HasValidCredentialsOptions>?
Expand Down
Loading