From 04eab283bad3c00330886bc2075f9e7763b60f79 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 15 Aug 2024 17:17:31 +0200 Subject: [PATCH 01/11] feat: broadcast auth events on web --- packages/gotrue/lib/src/broadcast_stub.dart | 6 ++ packages/gotrue/lib/src/broadcast_web.dart | 11 ++++ packages/gotrue/lib/src/gotrue_client.dart | 62 ++++++++++++++++++- packages/gotrue/lib/src/types/types.dart | 4 ++ .../lib/src/local_storage.dart | 1 + 5 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 packages/gotrue/lib/src/broadcast_stub.dart create mode 100644 packages/gotrue/lib/src/broadcast_web.dart create mode 100644 packages/gotrue/lib/src/types/types.dart diff --git a/packages/gotrue/lib/src/broadcast_stub.dart b/packages/gotrue/lib/src/broadcast_stub.dart new file mode 100644 index 00000000..95ee5d0e --- /dev/null +++ b/packages/gotrue/lib/src/broadcast_stub.dart @@ -0,0 +1,6 @@ +import 'package:gotrue/src/types/types.dart'; + +/// Stub implementation of [BroadcastChannel] for platforms that don't support it. +BroadcastChannel getBroadcastChannel(String broadcastKey) { + throw UnimplementedError(); +} diff --git a/packages/gotrue/lib/src/broadcast_web.dart b/packages/gotrue/lib/src/broadcast_web.dart new file mode 100644 index 00000000..9c96c4aa --- /dev/null +++ b/packages/gotrue/lib/src/broadcast_web.dart @@ -0,0 +1,11 @@ +import 'dart:html' as html; + +import 'package:gotrue/src/types/types.dart'; + +BroadcastChannel getBroadcastChannel(String broadcastKey) { + final broadcast = html.BroadcastChannel(broadcastKey); + return ( + onMessage: broadcast.onMessage.map((event) => event.data.toString()), + postMessage: broadcast.postMessage, + ); +} diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 08c4e90e..230b22b2 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -9,12 +9,16 @@ import 'package:gotrue/src/fetch.dart'; import 'package:gotrue/src/helper.dart'; import 'package:gotrue/src/types/auth_response.dart'; import 'package:gotrue/src/types/fetch_options.dart'; +import 'package:gotrue/src/types/types.dart'; import 'package:http/http.dart'; import 'package:jwt_decode/jwt_decode.dart'; import 'package:meta/meta.dart'; import 'package:retry/retry.dart'; import 'package:rxdart/subjects.dart'; +import 'broadcast_stub.dart' if (dart.library.html) './broadcast_web.dart' + as web; + part 'gotrue_mfa_api.dart'; /// {@template gotrue_client} @@ -83,6 +87,9 @@ class GoTrueClient { _onAuthStateChangeControllerSync.stream; final AuthFlowType _flowType; + final bool _broadcastSession; + + BroadcastChannel? _broadcastChannel; /// {@macro gotrue_client} GoTrueClient({ @@ -92,11 +99,13 @@ class GoTrueClient { Client? httpClient, GotrueAsyncStorage? asyncStorage, AuthFlowType flowType = AuthFlowType.pkce, + bool broadcastSession = true, }) : _url = url ?? Constants.defaultGotrueUrl, _headers = headers ?? {}, _httpClient = httpClient, _asyncStorage = asyncStorage, - _flowType = flowType { + _flowType = flowType, + _broadcastSession = broadcastSession { _autoRefreshToken = autoRefreshToken ?? true; final gotrueUrl = url ?? Constants.defaultGotrueUrl; @@ -116,6 +125,8 @@ class GoTrueClient { if (_autoRefreshToken) { startAutoRefresh(); } + + mayStartBroadcastChannel(); } /// Getter for the headers @@ -1128,6 +1139,41 @@ class GoTrueClient { _currentUser = null; } + void mayStartBroadcastChannel() { + if (const bool.fromEnvironment('dart.library.html') && _broadcastSession) { + // Used by the js library as well + final broadcastKey = + "sb-${Uri.parse(_url).host.split(".").first}-auth-token"; + + _broadcastChannel = web.getBroadcastChannel(broadcastKey); + _broadcastChannel?.onMessage.listen((messageEvent) { + final Map parsedMessageEvent = json.decode(messageEvent); + final rawEvent = parsedMessageEvent['event']; + final event = switch (rawEvent) { + // Handle events from js library as well + 'INITIAL_SESSION' => AuthChangeEvent.initialSession, + 'PASSWORD_RECOVERY' => AuthChangeEvent.passwordRecovery, + 'SIGNED_IN' => AuthChangeEvent.signedIn, + 'SIGNED_OUT' => AuthChangeEvent.signedOut, + 'TOKEN_REFRESHED' => AuthChangeEvent.tokenRefreshed, + 'USER_UPDATED' => AuthChangeEvent.userUpdated, + _ => AuthChangeEvent.values + .firstWhereOrNull((event) => event.name == rawEvent), + }; + + if (event != null) { + Session? session; + try { + session = Session.fromJson(parsedMessageEvent['session']); + } catch (e) { + // ignore + } + notifyAllSubscribers(event, session: session, broadcast: false); + } + }); + } + } + /// Generates a new JWT. /// /// To prevent multiple simultaneous requests it catches an already ongoing request by using the global [_refreshTokenCompleter]. @@ -1182,8 +1228,18 @@ class GoTrueClient { /// For internal use only. @internal - void notifyAllSubscribers(AuthChangeEvent event) { - final state = AuthState(event, currentSession); + void notifyAllSubscribers( + AuthChangeEvent event, { + Session? session, + bool broadcast = true, + }) { + if (broadcast) { + _broadcastChannel?.postMessage(json.encode({ + 'event': event.name, + 'session': session?.toJson(), + })); + } + final state = AuthState(event, session ?? currentSession); _onAuthStateChangeController.add(state); _onAuthStateChangeControllerSync.add(state); } diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart new file mode 100644 index 00000000..ff959705 --- /dev/null +++ b/packages/gotrue/lib/src/types/types.dart @@ -0,0 +1,4 @@ +typedef BroadcastChannel = ({ + Stream onMessage, + void Function(String) postMessage, +}); diff --git a/packages/supabase_flutter/lib/src/local_storage.dart b/packages/supabase_flutter/lib/src/local_storage.dart index 607d4a15..556d15c4 100644 --- a/packages/supabase_flutter/lib/src/local_storage.dart +++ b/packages/supabase_flutter/lib/src/local_storage.dart @@ -8,6 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import './local_storage_stub.dart' if (dart.library.html) './local_storage_web.dart' as web; +@Deprecated("No longer in use") const supabasePersistSessionKey = 'SUPABASE_PERSIST_SESSION_KEY'; /// LocalStorage is used to persist the user session in the device. From e372c46e7d71ed0eeafd00c6dcc67f9fd660e687 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 15 Aug 2024 17:18:01 +0200 Subject: [PATCH 02/11] refactor: consolidate types in one file --- packages/gotrue/lib/gotrue.dart | 3 +- packages/gotrue/lib/src/gotrue_client.dart | 1 - .../gotrue/lib/src/types/o_auth_provider.dart | 22 --------------- .../gotrue/lib/src/types/oauth_flow_type.dart | 4 --- packages/gotrue/lib/src/types/types.dart | 28 +++++++++++++++++++ 5 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 packages/gotrue/lib/src/types/o_auth_provider.dart delete mode 100644 packages/gotrue/lib/src/types/oauth_flow_type.dart diff --git a/packages/gotrue/lib/gotrue.dart b/packages/gotrue/lib/gotrue.dart index 7e27c223..0799ee52 100644 --- a/packages/gotrue/lib/gotrue.dart +++ b/packages/gotrue/lib/gotrue.dart @@ -9,8 +9,7 @@ export 'src/types/auth_response.dart' hide ToSnakeCase; export 'src/types/auth_state.dart'; export 'src/types/gotrue_async_storage.dart'; export 'src/types/mfa.dart'; -export 'src/types/o_auth_provider.dart'; -export 'src/types/oauth_flow_type.dart'; +export 'src/types/types.dart'; export 'src/types/session.dart'; export 'src/types/user.dart'; export 'src/types/user_attributes.dart'; diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 230b22b2..c39b3f01 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -9,7 +9,6 @@ import 'package:gotrue/src/fetch.dart'; import 'package:gotrue/src/helper.dart'; import 'package:gotrue/src/types/auth_response.dart'; import 'package:gotrue/src/types/fetch_options.dart'; -import 'package:gotrue/src/types/types.dart'; import 'package:http/http.dart'; import 'package:jwt_decode/jwt_decode.dart'; import 'package:meta/meta.dart'; diff --git a/packages/gotrue/lib/src/types/o_auth_provider.dart b/packages/gotrue/lib/src/types/o_auth_provider.dart deleted file mode 100644 index 1851bc7a..00000000 --- a/packages/gotrue/lib/src/types/o_auth_provider.dart +++ /dev/null @@ -1,22 +0,0 @@ -enum OAuthProvider { - apple, - azure, - bitbucket, - discord, - facebook, - figma, - github, - gitlab, - google, - kakao, - keycloak, - linkedin, - linkedinOidc, - notion, - slack, - spotify, - twitch, - twitter, - workos, - zoom, -} diff --git a/packages/gotrue/lib/src/types/oauth_flow_type.dart b/packages/gotrue/lib/src/types/oauth_flow_type.dart deleted file mode 100644 index 51803805..00000000 --- a/packages/gotrue/lib/src/types/oauth_flow_type.dart +++ /dev/null @@ -1,4 +0,0 @@ -enum AuthFlowType { - implicit, - pkce, -} diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart index ff959705..8a5e9f5c 100644 --- a/packages/gotrue/lib/src/types/types.dart +++ b/packages/gotrue/lib/src/types/types.dart @@ -2,3 +2,31 @@ typedef BroadcastChannel = ({ Stream onMessage, void Function(String) postMessage, }); + +enum AuthFlowType { + implicit, + pkce, +} + +enum OAuthProvider { + apple, + azure, + bitbucket, + discord, + facebook, + figma, + github, + gitlab, + google, + kakao, + keycloak, + linkedin, + linkedinOidc, + notion, + slack, + spotify, + twitch, + twitter, + workos, + zoom, +} From 4b400f0686297ea2656dec41f1b8d2174af3c042 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Thu, 15 Aug 2024 17:36:10 +0200 Subject: [PATCH 03/11] fix: add dispose to auth client --- packages/gotrue/lib/src/broadcast_web.dart | 1 + packages/gotrue/lib/src/gotrue_client.dart | 8 ++++++++ packages/gotrue/lib/src/types/types.dart | 1 + packages/supabase/lib/src/supabase_client.dart | 1 + 4 files changed, 11 insertions(+) diff --git a/packages/gotrue/lib/src/broadcast_web.dart b/packages/gotrue/lib/src/broadcast_web.dart index 9c96c4aa..8d3dae4f 100644 --- a/packages/gotrue/lib/src/broadcast_web.dart +++ b/packages/gotrue/lib/src/broadcast_web.dart @@ -7,5 +7,6 @@ BroadcastChannel getBroadcastChannel(String broadcastKey) { return ( onMessage: broadcast.onMessage.map((event) => event.data.toString()), postMessage: broadcast.postMessage, + close: broadcast.close, ); } diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index c39b3f01..6fc3efa7 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1173,6 +1173,14 @@ class GoTrueClient { } } + void dispose() { + _onAuthStateChangeController.close(); + _onAuthStateChangeControllerSync.close(); + _broadcastChannel?.close(); + _refreshTokenCompleter?.completeError(AuthException('Disposed')); + _autoRefreshTicker?.cancel(); + } + /// Generates a new JWT. /// /// To prevent multiple simultaneous requests it catches an already ongoing request by using the global [_refreshTokenCompleter]. diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart index 8a5e9f5c..cf142559 100644 --- a/packages/gotrue/lib/src/types/types.dart +++ b/packages/gotrue/lib/src/types/types.dart @@ -1,6 +1,7 @@ typedef BroadcastChannel = ({ Stream onMessage, void Function(String) postMessage, + void Function() close, }); enum AuthFlowType { diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 3c5a9c63..4323a7ea 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -250,6 +250,7 @@ class SupabaseClient { Future dispose() async { await _authStateSubscription?.cancel(); await _isolate.dispose(); + auth.dispose(); } GoTrueClient _initSupabaseAuthClient({ From 1bc955774615e674b31b56085a62ff44b2a59b4d Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 16 Aug 2024 22:18:09 +0200 Subject: [PATCH 04/11] refactor: use js object for messaging --- packages/gotrue/lib/src/broadcast_web.dart | 15 ++++++++++++-- packages/gotrue/lib/src/constants.dart | 21 ++++++++++++-------- packages/gotrue/lib/src/gotrue_client.dart | 23 ++++++++++++++-------- packages/gotrue/lib/src/types/types.dart | 4 ++-- 4 files changed, 43 insertions(+), 20 deletions(-) diff --git a/packages/gotrue/lib/src/broadcast_web.dart b/packages/gotrue/lib/src/broadcast_web.dart index 8d3dae4f..b754666a 100644 --- a/packages/gotrue/lib/src/broadcast_web.dart +++ b/packages/gotrue/lib/src/broadcast_web.dart @@ -1,12 +1,23 @@ +import 'dart:convert'; import 'dart:html' as html; +import 'dart:js_util' as js_util; import 'package:gotrue/src/types/types.dart'; BroadcastChannel getBroadcastChannel(String broadcastKey) { final broadcast = html.BroadcastChannel(broadcastKey); return ( - onMessage: broadcast.onMessage.map((event) => event.data.toString()), - postMessage: broadcast.postMessage, + onMessage: broadcast.onMessage.map((event) { + final dataMap = js_util.dartify(event.data); + + // some parts have the wrong map type. This is an easy workaround and + // should be efficient enough for the small session and user data + return json.decode(json.encode(dataMap)); + }), + postMessage: (message) { + final jsMessage = js_util.jsify(message); + broadcast.postMessage(jsMessage); + }, close: broadcast.close, ); } diff --git a/packages/gotrue/lib/src/constants.dart b/packages/gotrue/lib/src/constants.dart index e1bdc87d..c44d5ff4 100644 --- a/packages/gotrue/lib/src/constants.dart +++ b/packages/gotrue/lib/src/constants.dart @@ -34,14 +34,19 @@ class ApiVersions { } enum AuthChangeEvent { - initialSession, - passwordRecovery, - signedIn, - signedOut, - tokenRefreshed, - userUpdated, - userDeleted, - mfaChallengeVerified, + initialSession('INITIAL_SESSION'), + passwordRecovery('PASSWORD_RECOVERY'), + signedIn('SIGNED_IN'), + signedOut('SIGNED_OUT'), + tokenRefreshed('TOKEN_REFRESHED'), + userUpdated('USER_UPDATED'), + + @Deprecated('Was never in use and might be removed in the future.') + userDeleted(''), + mfaChallengeVerified('MFA_CHALLENGE_VERIFIED'); + + final String jsName; + const AuthChangeEvent(this.jsName); } extension AuthChangeEventExtended on AuthChangeEvent { diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 6fc3efa7..2aa4100a 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -31,6 +31,8 @@ part 'gotrue_mfa_api.dart'; /// /// [asyncStorage] local storage to store pkce code verifiers. Required when using the pkce flow. /// +/// [broadcastSession] whether to broadcast session changes to other tabs on web. Defaults to true. +/// /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow. /// {@endtemplate} class GoTrueClient { @@ -86,8 +88,11 @@ class GoTrueClient { _onAuthStateChangeControllerSync.stream; final AuthFlowType _flowType; + + /// Whether to broadcast session changes to other tabs on web. final bool _broadcastSession; + /// Proxy to the web BroadcastChannel API. Should be null on non-web platforms. BroadcastChannel? _broadcastChannel; /// {@macro gotrue_client} @@ -1146,16 +1151,18 @@ class GoTrueClient { _broadcastChannel = web.getBroadcastChannel(broadcastKey); _broadcastChannel?.onMessage.listen((messageEvent) { - final Map parsedMessageEvent = json.decode(messageEvent); - final rawEvent = parsedMessageEvent['event']; + final rawEvent = messageEvent['event']; final event = switch (rawEvent) { - // Handle events from js library as well + // This library sends the js name of the event to be comptabile with + // the js library, so we need to convert it back to the dart name 'INITIAL_SESSION' => AuthChangeEvent.initialSession, 'PASSWORD_RECOVERY' => AuthChangeEvent.passwordRecovery, 'SIGNED_IN' => AuthChangeEvent.signedIn, 'SIGNED_OUT' => AuthChangeEvent.signedOut, 'TOKEN_REFRESHED' => AuthChangeEvent.tokenRefreshed, 'USER_UPDATED' => AuthChangeEvent.userUpdated, + 'MFA_CHALLENGE_VERIFIED' => AuthChangeEvent.mfaChallengeVerified, + // This case should never happen though _ => AuthChangeEvent.values .firstWhereOrNull((event) => event.name == rawEvent), }; @@ -1163,7 +1170,7 @@ class GoTrueClient { if (event != null) { Session? session; try { - session = Session.fromJson(parsedMessageEvent['session']); + session = Session.fromJson(messageEvent['session']); } catch (e) { // ignore } @@ -1240,11 +1247,11 @@ class GoTrueClient { Session? session, bool broadcast = true, }) { - if (broadcast) { - _broadcastChannel?.postMessage(json.encode({ - 'event': event.name, + if (broadcast && event != AuthChangeEvent.initialSession) { + _broadcastChannel?.postMessage({ + 'event': event.jsName, 'session': session?.toJson(), - })); + }); } final state = AuthState(event, session ?? currentSession); _onAuthStateChangeController.add(state); diff --git a/packages/gotrue/lib/src/types/types.dart b/packages/gotrue/lib/src/types/types.dart index cf142559..a2d11a69 100644 --- a/packages/gotrue/lib/src/types/types.dart +++ b/packages/gotrue/lib/src/types/types.dart @@ -1,6 +1,6 @@ typedef BroadcastChannel = ({ - Stream onMessage, - void Function(String) postMessage, + Stream> onMessage, + void Function(Map) postMessage, void Function() close, }); From 16449e661d2ce2aa8f8fdfb0b7b2c37478692982 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 16 Aug 2024 22:21:52 +0200 Subject: [PATCH 05/11] style: remove unused userDeleted event --- packages/supabase/lib/src/supabase_client.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index 4323a7ea..b51b4ca8 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -340,8 +340,7 @@ class SupabaseClient { event == AuthChangeEvent.tokenRefreshed || event == AuthChangeEvent.signedIn) { realtime.setAuth(token); - } else if (event == AuthChangeEvent.signedOut || - event == AuthChangeEvent.userDeleted) { + } else if (event == AuthChangeEvent.signedOut) { // Token is removed realtime.setAuth(_supabaseKey); From 99633968b8ef29fa7957192ed4f721a1751c2f12 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Fri, 16 Aug 2024 22:31:59 +0200 Subject: [PATCH 06/11] refactor: remove deprecation of supabasePersistSessionKey --- packages/supabase_flutter/lib/src/local_storage.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/supabase_flutter/lib/src/local_storage.dart b/packages/supabase_flutter/lib/src/local_storage.dart index 556d15c4..7b201584 100644 --- a/packages/supabase_flutter/lib/src/local_storage.dart +++ b/packages/supabase_flutter/lib/src/local_storage.dart @@ -8,7 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import './local_storage_stub.dart' if (dart.library.html) './local_storage_web.dart' as web; -@Deprecated("No longer in use") +/// Only used for migration from Hive to SharedPreferences. Not actually in use. const supabasePersistSessionKey = 'SUPABASE_PERSIST_SESSION_KEY'; /// LocalStorage is used to persist the user session in the device. From 40c29e19943aa717286c1b6ebc3cabde0b3fb4b2 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Sat, 17 Aug 2024 00:48:38 +0200 Subject: [PATCH 07/11] fix: store session in client --- packages/gotrue/lib/src/gotrue_client.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 2aa4100a..224acb11 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1174,6 +1174,9 @@ class GoTrueClient { } catch (e) { // ignore } + if (session != null) { + _saveSession(session); + } notifyAllSubscribers(event, session: session, broadcast: false); } }); From 8cb8735d77da67809dd96b3620752027003243a9 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Tue, 10 Sep 2024 23:28:45 +0200 Subject: [PATCH 08/11] fix: allow removing session on broadcast and add bool to AuthState --- packages/gotrue/lib/src/gotrue_client.dart | 9 ++++++++- packages/gotrue/lib/src/types/auth_state.dart | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 224acb11..a729af31 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1173,9 +1173,12 @@ class GoTrueClient { session = Session.fromJson(messageEvent['session']); } catch (e) { // ignore + return; } if (session != null) { _saveSession(session); + } else { + _removeSession(); } notifyAllSubscribers(event, session: session, broadcast: false); } @@ -1244,19 +1247,23 @@ class GoTrueClient { } /// For internal use only. + /// + /// [broadcast] is used to determine if the event should be broadcasted to + /// other tabs. @internal void notifyAllSubscribers( AuthChangeEvent event, { Session? session, bool broadcast = true, }) { + session ??= currentSession; if (broadcast && event != AuthChangeEvent.initialSession) { _broadcastChannel?.postMessage({ 'event': event.jsName, 'session': session?.toJson(), }); } - final state = AuthState(event, session ?? currentSession); + final state = AuthState(event, session, fromBroadcast: !broadcast); _onAuthStateChangeController.add(state); _onAuthStateChangeControllerSync.add(state); } diff --git a/packages/gotrue/lib/src/types/auth_state.dart b/packages/gotrue/lib/src/types/auth_state.dart index b23b612a..3a5ca815 100644 --- a/packages/gotrue/lib/src/types/auth_state.dart +++ b/packages/gotrue/lib/src/types/auth_state.dart @@ -4,6 +4,12 @@ import 'package:gotrue/src/types/session.dart'; class AuthState { final AuthChangeEvent event; final Session? session; + final bool fromBroadcast; - AuthState(this.event, this.session); + AuthState(this.event, this.session, {this.fromBroadcast = false}); + + @override + String toString() { + return 'AuthState{event: $event, session: $session, fromBroadcast: $fromBroadcast}'; + } } From cb4050f1908b45d45fdb46a9e39052f2e8cb6fda Mon Sep 17 00:00:00 2001 From: Vinzent Date: Tue, 10 Sep 2024 23:33:41 +0200 Subject: [PATCH 09/11] fix: catch session being null --- packages/gotrue/lib/src/gotrue_client.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index a729af31..8c5fb9dc 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -1169,11 +1169,13 @@ class GoTrueClient { if (event != null) { Session? session; - try { - session = Session.fromJson(messageEvent['session']); - } catch (e) { - // ignore - return; + if (messageEvent['session'] != null) { + try { + session = Session.fromJson(messageEvent['session']); + } catch (e) { + // ignore + return; + } } if (session != null) { _saveSession(session); From c6185269f8e00cb51e2f2908ae4d357379c81df8 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Sun, 15 Sep 2024 21:08:49 +0200 Subject: [PATCH 10/11] fix: improvements from code review --- packages/gotrue/lib/src/gotrue_client.dart | 84 ++++++++++--------- packages/gotrue/lib/src/types/auth_state.dart | 2 +- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index 8c5fb9dc..c4e9a30d 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -89,12 +89,11 @@ class GoTrueClient { final AuthFlowType _flowType; - /// Whether to broadcast session changes to other tabs on web. - final bool _broadcastSession; - /// Proxy to the web BroadcastChannel API. Should be null on non-web platforms. BroadcastChannel? _broadcastChannel; + StreamSubscription? _broadcastChannelSubscription; + /// {@macro gotrue_client} GoTrueClient({ String? url, @@ -108,8 +107,7 @@ class GoTrueClient { _headers = headers ?? {}, _httpClient = httpClient, _asyncStorage = asyncStorage, - _flowType = flowType, - _broadcastSession = broadcastSession { + _flowType = flowType { _autoRefreshToken = autoRefreshToken ?? true; final gotrueUrl = url ?? Constants.defaultGotrueUrl; @@ -130,7 +128,7 @@ class GoTrueClient { startAutoRefresh(); } - mayStartBroadcastChannel(); + _mayStartBroadcastChannel(); } /// Getter for the headers @@ -1143,55 +1141,59 @@ class GoTrueClient { _currentUser = null; } - void mayStartBroadcastChannel() { - if (const bool.fromEnvironment('dart.library.html') && _broadcastSession) { + void _mayStartBroadcastChannel() { + if (const bool.fromEnvironment('dart.library.html')) { // Used by the js library as well final broadcastKey = "sb-${Uri.parse(_url).host.split(".").first}-auth-token"; - _broadcastChannel = web.getBroadcastChannel(broadcastKey); - _broadcastChannel?.onMessage.listen((messageEvent) { - final rawEvent = messageEvent['event']; - final event = switch (rawEvent) { - // This library sends the js name of the event to be comptabile with - // the js library, so we need to convert it back to the dart name - 'INITIAL_SESSION' => AuthChangeEvent.initialSession, - 'PASSWORD_RECOVERY' => AuthChangeEvent.passwordRecovery, - 'SIGNED_IN' => AuthChangeEvent.signedIn, - 'SIGNED_OUT' => AuthChangeEvent.signedOut, - 'TOKEN_REFRESHED' => AuthChangeEvent.tokenRefreshed, - 'USER_UPDATED' => AuthChangeEvent.userUpdated, - 'MFA_CHALLENGE_VERIFIED' => AuthChangeEvent.mfaChallengeVerified, - // This case should never happen though - _ => AuthChangeEvent.values - .firstWhereOrNull((event) => event.name == rawEvent), - }; - - if (event != null) { - Session? session; - if (messageEvent['session'] != null) { - try { + assert(_broadcastChannel == null, + 'Broadcast channel should not be started more than once.'); + try { + _broadcastChannel = web.getBroadcastChannel(broadcastKey); + _broadcastChannelSubscription = + _broadcastChannel?.onMessage.listen((messageEvent) { + final rawEvent = messageEvent['event']; + final event = switch (rawEvent) { + // This library sends the js name of the event to be comptabile with + // the js library, so we need to convert it back to the dart name + 'INITIAL_SESSION' => AuthChangeEvent.initialSession, + 'PASSWORD_RECOVERY' => AuthChangeEvent.passwordRecovery, + 'SIGNED_IN' => AuthChangeEvent.signedIn, + 'SIGNED_OUT' => AuthChangeEvent.signedOut, + 'TOKEN_REFRESHED' => AuthChangeEvent.tokenRefreshed, + 'USER_UPDATED' => AuthChangeEvent.userUpdated, + 'MFA_CHALLENGE_VERIFIED' => AuthChangeEvent.mfaChallengeVerified, + // This case should never happen though + _ => AuthChangeEvent.values + .firstWhereOrNull((event) => event.name == rawEvent), + }; + + if (event != null) { + Session? session; + if (messageEvent['session'] != null) { session = Session.fromJson(messageEvent['session']); - } catch (e) { - // ignore - return; } + if (session != null) { + _saveSession(session); + } else { + _removeSession(); + } + notifyAllSubscribers(event, session: session, broadcast: false); } - if (session != null) { - _saveSession(session); - } else { - _removeSession(); - } - notifyAllSubscribers(event, session: session, broadcast: false); - } - }); + }); + } catch (e) { + // Ignoring + } } } + @mustCallSuper void dispose() { _onAuthStateChangeController.close(); _onAuthStateChangeControllerSync.close(); _broadcastChannel?.close(); + _broadcastChannelSubscription?.cancel(); _refreshTokenCompleter?.completeError(AuthException('Disposed')); _autoRefreshTicker?.cancel(); } diff --git a/packages/gotrue/lib/src/types/auth_state.dart b/packages/gotrue/lib/src/types/auth_state.dart index 3a5ca815..93369b69 100644 --- a/packages/gotrue/lib/src/types/auth_state.dart +++ b/packages/gotrue/lib/src/types/auth_state.dart @@ -6,7 +6,7 @@ class AuthState { final Session? session; final bool fromBroadcast; - AuthState(this.event, this.session, {this.fromBroadcast = false}); + const AuthState(this.event, this.session, {this.fromBroadcast = false}); @override String toString() { From 5b84d46f9b1a7f69cdda4ca3fbb7a5159bc411f6 Mon Sep 17 00:00:00 2001 From: Vinzent Date: Mon, 16 Sep 2024 14:34:54 +0200 Subject: [PATCH 11/11] fix: remove broadcastSession from constructor --- packages/gotrue/lib/src/gotrue_client.dart | 3 --- packages/gotrue/lib/src/types/auth_state.dart | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gotrue/lib/src/gotrue_client.dart b/packages/gotrue/lib/src/gotrue_client.dart index c4e9a30d..8a2c0e2b 100644 --- a/packages/gotrue/lib/src/gotrue_client.dart +++ b/packages/gotrue/lib/src/gotrue_client.dart @@ -31,8 +31,6 @@ part 'gotrue_mfa_api.dart'; /// /// [asyncStorage] local storage to store pkce code verifiers. Required when using the pkce flow. /// -/// [broadcastSession] whether to broadcast session changes to other tabs on web. Defaults to true. -/// /// Set [flowType] to [AuthFlowType.implicit] to perform old implicit auth flow. /// {@endtemplate} class GoTrueClient { @@ -102,7 +100,6 @@ class GoTrueClient { Client? httpClient, GotrueAsyncStorage? asyncStorage, AuthFlowType flowType = AuthFlowType.pkce, - bool broadcastSession = true, }) : _url = url ?? Constants.defaultGotrueUrl, _headers = headers ?? {}, _httpClient = httpClient, diff --git a/packages/gotrue/lib/src/types/auth_state.dart b/packages/gotrue/lib/src/types/auth_state.dart index 93369b69..c610790a 100644 --- a/packages/gotrue/lib/src/types/auth_state.dart +++ b/packages/gotrue/lib/src/types/auth_state.dart @@ -4,6 +4,9 @@ import 'package:gotrue/src/types/session.dart'; class AuthState { final AuthChangeEvent event; final Session? session; + + /// Whether this state was broadcasted via `html.ChannelBroadcast` on web from + /// another tab or window. final bool fromBroadcast; const AuthState(this.event, this.session, {this.fromBroadcast = false});