diff --git a/README.md b/README.md index fd50c109..480f0881 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); - Supabase( + Supabase.initialize( url: SUPABASE_URL, anonKey: SUPABASE_ANNON_KEY, authCallbackUrlHostname: 'login-callback', // optional @@ -45,7 +45,7 @@ Now you can access Supabase client anywhere in your app. ```dart import 'package:supabase_flutter/supabase_flutter.dart'; -final response = await Supabase().client.auth.signIn(email: _email, password: _password); +final response = await Supabase.instance.client.auth.signIn(email: _email, password: _password); ``` #### SupabaseAuthState @@ -67,8 +67,8 @@ For more details, take a look at the example [here](https://github.com/phamhieu/ This method will automatically launch the auth url and open a browser for user to sign in with 3rd party login. ```dart -Supabase().client.auth.signInWithProvider( - supabase.Provider.github, +Supabase.instance.client.auth.signInWithProvider( + Provider.github, options: supabase.AuthOptions(redirectTo: ''), ); ``` @@ -93,7 +93,8 @@ final localStorage = LocalStorage( const storage = FlutterSecureStorage(); return storage.write(key: supabasePersistSessionKey, value: value); }); -Supabase( + +Supabase.initialize( ... localStorage: localStorage, ); diff --git a/lib/src/supabase.dart b/lib/src/supabase.dart index d2d1ff16..580b8234 100644 --- a/lib/src/supabase.dart +++ b/lib/src/supabase.dart @@ -4,60 +4,75 @@ import 'package:url_launcher/url_launcher.dart'; const supabasePersistSessionKey = 'SUPABASE_PERSIST_SESSION_KEY'; -Future defHasAccessToken() async { +Future _defHasAccessToken() async { final prefs = await SharedPreferences.getInstance(); final exist = prefs.containsKey(supabasePersistSessionKey); return exist; } -Future defAccessToken() async { +Future _defAccessToken() async { final prefs = await SharedPreferences.getInstance(); final jsonStr = prefs.getString(supabasePersistSessionKey); return jsonStr; } -Future defRemovePersistedSession() async { +Future _defRemovePersistedSession() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.remove(supabasePersistSessionKey); } -Future defPersistSession(String persistSessionString) async { +Future _defPersistSession(String persistSessionString) async { final prefs = await SharedPreferences.getInstance(); return prefs.setString(supabasePersistSessionKey, persistSessionString); } class LocalStorage { - LocalStorage( - {Future Function()? hasAccessToken, - Future Function()? accessToken, - Future Function()? removePersistedSession, - Future Function(String)? persistSession}) { - this.hasAccessToken = hasAccessToken ?? defHasAccessToken; - this.accessToken = accessToken ?? defAccessToken; - this.removePersistedSession = - removePersistedSession ?? defRemovePersistedSession; - this.persistSession = persistSession ?? defPersistSession; - } - - Future Function() hasAccessToken = defHasAccessToken; - Future Function() accessToken = defAccessToken; - Future Function() removePersistedSession = defRemovePersistedSession; - Future Function(String) persistSession = defPersistSession; + /// Creates a `LocalStorage` instance + const LocalStorage({ + this.hasAccessToken = _defHasAccessToken, + this.accessToken = _defAccessToken, + this.removePersistedSession = _defRemovePersistedSession, + this.persistSession = _defPersistSession, + }); + + final Future Function() hasAccessToken; + final Future Function() accessToken; + final Future Function() removePersistedSession; + final Future Function(String) persistSession; } class Supabase { - factory Supabase({ + /// Gets the current supabase instance. + /// + /// An error is thrown if supabase isn't initialized yet + static Supabase get instance { + assert( + _instance._initialized, + 'You must initialize the supabase instance before calling Supabase.instance', + ); + return _instance; + } + + /// Initialize the current supabase instance + /// + /// This must be called only once. If called more than once, an + /// [AssertionError] is thrown + factory Supabase.initialize({ String? url, String? anonKey, String? authCallbackUrlHostname, bool? debug, LocalStorage? localStorage, }) { + assert( + !_instance._initialized, + 'This instance is already initialized', + ); if (url != null && anonKey != null) { _instance._init(url, anonKey); _instance._authCallbackUrlHostname = authCallbackUrlHostname; _instance._debugEnable = debug ?? false; - _instance._localStorage = localStorage ?? LocalStorage(); + _instance._localStorage = localStorage ?? const LocalStorage(); _instance.log('***** Supabase init completed $_instance'); } @@ -67,48 +82,37 @@ class Supabase { Supabase._privateConstructor(); static final Supabase _instance = Supabase._privateConstructor(); - SupabaseClient? _client; + bool _initialized = false; + + /// The supabase client for this instance + late final SupabaseClient client; GotrueSubscription? _initialClientSubscription; bool _initialDeeplinkIsHandled = false; bool _debugEnable = false; String? _authCallbackUrlHostname; - LocalStorage _localStorage = LocalStorage(); + LocalStorage _localStorage = const LocalStorage(); + /// Dispose the instance void dispose() { if (_initialClientSubscription != null) { _initialClientSubscription!.data!.unsubscribe(); } + _initialized = false; } void _init(String supabaseUrl, String supabaseAnonKey) { - if (_client != null) { - throw 'Supabase client is initialized more than once $_client'; - } - - _client = SupabaseClient(supabaseUrl, supabaseAnonKey); + client = SupabaseClient(supabaseUrl, supabaseAnonKey); _initialClientSubscription = - _client!.auth.onAuthStateChange(_onAuthStateChange); - } - - SupabaseClient get client { - if (_client == null) { - throw 'Supabase client is not initialized'; - } - return _client!; + client.auth.onAuthStateChange(_onAuthStateChange); + _initialized = true; } - LocalStorage get localStorage { - return _localStorage; - } + LocalStorage get localStorage => _localStorage; - Future get hasAccessToken async { - return _localStorage.hasAccessToken(); - } + Future get hasAccessToken => _localStorage.hasAccessToken(); - Future get accessToken async { - return _localStorage.accessToken(); - } + Future get accessToken => _localStorage.accessToken(); void _onAuthStateChange(AuthChangeEvent event, Session? session) { log('**** onAuthStateChange: $event'); @@ -172,7 +176,8 @@ class Supabase { } extension GoTrueClientSignInProvider on GoTrueClient { - Future signInWithProvider(Provider? provider, + /// Signs the user in using a thrid parties providers. + Future signInWithProvider(Provider provider, {AuthOptions? options}) async { final res = await signIn( provider: provider, diff --git a/lib/src/supabase_auth_required_state.dart b/lib/src/supabase_auth_required_state.dart index a8d20eff..7f9930e0 100644 --- a/lib/src/supabase_auth_required_state.dart +++ b/lib/src/supabase_auth_required_state.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:supabase/supabase.dart'; import 'package:supabase_flutter/src/supabase.dart'; import 'package:supabase_flutter/src/supabase_state.dart'; @@ -10,22 +10,22 @@ abstract class SupabaseAuthRequiredState void initState() { super.initState(); - if (Supabase().client.auth.currentSession == null) { + if (Supabase.instance.client.auth.currentSession == null) { _recoverSupabaseSession(); } else { - onAuthenticated(Supabase().client.auth.currentSession!); + onAuthenticated(Supabase.instance.client.auth.currentSession!); } } @override void startAuthObserver() { - Supabase().log('***** SupabaseAuthRequiredState startAuthObserver'); + Supabase.instance.log('***** SupabaseAuthRequiredState startAuthObserver'); WidgetsBinding.instance?.addObserver(this); } @override void stopAuthObserver() { - Supabase().log('***** SupabaseAuthRequiredState stopAuthObserver'); + Supabase.instance.log('***** SupabaseAuthRequiredState stopAuthObserver'); WidgetsBinding.instance?.removeObserver(this); } @@ -45,26 +45,27 @@ abstract class SupabaseAuthRequiredState } Future onResumed() async { - Supabase().log('***** SupabaseAuthRequiredState onResumed'); + Supabase.instance.log('***** SupabaseAuthRequiredState onResumed'); return _recoverSupabaseSession(); } Future _recoverSupabaseSession() async { - final bool exist = await Supabase().localStorage.hasAccessToken(); + final bool exist = await Supabase.instance.localStorage.hasAccessToken(); if (!exist) { onUnauthenticated(); return false; } - final String? jsonStr = await Supabase().localStorage.accessToken(); + final String? jsonStr = await Supabase.instance.localStorage.accessToken(); if (jsonStr == null) { onUnauthenticated(); return false; } - final response = await Supabase().client.auth.recoverSession(jsonStr); + final response = + await Supabase.instance.client.auth.recoverSession(jsonStr); if (response.error != null) { - Supabase().localStorage.removePersistedSession(); + Supabase.instance.localStorage.removePersistedSession(); onUnauthenticated(); return false; } else { diff --git a/lib/src/supabase_auth_state.dart b/lib/src/supabase_auth_state.dart index 9871ff27..dd0ec3aa 100644 --- a/lib/src/supabase_auth_state.dart +++ b/lib/src/supabase_auth_state.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:supabase/supabase.dart'; import 'package:supabase_flutter/src/supabase.dart'; import 'package:supabase_flutter/src/supabase_state.dart'; @@ -10,21 +10,21 @@ abstract class SupabaseAuthState extends SupabaseState with SupabaseDeepLinkingMixin { @override void startAuthObserver() { - Supabase().log('***** SupabaseAuthState startAuthObserver'); + Supabase.instance.log('***** SupabaseAuthState startAuthObserver'); startDeeplinkObserver(); } @override void stopAuthObserver() { - Supabase().log('***** SupabaseAuthState stopAuthObserver'); + Supabase.instance.log('***** SupabaseAuthState stopAuthObserver'); stopDeeplinkObserver(); } @override Future handleDeeplink(Uri uri) async { - if (!Supabase().isAuthCallbackDeeplink(uri)) return false; + if (!Supabase.instance.isAuthCallbackDeeplink(uri)) return false; - Supabase().log('***** SupabaseAuthState handleDeeplink $uri'); + Supabase.instance.log('***** SupabaseAuthState handleDeeplink $uri'); // notify auth deeplink received onReceivedAuthDeeplink(uri); @@ -34,15 +34,15 @@ abstract class SupabaseAuthState @override void onErrorReceivingDeeplink(String message) { - Supabase().log('onErrorReceivingDeppLink message: $message'); + Supabase.instance.log('onErrorReceivingDeppLink message: $message'); } Future recoverSessionFromUrl(Uri uri) async { - final uriParameters = Supabase().parseUriParameters(uri); + final uriParameters = Supabase.instance.parseUriParameters(uri); final type = uriParameters['type'] ?? ''; // recover session from deeplink - final response = await Supabase().client.auth.getSessionFromUrl(uri); + final response = await Supabase.instance.client.auth.getSessionFromUrl(uri); if (response.error != null) { onErrorAuthenticating(response.error!.message); } else { @@ -58,21 +58,22 @@ abstract class SupabaseAuthState /// Recover/refresh session if it's available /// e.g. called on a Splash screen when app starts. Future recoverSupabaseSession() async { - final bool exist = await Supabase().localStorage.hasAccessToken(); + final bool exist = await Supabase.instance.localStorage.hasAccessToken(); if (!exist) { onUnauthenticated(); return false; } - final String? jsonStr = await Supabase().localStorage.accessToken(); + final String? jsonStr = await Supabase.instance.localStorage.accessToken(); if (jsonStr == null) { onUnauthenticated(); return false; } - final response = await Supabase().client.auth.recoverSession(jsonStr); + final response = + await Supabase.instance.client.auth.recoverSession(jsonStr); if (response.error != null) { - Supabase().localStorage.removePersistedSession(); + Supabase.instance.localStorage.removePersistedSession(); onUnauthenticated(); return false; } else { @@ -83,7 +84,7 @@ abstract class SupabaseAuthState /// Callback when deeplink received and is processing. Optional void onReceivedAuthDeeplink(Uri uri) { - Supabase().log('onReceivedAuthDeeplink uri: $uri'); + Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); } /// Callback when user is unauthenticated diff --git a/lib/src/supabase_deep_linking_mixin.dart b/lib/src/supabase_deep_linking_mixin.dart index d6b6df13..dbd41876 100644 --- a/lib/src/supabase_deep_linking_mixin.dart +++ b/lib/src/supabase_deep_linking_mixin.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter/services.dart'; import 'package:supabase_flutter/src/supabase.dart'; import 'package:uni_links/uni_links.dart'; @@ -10,13 +10,13 @@ mixin SupabaseDeepLinkingMixin on State { StreamSubscription? _sub; void startDeeplinkObserver() { - Supabase().log('***** SupabaseDeepLinkingMixin startAuthObserver'); + Supabase.instance.log('***** SupabaseDeepLinkingMixin startAuthObserver'); _handleIncomingLinks(); _handleInitialUri(); } void stopDeeplinkObserver() { - Supabase().log('***** SupabaseDeepLinkingMixin stopAuthObserver'); + Supabase.instance.log('***** SupabaseDeepLinkingMixin stopAuthObserver'); if (_sub != null) _sub?.cancel(); } @@ -45,7 +45,7 @@ mixin SupabaseDeepLinkingMixin on State { /// /// We handle all exceptions, since it is called from initState. Future _handleInitialUri() async { - if (!Supabase().shouldHandleInitialDeeplink()) return; + if (!Supabase.instance.shouldHandleInitialDeeplink()) return; try { final uri = await getInitialUri(); diff --git a/lib/src/supabase_state.dart b/lib/src/supabase_state.dart index 2436a97b..55af5544 100644 --- a/lib/src/supabase_state.dart +++ b/lib/src/supabase_state.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; /// Interface for screen that requires an authenticated user abstract class SupabaseState extends State { diff --git a/test/supabase_flutter_test.dart b/test/supabase_flutter_test.dart index 687494bf..2bc197db 100644 --- a/test/supabase_flutter_test.dart +++ b/test/supabase_flutter_test.dart @@ -8,18 +8,18 @@ void main() { setUpAll(() { // initial Supabase singleton - Supabase(url: supabaseUrl, anonKey: supabaseKey); + Supabase.initialize(url: supabaseUrl, anonKey: supabaseKey); }); test('can access Supabase singleton', () async { - final client = Supabase().client; + final client = Supabase.instance.client; expect(client, isNotNull); }); test('can parse deeplink', () async { final uri = Uri.parse( "io.supabase.flutterdemo://login-callback#access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=recovery"); - final uriParams = Supabase().parseUriParameters(uri); + final uriParams = Supabase.instance.parseUriParameters(uri); expect(uriParams.length, equals(5)); expect(uriParams['access_token'], equals('aaa')); expect(uriParams['refresh_token'], equals('bbb')); @@ -28,7 +28,7 @@ void main() { test('can parse flutter web redirect link', () async { final uri = Uri.parse( "http://localhost:55510/#access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=magiclink"); - final uriParams = Supabase().parseUriParameters(uri); + final uriParams = Supabase.instance.parseUriParameters(uri); expect(uriParams.length, equals(5)); expect(uriParams['access_token'], equals('aaa')); expect(uriParams['refresh_token'], equals('bbb')); @@ -37,7 +37,7 @@ void main() { test('can parse flutter web custom page redirect link', () async { final uri = Uri.parse( "http://localhost:55510/#/webAuth%23access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=magiclink"); - final uriParams = Supabase().parseUriParameters(uri); + final uriParams = Supabase.instance.parseUriParameters(uri); expect(uriParams.length, equals(5)); expect(uriParams['access_token'], equals('aaa')); expect(uriParams['refresh_token'], equals('bbb'));