diff --git a/packages/supabase/lib/src/supabase_client.dart b/packages/supabase/lib/src/supabase_client.dart index ee9a352f..fb77e9e5 100644 --- a/packages/supabase/lib/src/supabase_client.dart +++ b/packages/supabase/lib/src/supabase_client.dart @@ -1,14 +1,8 @@ import 'dart:async'; -import 'package:functions_client/functions_client.dart'; -import 'package:gotrue/gotrue.dart'; import 'package:http/http.dart'; -import 'package:postgrest/postgrest.dart'; -import 'package:realtime_client/realtime_client.dart'; -import 'package:storage_client/storage_client.dart'; import 'package:supabase/src/constants.dart'; -import 'package:supabase/src/realtime_client_options.dart'; -import 'package:supabase/src/supabase_query_builder.dart'; +import 'package:supabase/supabase.dart'; import 'package:yet_another_json_isolate/yet_another_json_isolate.dart'; import 'auth_http_client.dart'; @@ -38,12 +32,13 @@ import 'auth_http_client.dart'; class SupabaseClient { final String supabaseUrl; final String supabaseKey; - final String schema; - final String restUrl; - final String realtimeUrl; - final String authUrl; - final String storageUrl; - final String functionsUrl; + final PostgrestClientOptions _postgrestOptions; + + final String _restUrl; + final String _realtimeUrl; + final String _authUrl; + final String _storageUrl; + final String _functionsUrl; final Map _headers; final Client? _httpClient; late final Client _authHttpClient; @@ -125,21 +120,19 @@ class SupabaseClient { SupabaseClient( this.supabaseUrl, this.supabaseKey, { - String? schema, - bool autoRefreshToken = true, + PostgrestClientOptions postgrestOptions = const PostgrestClientOptions(), + AuthClientOptions authOptions = const AuthClientOptions(), + StorageClientOptions storageOptions = const StorageClientOptions(), + RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(), Map? headers, Client? httpClient, - int storageRetryAttempts = 0, - RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(), YAJsonIsolate? isolate, - GotrueAsyncStorage? gotrueAsyncStorage, - AuthFlowType authFlowType = AuthFlowType.pkce, - }) : restUrl = '$supabaseUrl/rest/v1', - realtimeUrl = '$supabaseUrl/realtime/v1'.replaceAll('http', 'ws'), - authUrl = '$supabaseUrl/auth/v1', - storageUrl = '$supabaseUrl/storage/v1', - functionsUrl = '$supabaseUrl/functions/v1', - schema = schema ?? 'public', + }) : _restUrl = '$supabaseUrl/rest/v1', + _realtimeUrl = '$supabaseUrl/realtime/v1'.replaceAll('http', 'ws'), + _authUrl = '$supabaseUrl/auth/v1', + _storageUrl = '$supabaseUrl/storage/v1', + _functionsUrl = '$supabaseUrl/functions/v1', + _postgrestOptions = postgrestOptions, _headers = { ...Constants.defaultHeaders, if (headers != null) ...headers @@ -147,27 +140,27 @@ class SupabaseClient { _httpClient = httpClient, _isolate = isolate ?? (YAJsonIsolate()..initialize()) { auth = _initSupabaseAuthClient( - autoRefreshToken: autoRefreshToken, - gotrueAsyncStorage: gotrueAsyncStorage, - authFlowType: authFlowType, + autoRefreshToken: authOptions.autoRefreshToken, + gotrueAsyncStorage: authOptions.pkceAsyncStorage, + authFlowType: authOptions.authFlowType, ); _authHttpClient = AuthHttpClient(supabaseKey, httpClient ?? Client(), auth); rest = _initRestClient(); functions = _initFunctionsClient(); - storage = _initStorageClient(storageRetryAttempts); + storage = _initStorageClient(storageOptions.retryAttempts); realtime = _initRealtimeClient(options: realtimeClientOptions); _listenForAuthEvents(); } /// Perform a table operation. SupabaseQueryBuilder from(String table) { - final url = '$restUrl/$table'; + final url = '$_restUrl/$table'; _incrementId++; return SupabaseQueryBuilder( url, realtime, headers: {...rest.headers, ...headers}, - schema: schema, + schema: _postgrestOptions.schema, table: table, httpClient: _authHttpClient, incrementId: _incrementId, @@ -229,7 +222,7 @@ class SupabaseClient { authHeaders['Authorization'] = 'Bearer $supabaseKey'; return GoTrueClient( - url: authUrl, + url: _authUrl, headers: authHeaders, autoRefreshToken: autoRefreshToken, httpClient: _httpClient, @@ -240,9 +233,9 @@ class SupabaseClient { PostgrestClient _initRestClient() { return PostgrestClient( - restUrl, + _restUrl, headers: {...headers}, - schema: schema, + schema: _postgrestOptions.schema, httpClient: _authHttpClient, isolate: _isolate, ); @@ -250,7 +243,7 @@ class SupabaseClient { FunctionsClient _initFunctionsClient() { return FunctionsClient( - functionsUrl, + _functionsUrl, {...headers}, httpClient: _authHttpClient, isolate: _isolate, @@ -259,7 +252,7 @@ class SupabaseClient { SupabaseStorageClient _initStorageClient(int storageRetryAttempts) { return SupabaseStorageClient( - storageUrl, + _storageUrl, {...headers}, httpClient: _authHttpClient, retryAttempts: storageRetryAttempts, @@ -271,7 +264,7 @@ class SupabaseClient { }) { final eventsPerSecond = options.eventsPerSecond; return RealtimeClient( - realtimeUrl, + _realtimeUrl, params: { 'apikey': supabaseKey, if (eventsPerSecond != null) 'eventsPerSecond': '$eventsPerSecond' diff --git a/packages/supabase/lib/src/supabase_client_options.dart b/packages/supabase/lib/src/supabase_client_options.dart new file mode 100644 index 00000000..3587e618 --- /dev/null +++ b/packages/supabase/lib/src/supabase_client_options.dart @@ -0,0 +1,25 @@ +import 'package:supabase/supabase.dart'; + +class PostgrestClientOptions { + final String schema; + + const PostgrestClientOptions({this.schema = 'public'}); +} + +class AuthClientOptions { + final bool autoRefreshToken; + final GotrueAsyncStorage? pkceAsyncStorage; + final AuthFlowType authFlowType; + + const AuthClientOptions({ + this.autoRefreshToken = true, + this.pkceAsyncStorage, + this.authFlowType = AuthFlowType.pkce, + }); +} + +class StorageClientOptions { + final int retryAttempts; + + const StorageClientOptions({this.retryAttempts = 0}); +} diff --git a/packages/supabase/lib/supabase.dart b/packages/supabase/lib/supabase.dart index 5a931c5f..cf81414c 100644 --- a/packages/supabase/lib/supabase.dart +++ b/packages/supabase/lib/supabase.dart @@ -14,6 +14,7 @@ export 'src/auth_user.dart'; export 'src/realtime_client_options.dart'; export 'src/remove_subscription_result.dart'; export 'src/supabase_client.dart'; +export 'src/supabase_client_options.dart'; export 'src/supabase_event_types.dart'; export 'src/supabase_query_builder.dart'; export 'src/supabase_realtime_error.dart'; diff --git a/packages/supabase/test/client_test.dart b/packages/supabase/test/client_test.dart index 80559a24..559311b6 100644 --- a/packages/supabase/test/client_test.dart +++ b/packages/supabase/test/client_test.dart @@ -69,7 +69,7 @@ void main() { final client = SupabaseClient( 'http://${mockServer.address.host}:${mockServer.port}', "supabaseKey", - autoRefreshToken: false, + authOptions: AuthClientOptions(autoRefreshToken: false), ); await client.auth.recoverSession(sessionString); @@ -101,7 +101,7 @@ void main() { final client = SupabaseClient( 'http://${mockServer.address.host}:${mockServer.port}', "supabaseKey", - autoRefreshToken: false, + authOptions: AuthClientOptions(autoRefreshToken: false), ); final sessionData = getSessionData(expiresAt); await client.auth.recoverSession(sessionData.sessionString); diff --git a/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart new file mode 100644 index 00000000..c70f38b8 --- /dev/null +++ b/packages/supabase_flutter/lib/src/flutter_go_true_client_options.dart @@ -0,0 +1,26 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; + +class FlutterAuthClientOptions extends AuthClientOptions { + final LocalStorage? localStorage; + + const FlutterAuthClientOptions({ + super.authFlowType, + super.autoRefreshToken, + super.pkceAsyncStorage, + this.localStorage, + }); + + FlutterAuthClientOptions copyWith({ + AuthFlowType? authFlowType, + bool? autoRefreshToken, + LocalStorage? localStorage, + dynamic pkceAsyncStorage, + }) { + return FlutterAuthClientOptions( + authFlowType: authFlowType ?? this.authFlowType, + autoRefreshToken: autoRefreshToken ?? this.autoRefreshToken, + localStorage: localStorage ?? this.localStorage, + pkceAsyncStorage: pkceAsyncStorage ?? this.pkceAsyncStorage, + ); + } +} diff --git a/packages/supabase_flutter/lib/src/supabase.dart b/packages/supabase_flutter/lib/src/supabase.dart index 30062333..b5176b3a 100644 --- a/packages/supabase_flutter/lib/src/supabase.dart +++ b/packages/supabase_flutter/lib/src/supabase.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:supabase/supabase.dart'; import 'package:supabase_flutter/src/constants.dart'; +import 'package:supabase_flutter/src/flutter_go_true_client_options.dart'; import 'package:supabase_flutter/src/local_storage.dart'; import 'package:supabase_flutter/src/supabase_auth.dart'; @@ -65,42 +66,47 @@ class Supabase { static Future initialize({ required String url, required String anonKey, - String? schema, Map? headers, - LocalStorage? localStorage, Client? httpClient, - int storageRetryAttempts = 0, RealtimeClientOptions realtimeClientOptions = const RealtimeClientOptions(), - AuthFlowType authFlowType = AuthFlowType.pkce, - GotrueAsyncStorage? pkceAsyncStorage, + PostgrestClientOptions postgrestOptions = const PostgrestClientOptions(), + StorageClientOptions storageOptions = const StorageClientOptions(), + FlutterAuthClientOptions authOptions = + const FlutterAuthClientOptions(), bool? debug, }) async { assert( !_instance._initialized, 'This instance is already initialized', ); + if (authOptions.pkceAsyncStorage == null) { + authOptions = authOptions.copyWith( + pkceAsyncStorage: SharedPreferencesGotrueAsyncStorage(), + ); + } + if (authOptions.localStorage == null) { + authOptions = authOptions.copyWith( + localStorage: MigrationLocalStorage( + persistSessionKey: + "sb-${Uri.parse(url).host.split(".").first}-auth-token", + ), + ); + } _instance._init( url, anonKey, httpClient: httpClient, customHeaders: headers, - schema: schema, - storageRetryAttempts: storageRetryAttempts, realtimeClientOptions: realtimeClientOptions, - gotrueAsyncStorage: - pkceAsyncStorage ?? SharedPreferencesGotrueAsyncStorage(), - authFlowType: authFlowType, + authOptions: authOptions, + postgrestOptions: postgrestOptions, + storageOptions: storageOptions, ); _instance._debugEnable = debug ?? kDebugMode; _instance.log('***** Supabase init completed $_instance'); - await SupabaseAuth.initialize( - localStorage: localStorage ?? - MigrationLocalStorage( - persistSessionKey: - "sb-${Uri.parse(url).host.split(".").first}-auth-token"), - authFlowType: authFlowType, - ); + _instance._supabaseAuth = SupabaseAuth(); + await _instance._supabaseAuth.initialize(options: authOptions); return _instance; } @@ -114,12 +120,15 @@ class Supabase { /// /// Throws an error if [Supabase.initialize] was not called. late SupabaseClient client; + + late SupabaseAuth _supabaseAuth; + bool _debugEnable = false; /// Dispose the instance to free up resources. void dispose() { client.dispose(); - SupabaseAuth.instance.dispose(); + _instance._supabaseAuth.dispose(); _initialized = false; } @@ -128,11 +137,10 @@ class Supabase { String supabaseAnonKey, { Client? httpClient, Map? customHeaders, - String? schema, - required int storageRetryAttempts, required RealtimeClientOptions realtimeClientOptions, - required GotrueAsyncStorage gotrueAsyncStorage, - required AuthFlowType authFlowType, + required PostgrestClientOptions postgrestOptions, + required StorageClientOptions storageOptions, + required AuthClientOptions authOptions, }) { final headers = { ...Constants.defaultHeaders, @@ -143,11 +151,10 @@ class Supabase { supabaseAnonKey, httpClient: httpClient, headers: headers, - schema: schema, - storageRetryAttempts: storageRetryAttempts, realtimeClientOptions: realtimeClientOptions, - gotrueAsyncStorage: gotrueAsyncStorage, - authFlowType: authFlowType, + postgrestOptions: postgrestOptions, + storageOptions: storageOptions, + authOptions: authOptions, ); _initialized = true; } diff --git a/packages/supabase_flutter/lib/src/supabase_auth.dart b/packages/supabase_flutter/lib/src/supabase_auth.dart index 7d61df97..9ebf49ca 100644 --- a/packages/supabase_flutter/lib/src/supabase_auth.dart +++ b/packages/supabase_flutter/lib/src/supabase_auth.dart @@ -17,29 +17,15 @@ import 'package:webview_flutter/webview_flutter.dart'; /// SupabaseAuth class SupabaseAuth with WidgetsBindingObserver { - SupabaseAuth._(); - static WidgetsBinding? get _widgetsBindingInstance => WidgetsBinding.instance; - static final SupabaseAuth _instance = SupabaseAuth._(); - - bool _initialized = false; late LocalStorage _localStorage; late AuthFlowType _authFlowType; - /// The [LocalStorage] instance used to persist the user session. - LocalStorage get localStorage => _localStorage; - - /// {@macro supabase.localstorage.hasAccessToken} - Future get hasAccessToken => _localStorage.hasAccessToken(); - - /// {@macro supabase.localstorage.accessToken} - Future get accessToken => _localStorage.accessToken(); - /// **ATTENTION**: `getInitialLink`/`getInitialUri` should be handled /// ONLY ONCE in your app's lifetime, since it is not meant to change /// throughout your app's life. - bool _initialDeeplinkIsHandled = false; + static bool _initialDeeplinkIsHandled = false; StreamSubscription? _authSubscription; @@ -47,45 +33,27 @@ class SupabaseAuth with WidgetsBindingObserver { final _appLinks = AppLinks(); - /// A [SupabaseAuth] instance. - /// - /// If not initialized, an [AssertionError] is thrown - static SupabaseAuth get instance { - assert( - _instance._initialized, - 'You must initialize the supabase instance before calling Supabase.instance', - ); - - return _instance; - } - - /// Initialize the [SupabaseAuth] instance. - /// - /// It's necessary to initialize before calling [SupabaseAuth.instance] - static Future initialize({ - required LocalStorage localStorage, - required AuthFlowType authFlowType, + Future initialize({ + required FlutterAuthClientOptions options, }) async { - _instance._initialized = true; - _instance._localStorage = localStorage; - _instance._authFlowType = authFlowType; + _localStorage = options.localStorage!; + _authFlowType = options.authFlowType; - _instance._authSubscription = - Supabase.instance.client.auth.onAuthStateChange.listen( + _authSubscription = Supabase.instance.client.auth.onAuthStateChange.listen( (data) { - _instance._onAuthStateChange(data.event, data.session); + _onAuthStateChange(data.event, data.session); }, onError: (error, stackTrace) { Supabase.instance.log(error.toString(), stackTrace); }, ); - await _instance._localStorage.initialize(); + await _localStorage.initialize(); - final hasPersistedSession = await _instance._localStorage.hasAccessToken(); + final hasPersistedSession = await _localStorage.hasAccessToken(); var shouldEmitInitialSession = true; if (hasPersistedSession) { - final persistedSession = await _instance._localStorage.accessToken(); + final persistedSession = await _localStorage.accessToken(); if (persistedSession != null) { try { // At this point either an [AuthChangeEvent.signedIn] event or an exception should next be emitted by `onAuthStateChange` @@ -98,14 +66,14 @@ class SupabaseAuth with WidgetsBindingObserver { } } } - _widgetsBindingInstance?.addObserver(_instance); + _widgetsBindingInstance?.addObserver(this); if (kIsWeb || Platform.isAndroid || Platform.isIOS || Platform.isMacOS || Platform.isWindows || Platform.environment.containsKey('FLUTTER_TEST')) { - await _instance._startDeeplinkObserver(); + await _startDeeplinkObserver(); } // Emit a null session if the user did not have persisted session @@ -114,7 +82,6 @@ class SupabaseAuth with WidgetsBindingObserver { // ignore: invalid_use_of_internal_member .notifyAllSubscribers(AuthChangeEvent.initialSession); } - return _instance; } /// Dispose the instance to free up resources @@ -139,14 +106,12 @@ class SupabaseAuth with WidgetsBindingObserver { /// Recover/refresh session if it's available /// e.g. called on a splash screen when the app starts. Future _recoverSupabaseSession() async { - final bool exist = - await SupabaseAuth.instance.localStorage.hasAccessToken(); + final bool exist = await _localStorage.hasAccessToken(); if (!exist) { return false; } - final String? jsonStr = - await SupabaseAuth.instance.localStorage.accessToken(); + final String? jsonStr = await _localStorage.accessToken(); if (jsonStr == null) { return false; } @@ -155,7 +120,7 @@ class SupabaseAuth with WidgetsBindingObserver { await Supabase.instance.client.auth.recoverSession(jsonStr); return true; } catch (error) { - SupabaseAuth.instance.localStorage.removePersistedSession(); + _localStorage.removePersistedSession(); return false; } } @@ -240,7 +205,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// Callback when deeplink receiving succeeds Future _handleDeeplink(Uri uri) async { - if (!_instance._isAuthCallbackDeeplink(uri)) return; + if (!_isAuthCallbackDeeplink(uri)) return; Supabase.instance.log('***** SupabaseAuthState handleDeeplink $uri'); diff --git a/packages/supabase_flutter/lib/supabase_flutter.dart b/packages/supabase_flutter/lib/supabase_flutter.dart index 28a2da5a..b8032558 100644 --- a/packages/supabase_flutter/lib/supabase_flutter.dart +++ b/packages/supabase_flutter/lib/supabase_flutter.dart @@ -4,7 +4,8 @@ library supabase_flutter; export 'package:supabase/supabase.dart'; +export 'package:url_launcher/url_launcher.dart' show LaunchMode; + +export 'src/flutter_go_true_client_options.dart'; export 'src/local_storage.dart'; export 'src/supabase.dart'; -export 'src/supabase_auth.dart'; -export 'package:url_launcher/url_launcher.dart' show LaunchMode; diff --git a/packages/supabase_flutter/test/supabase_flutter_test.dart b/packages/supabase_flutter/test/supabase_flutter_test.dart index 73309ebd..7ce163ed 100644 --- a/packages/supabase_flutter/test/supabase_flutter_test.dart +++ b/packages/supabase_flutter/test/supabase_flutter_test.dart @@ -14,8 +14,10 @@ void main() { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseKey, - localStorage: MockLocalStorage(), - pkceAsyncStorage: MockAsyncStorage(), + authOptions: FlutterAuthClientOptions( + localStorage: MockLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), ); }); @@ -33,8 +35,10 @@ void main() { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseKey, - localStorage: MockLocalStorage(), - pkceAsyncStorage: MockAsyncStorage(), + authOptions: FlutterAuthClientOptions( + localStorage: MockLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), ); final newClient = Supabase.instance.client; @@ -48,8 +52,10 @@ void main() { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseKey, - localStorage: MockExpiredStorage(), - pkceAsyncStorage: MockAsyncStorage(), + authOptions: FlutterAuthClientOptions( + localStorage: MockExpiredStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), ); }); @@ -67,8 +73,10 @@ void main() { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseKey, - localStorage: MockEmptyLocalStorage(), - pkceAsyncStorage: MockAsyncStorage(), + authOptions: FlutterAuthClientOptions( + localStorage: MockEmptyLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), ); }); @@ -93,10 +101,11 @@ void main() { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseKey, - authFlowType: AuthFlowType.pkce, httpClient: pkceHttpClient, - localStorage: MockEmptyLocalStorage(), - pkceAsyncStorage: MockAsyncStorage(), + authOptions: FlutterAuthClientOptions( + localStorage: MockEmptyLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), ); }); diff --git a/packages/supabase_flutter/test/widget_test.dart b/packages/supabase_flutter/test/widget_test.dart index 06cda38f..47f9eff2 100644 --- a/packages/supabase_flutter/test/widget_test.dart +++ b/packages/supabase_flutter/test/widget_test.dart @@ -21,8 +21,10 @@ void main() { await Supabase.initialize( url: supabaseUrl, anonKey: supabaseKey, - localStorage: MockLocalStorage(), - pkceAsyncStorage: MockAsyncStorage(), + authOptions: FlutterAuthClientOptions( + localStorage: MockLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), ); await tester.pumpWidget(const MaterialApp(home: MockWidget())); await tester.tap(find.text('Sign out'));