From 3486a1057cfe7f823fd60bbacf4659e480df8d07 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Mon, 30 May 2022 19:22:16 +0900 Subject: [PATCH 01/25] breaking: consolidated supabase_state, and supabase_auth_required_state to supabase_auth_state --- lib/src/supabase_auth_required_state.dart | 104 ---------------------- lib/src/supabase_auth_state.dart | 62 +++++++------ lib/src/supabase_deep_linking_mixin.dart | 2 +- lib/src/supabase_state.dart | 36 -------- lib/supabase_flutter.dart | 1 - test/widget_test_stubs.dart | 2 +- 6 files changed, 36 insertions(+), 171 deletions(-) delete mode 100644 lib/src/supabase_auth_required_state.dart delete mode 100644 lib/src/supabase_state.dart diff --git a/lib/src/supabase_auth_required_state.dart b/lib/src/supabase_auth_required_state.dart deleted file mode 100644 index 112beb4e..00000000 --- a/lib/src/supabase_auth_required_state.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:supabase_flutter/src/supabase_state.dart'; - -import 'package:supabase_flutter/supabase_flutter.dart'; - -// Because `WidgetsBinding.instance` became none-nullable in Flutter 3.0 -// we need the following ignore rule to avoid analysis warning -// in both Flutter 2.0 and 3.0 -// ignore_for_file: invalid_null_aware_operator - -abstract class SupabaseAuthRequiredState - extends SupabaseState with WidgetsBindingObserver { - late final StreamSubscription _authStateListener; - - @override - void initState() { - super.initState(); - - _authStateListener = SupabaseAuth.instance.onAuthChange.listen((event) { - if (event == AuthChangeEvent.signedOut) { - onUnauthenticated(); - } - }); - - if (Supabase.instance.client.auth.currentSession == null) { - _recoverSupabaseSession(); - } else { - onAuthenticated(Supabase.instance.client.auth.currentSession!); - } - } - - @override - void dispose() { - _authStateListener.cancel(); - super.dispose(); - } - - @override - void startAuthObserver() { - Supabase.instance.log('***** SupabaseAuthRequiredState startAuthObserver'); - WidgetsBinding.instance?.addObserver(this); - } - - @override - void stopAuthObserver() { - Supabase.instance.log('***** SupabaseAuthRequiredState stopAuthObserver'); - WidgetsBinding.instance?.removeObserver(this); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.resumed: - onResumed(); - break; - case AppLifecycleState.inactive: - break; - case AppLifecycleState.paused: - break; - case AppLifecycleState.detached: - break; - } - } - - Future onResumed() async { - Supabase.instance.log('***** SupabaseAuthRequiredState onResumed'); - return _recoverSupabaseSession(); - } - - Future _recoverSupabaseSession() async { - final bool exist = - await SupabaseAuth.instance.localStorage.hasAccessToken(); - if (!exist) { - onUnauthenticated(); - return false; - } - - final String? jsonStr = - await SupabaseAuth.instance.localStorage.accessToken(); - if (jsonStr == null) { - onUnauthenticated(); - return false; - } - - final response = - await Supabase.instance.client.auth.recoverSession(jsonStr); - if (response.error != null) { - SupabaseAuth.instance.localStorage.removePersistedSession(); - onUnauthenticated(); - return false; - } else { - onAuthenticated(response.data!); - return true; - } - } - - /// Callback when user session is ready - void onAuthenticated(Session session) {} - - /// Callback when user is unauthenticated - void onUnauthenticated(); -} diff --git a/lib/src/supabase_auth_state.dart b/lib/src/supabase_auth_state.dart index 163f3aba..a2228487 100644 --- a/lib/src/supabase_auth_state.dart +++ b/lib/src/supabase_auth_state.dart @@ -2,21 +2,31 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:supabase_flutter/src/supabase_deep_linking_mixin.dart'; -import 'package:supabase_flutter/src/supabase_state.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; /// Interface for user authentication screen /// It supports deeplink authentication -abstract class SupabaseAuthState - extends SupabaseState with SupabaseDeepLinkingMixin { - @override +abstract class SupabaseAuthState extends State + with SupabaseDeepLinkingMixin, WidgetsBindingObserver { + /// enable auth observer + /// e.g. on nested authentication flow, call this method on navigation push.then() + /// + /// ```dart + /// Navigator.pushNamed(context, '/signUp').then((_) => startAuthObserver()); + /// ``` void startAuthObserver() { Supabase.instance.log('***** SupabaseAuthState startAuthObserver'); startDeeplinkObserver(); } - @override + /// disable auth observer + /// e.g. on nested authentication flow, call this method before navigation push + /// + /// ```dart + /// stopAuthObserver(); + /// Navigator.pushNamed(context, '/signUp').then((_) =>{}); + /// ``` void stopAuthObserver() { Supabase.instance.log('***** SupabaseAuthState stopAuthObserver'); stopDeeplinkObserver(); @@ -31,7 +41,7 @@ abstract class SupabaseAuthState // notify auth deeplink received onReceivedAuthDeeplink(uri); - return recoverSessionFromUrl(uri); + return _recoverSessionFromUrl(uri); } @override @@ -39,25 +49,33 @@ abstract class SupabaseAuthState Supabase.instance.log('onErrorReceivingDeppLink message: $message'); } - late final StreamSubscription _authStateListener; - @override void initState() { - _authStateListener = SupabaseAuth.instance.onAuthChange.listen((event) { - if (event == AuthChangeEvent.signedOut) { - onUnauthenticated(); - } - }); + _recoverSupabaseSession(); super.initState(); } @override void dispose() { - _authStateListener.cancel(); super.dispose(); } - Future recoverSessionFromUrl(Uri uri) async { + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + _recoverSupabaseSession(); + break; + case AppLifecycleState.inactive: + break; + case AppLifecycleState.paused: + break; + case AppLifecycleState.detached: + break; + } + } + + Future _recoverSessionFromUrl(Uri uri) async { final uriParameters = SupabaseAuth.instance.parseUriParameters(uri); final type = uriParameters['type'] ?? ''; @@ -68,8 +86,6 @@ abstract class SupabaseAuthState } else { if (type == 'recovery') { onPasswordRecovery(response.data!); - } else { - onAuthenticated(response.data!); } } return true; @@ -77,18 +93,16 @@ abstract class SupabaseAuthState /// Recover/refresh session if it's available /// e.g. called on a Splash screen when app starts. - Future recoverSupabaseSession() async { + Future _recoverSupabaseSession() async { final bool exist = await SupabaseAuth.instance.localStorage.hasAccessToken(); if (!exist) { - onUnauthenticated(); return false; } final String? jsonStr = await SupabaseAuth.instance.localStorage.accessToken(); if (jsonStr == null) { - onUnauthenticated(); return false; } @@ -96,10 +110,8 @@ abstract class SupabaseAuthState await Supabase.instance.client.auth.recoverSession(jsonStr); if (response.error != null) { SupabaseAuth.instance.localStorage.removePersistedSession(); - onUnauthenticated(); return false; } else { - onAuthenticated(response.data!); return true; } } @@ -109,12 +121,6 @@ abstract class SupabaseAuthState Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); } - /// Callback when user is unauthenticated - void onUnauthenticated(); - - /// Callback when user is authenticated - void onAuthenticated(Session session); - /// Callback when authentication deeplink is recovery password type void onPasswordRecovery(Session session); diff --git a/lib/src/supabase_deep_linking_mixin.dart b/lib/src/supabase_deep_linking_mixin.dart index 18f89cae..5a72b36a 100644 --- a/lib/src/supabase_deep_linking_mixin.dart +++ b/lib/src/supabase_deep_linking_mixin.dart @@ -8,7 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uni_links/uni_links.dart'; mixin SupabaseDeepLinkingMixin on State { - StreamSubscription? _sub; + StreamSubscription? _sub; void startDeeplinkObserver() { Supabase.instance.log('***** SupabaseDeepLinkingMixin startAuthObserver'); diff --git a/lib/src/supabase_state.dart b/lib/src/supabase_state.dart deleted file mode 100644 index 55af5544..00000000 --- a/lib/src/supabase_state.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// Interface for screen that requires an authenticated user -abstract class SupabaseState extends State { - @override - void initState() { - super.initState(); - startAuthObserver(); - } - - @override - void dispose() { - stopAuthObserver(); - super.dispose(); - } - - @override - Widget build(BuildContext context) => const SizedBox.shrink(); - - /// enable auth observer - /// e.g. on nested authentication flow, call this method on navigation push.then() - /// - /// ```dart - /// Navigator.pushNamed(context, '/signUp').then((_) => startAuthObserver()); - /// ``` - void startAuthObserver(); - - /// disable auth observer - /// e.g. on nested authentication flow, call this method before navigation push - /// - /// ```dart - /// stopAuthObserver(); - /// Navigator.pushNamed(context, '/signUp').then((_) =>{}); - /// ``` - void stopAuthObserver(); -} diff --git a/lib/supabase_flutter.dart b/lib/supabase_flutter.dart index f904b4c1..0786cb63 100644 --- a/lib/supabase_flutter.dart +++ b/lib/supabase_flutter.dart @@ -7,5 +7,4 @@ export 'package:supabase/supabase.dart'; export 'src/local_storage.dart'; export 'src/supabase.dart'; export 'src/supabase_auth.dart'; -export 'src/supabase_auth_required_state.dart'; export 'src/supabase_auth_state.dart'; diff --git a/test/widget_test_stubs.dart b/test/widget_test_stubs.dart index b4b56a08..1f7ed19f 100644 --- a/test/widget_test_stubs.dart +++ b/test/widget_test_stubs.dart @@ -8,7 +8,7 @@ class MockWidget extends StatefulWidget { _MockWidgetState createState() => _MockWidgetState(); } -class _MockWidgetState extends SupabaseAuthRequiredState { +class _MockWidgetState extends SupabaseAuthState { bool isSignedIn = true; @override From 05770a47c5db523cbe03c89d0977523dc576a894 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Tue, 31 May 2022 14:26:26 +0900 Subject: [PATCH 02/25] breaking: callback methods are now optional for since they are not always used --- lib/src/supabase_auth_state.dart | 12 ++++++++---- test/widget_test_stubs.dart | 7 ------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/src/supabase_auth_state.dart b/lib/src/supabase_auth_state.dart index a2228487..3bf04505 100644 --- a/lib/src/supabase_auth_state.dart +++ b/lib/src/supabase_auth_state.dart @@ -121,9 +121,13 @@ abstract class SupabaseAuthState extends State Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); } - /// Callback when authentication deeplink is recovery password type - void onPasswordRecovery(Session session); + /// Callback when authentication deeplink is recovery password type. Optional + void onPasswordRecovery(Session session) { + Supabase.instance.log(session.toString()); + } - /// Callback when recovering session from authentication deeplink throws error - void onErrorAuthenticating(String message); + /// Callback when recovering session from authentication deeplink throws error. Optional + void onErrorAuthenticating(String message) { + Supabase.instance.log(message); + } } diff --git a/test/widget_test_stubs.dart b/test/widget_test_stubs.dart index 1f7ed19f..c1d7cca1 100644 --- a/test/widget_test_stubs.dart +++ b/test/widget_test_stubs.dart @@ -11,13 +11,6 @@ class MockWidget extends StatefulWidget { class _MockWidgetState extends SupabaseAuthState { bool isSignedIn = true; - @override - void onUnauthenticated() { - setState(() { - isSignedIn = false; - }); - } - @override Widget build(BuildContext context) { return isSignedIn From fdc3adeae37fa09d21f6c42ee3eec4433c1811db Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Tue, 31 May 2022 17:51:22 +0900 Subject: [PATCH 03/25] breaking: removed onPasswordRecovery on SupabaseAuthState. Users can use onAuthChange instead --- lib/src/supabase_auth_state.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/lib/src/supabase_auth_state.dart b/lib/src/supabase_auth_state.dart index 3bf04505..8b4866c1 100644 --- a/lib/src/supabase_auth_state.dart +++ b/lib/src/supabase_auth_state.dart @@ -76,17 +76,10 @@ abstract class SupabaseAuthState extends State } Future _recoverSessionFromUrl(Uri uri) async { - final uriParameters = SupabaseAuth.instance.parseUriParameters(uri); - final type = uriParameters['type'] ?? ''; - // recover session from deeplink final response = await Supabase.instance.client.auth.getSessionFromUrl(uri); if (response.error != null) { onErrorAuthenticating(response.error!.message); - } else { - if (type == 'recovery') { - onPasswordRecovery(response.data!); - } } return true; } @@ -121,11 +114,6 @@ abstract class SupabaseAuthState extends State Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); } - /// Callback when authentication deeplink is recovery password type. Optional - void onPasswordRecovery(Session session) { - Supabase.instance.log(session.toString()); - } - /// Callback when recovering session from authentication deeplink throws error. Optional void onErrorAuthenticating(String message) { Supabase.instance.log(message); From ff4c4e9cdfece44fab4553b8b4e14338263a1fed Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Wed, 1 Jun 2022 13:27:02 +0900 Subject: [PATCH 04/25] breaking: remove AuthRequiredState related stuff from readme --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 766507bf..eacfcfbd 100644 --- a/README.md +++ b/README.md @@ -88,18 +88,13 @@ Future signIn(String email, String password) async { ### SupabaseAuthState -It helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter... +It persists the auth state on local storage. +It also helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter... For more details, take a look at the example [here](https://github.com/phamhieu/supabase-flutter-demo/blob/main/lib/components/auth_state.dart) > When using with a nested authentication flow, remember to call `startAuthObserver()` and `stopAuthObserver()` before/after navigation to new screen to prevent multiple observers running at the same time. Take a look at the example [here](https://github.com/phamhieu/supabase-flutter-demo/blob/026c6e8cbb05a5b1b76a50ce82d936016844ba1b/lib/screens/signin_screen.dart#L165-L170) -### SupabaseAuthRequiredState - -It helps you protect route that requires an authenticated user. - -For more details, take a look at the example [here](https://github.com/phamhieu/supabase-flutter-demo/blob/main/lib/components/auth_required_state.dart) - ### signInWithProvider This method will automatically launch the auth url and open a browser for user to sign in with 3rd party login. From f10cd0aeec376d4ca4c60b1c98a98e04fabb5721 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Wed, 15 Jun 2022 17:00:32 +0900 Subject: [PATCH 05/25] breaking: moving token refresh logic into SupabaseAuth --- lib/src/supabase_auth.dart | 48 +++++++++++++++++++++++++++-- lib/src/supabase_auth_state.dart | 53 +------------------------------- 2 files changed, 47 insertions(+), 54 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 56375265..b6b02d1b 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -1,11 +1,14 @@ import 'dart:async'; +import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; /// SupabaseAuth -class SupabaseAuth { - SupabaseAuth._(); +class SupabaseAuth with WidgetsBindingObserver { + SupabaseAuth._() { + WidgetsBinding.instance.addObserver(this); + } static final SupabaseAuth _instance = SupabaseAuth._(); bool _initialized = false; @@ -95,6 +98,47 @@ class SupabaseAuth { void dispose() { _listenerController.close(); _authSubscription?.data?.unsubscribe(); + WidgetsBinding.instance.removeObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + _recoverSupabaseSession(); + break; + case AppLifecycleState.inactive: + break; + case AppLifecycleState.paused: + break; + case AppLifecycleState.detached: + break; + } + } + + /// Recover/refresh session if it's available + /// e.g. called on a Splash screen when app starts. + Future _recoverSupabaseSession() async { + final bool exist = + await SupabaseAuth.instance.localStorage.hasAccessToken(); + if (!exist) { + return false; + } + + final String? jsonStr = + await SupabaseAuth.instance.localStorage.accessToken(); + if (jsonStr == null) { + return false; + } + + final response = + await Supabase.instance.client.auth.recoverSession(jsonStr); + if (response.error != null) { + SupabaseAuth.instance.localStorage.removePersistedSession(); + return false; + } else { + return true; + } } void _onAuthStateChange(AuthChangeEvent event, Session? session) { diff --git a/lib/src/supabase_auth_state.dart b/lib/src/supabase_auth_state.dart index 8b4866c1..830be435 100644 --- a/lib/src/supabase_auth_state.dart +++ b/lib/src/supabase_auth_state.dart @@ -8,7 +8,7 @@ import 'package:supabase_flutter/supabase_flutter.dart'; /// Interface for user authentication screen /// It supports deeplink authentication abstract class SupabaseAuthState extends State - with SupabaseDeepLinkingMixin, WidgetsBindingObserver { + with SupabaseDeepLinkingMixin { /// enable auth observer /// e.g. on nested authentication flow, call this method on navigation push.then() /// @@ -49,32 +49,6 @@ abstract class SupabaseAuthState extends State Supabase.instance.log('onErrorReceivingDeppLink message: $message'); } - @override - void initState() { - _recoverSupabaseSession(); - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - switch (state) { - case AppLifecycleState.resumed: - _recoverSupabaseSession(); - break; - case AppLifecycleState.inactive: - break; - case AppLifecycleState.paused: - break; - case AppLifecycleState.detached: - break; - } - } - Future _recoverSessionFromUrl(Uri uri) async { // recover session from deeplink final response = await Supabase.instance.client.auth.getSessionFromUrl(uri); @@ -84,31 +58,6 @@ abstract class SupabaseAuthState extends State return true; } - /// Recover/refresh session if it's available - /// e.g. called on a Splash screen when app starts. - Future _recoverSupabaseSession() async { - final bool exist = - await SupabaseAuth.instance.localStorage.hasAccessToken(); - if (!exist) { - return false; - } - - final String? jsonStr = - await SupabaseAuth.instance.localStorage.accessToken(); - if (jsonStr == null) { - return false; - } - - final response = - await Supabase.instance.client.auth.recoverSession(jsonStr); - if (response.error != null) { - SupabaseAuth.instance.localStorage.removePersistedSession(); - return false; - } else { - return true; - } - } - /// Callback when deeplink received and is processing. Optional void onReceivedAuthDeeplink(Uri uri) { Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); From 5a25f2f06e43bd05396f1860396f0c2338fe7fd6 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Wed, 15 Jun 2022 18:34:56 +0900 Subject: [PATCH 06/25] breaking: make supabase url and anonkey required --- lib/src/supabase.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/supabase.dart b/lib/src/supabase.dart index 9c490746..93ac2145 100644 --- a/lib/src/supabase.dart +++ b/lib/src/supabase.dart @@ -39,8 +39,8 @@ class Supabase { /// This must be called only once. If called more than once, an /// [AssertionError] is thrown static Future initialize({ - String? url, - String? anonKey, + required String url, + required String anonKey, String? authCallbackUrlHostname, bool? debug, LocalStorage? localStorage, From bafed0d330c727152dae0505fe8593f8f8f1e9b8 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 16 Jun 2022 00:55:15 +0900 Subject: [PATCH 07/25] breaking: removed all supabase classes and consolidate all auth related logic to SupabaseAuth --- lib/src/supabase.dart | 16 ++--- lib/src/supabase_auth.dart | 88 ++++++++++++++++++++++++ lib/src/supabase_auth_state.dart | 70 ------------------- lib/src/supabase_deep_linking_mixin.dart | 72 ------------------- lib/supabase_flutter.dart | 1 - 5 files changed, 95 insertions(+), 152 deletions(-) delete mode 100644 lib/src/supabase_auth_state.dart delete mode 100644 lib/src/supabase_deep_linking_mixin.dart diff --git a/lib/src/supabase.dart b/lib/src/supabase.dart index 93ac2145..9940a997 100644 --- a/lib/src/supabase.dart +++ b/lib/src/supabase.dart @@ -49,16 +49,14 @@ class Supabase { !_instance._initialized, 'This instance is already initialized', ); - if (url != null && anonKey != null) { - _instance._init(url, anonKey); - _instance._debugEnable = debug ?? kDebugMode; - _instance.log('***** Supabase init completed $_instance'); + _instance._init(url, anonKey); + _instance._debugEnable = debug ?? kDebugMode; + _instance.log('***** Supabase init completed $_instance'); - await SupabaseAuth.initialize( - localStorage: localStorage ?? const HiveLocalStorage(), - authCallbackUrlHostname: authCallbackUrlHostname, - ); - } + await SupabaseAuth.initialize( + localStorage: localStorage ?? const HiveLocalStorage(), + authCallbackUrlHostname: authCallbackUrlHostname, + ); return _instance; } diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index b6b02d1b..452a3e86 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -29,6 +32,8 @@ class SupabaseAuth with WidgetsBindingObserver { GotrueSubscription? _authSubscription; final _listenerController = StreamController.broadcast(); + StreamSubscription? _deeplinkSubscription; + /// Listen to auth change events. /// /// ```dart @@ -98,6 +103,7 @@ class SupabaseAuth with WidgetsBindingObserver { void dispose() { _listenerController.close(); _authSubscription?.data?.unsubscribe(); + stopDeeplinkObserver(); WidgetsBinding.instance.removeObserver(this); } @@ -194,6 +200,88 @@ class SupabaseAuth with WidgetsBindingObserver { return _authCallbackUrlHostname == uri.host; } } + + /// Enable deep link observer to handle deep links + void startDeeplinkObserver() { + Supabase.instance.log('***** SupabaseDeepLinkingMixin startAuthObserver'); + _handleIncomingLinks(); + _handleInitialUri(); + } + + /// Stop deep link observer + /// + /// Automatically called on dispose(). + void stopDeeplinkObserver() { + Supabase.instance.log('***** SupabaseDeepLinkingMixin stopAuthObserver'); + if (_deeplinkSubscription != null) _deeplinkSubscription?.cancel(); + } + + /// Handle incoming links - the ones that the app will recieve from the OS + /// while already started. + void _handleIncomingLinks() { + if (!kIsWeb) { + // It will handle app links while the app is already started - be it in + // the foreground or in the background. + _deeplinkSubscription = uriLinkStream.listen( + (Uri? uri) { + if (uri != null) { + _handleDeeplink(uri); + } + }, + onError: (Object err) { + _onErrorReceivingDeeplink(err.toString()); + }, + ); + } + } + + /// Handle the initial Uri - the one the app was started with + /// + /// **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. + /// + /// We handle all exceptions, since it is called from initState. + Future _handleInitialUri() async { + if (!SupabaseAuth.instance.shouldHandleInitialDeeplink()) return; + + try { + final uri = await getInitialUri(); + if (uri != null) { + _handleDeeplink(uri); + } + } on PlatformException { + // Platform messages may fail but we ignore the exception + } on FormatException catch (err) { + _onErrorReceivingDeeplink(err.message); + } + } + + /// Callback when deeplink receiving succeeds + Future _handleDeeplink(Uri uri) async { + if (!SupabaseAuth.instance.isAuthCallbackDeeplink(uri)) return false; + + Supabase.instance.log('***** SupabaseAuthState handleDeeplink $uri'); + + // notify auth deeplink received + Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); + + return _recoverSessionFromUrl(uri); + } + + Future _recoverSessionFromUrl(Uri uri) async { + // recover session from deeplink + final response = await Supabase.instance.client.auth.getSessionFromUrl(uri); + if (response.error != null) { + Supabase.instance.log(response.error!.message); + } + return true; + } + + /// Callback when deeplink receiving throw error + void _onErrorReceivingDeeplink(String message) { + Supabase.instance.log('onErrorReceivingDeppLink message: $message'); + } } extension GoTrueClientSignInProvider on GoTrueClient { diff --git a/lib/src/supabase_auth_state.dart b/lib/src/supabase_auth_state.dart deleted file mode 100644 index 830be435..00000000 --- a/lib/src/supabase_auth_state.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/widgets.dart'; -import 'package:supabase_flutter/src/supabase_deep_linking_mixin.dart'; - -import 'package:supabase_flutter/supabase_flutter.dart'; - -/// Interface for user authentication screen -/// It supports deeplink authentication -abstract class SupabaseAuthState extends State - with SupabaseDeepLinkingMixin { - /// enable auth observer - /// e.g. on nested authentication flow, call this method on navigation push.then() - /// - /// ```dart - /// Navigator.pushNamed(context, '/signUp').then((_) => startAuthObserver()); - /// ``` - void startAuthObserver() { - Supabase.instance.log('***** SupabaseAuthState startAuthObserver'); - startDeeplinkObserver(); - } - - /// disable auth observer - /// e.g. on nested authentication flow, call this method before navigation push - /// - /// ```dart - /// stopAuthObserver(); - /// Navigator.pushNamed(context, '/signUp').then((_) =>{}); - /// ``` - void stopAuthObserver() { - Supabase.instance.log('***** SupabaseAuthState stopAuthObserver'); - stopDeeplinkObserver(); - } - - @override - Future handleDeeplink(Uri uri) async { - if (!SupabaseAuth.instance.isAuthCallbackDeeplink(uri)) return false; - - Supabase.instance.log('***** SupabaseAuthState handleDeeplink $uri'); - - // notify auth deeplink received - onReceivedAuthDeeplink(uri); - - return _recoverSessionFromUrl(uri); - } - - @override - void onErrorReceivingDeeplink(String message) { - Supabase.instance.log('onErrorReceivingDeppLink message: $message'); - } - - Future _recoverSessionFromUrl(Uri uri) async { - // recover session from deeplink - final response = await Supabase.instance.client.auth.getSessionFromUrl(uri); - if (response.error != null) { - onErrorAuthenticating(response.error!.message); - } - return true; - } - - /// Callback when deeplink received and is processing. Optional - void onReceivedAuthDeeplink(Uri uri) { - Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); - } - - /// Callback when recovering session from authentication deeplink throws error. Optional - void onErrorAuthenticating(String message) { - Supabase.instance.log(message); - } -} diff --git a/lib/src/supabase_deep_linking_mixin.dart b/lib/src/supabase_deep_linking_mixin.dart deleted file mode 100644 index 5a72b36a..00000000 --- a/lib/src/supabase_deep_linking_mixin.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'package:supabase_flutter/supabase_flutter.dart'; -import 'package:uni_links/uni_links.dart'; - -mixin SupabaseDeepLinkingMixin on State { - StreamSubscription? _sub; - - void startDeeplinkObserver() { - Supabase.instance.log('***** SupabaseDeepLinkingMixin startAuthObserver'); - _handleIncomingLinks(); - _handleInitialUri(); - } - - void stopDeeplinkObserver() { - Supabase.instance.log('***** SupabaseDeepLinkingMixin stopAuthObserver'); - if (_sub != null) _sub?.cancel(); - } - - /// Handle incoming links - the ones that the app will recieve from the OS - /// while already started. - void _handleIncomingLinks() { - if (!kIsWeb) { - // It will handle app links while the app is already started - be it in - // the foreground or in the background. - _sub = uriLinkStream.listen( - (Uri? uri) { - if (mounted && uri != null) { - handleDeeplink(uri); - } - }, - onError: (Object err) { - if (!mounted) return; - onErrorReceivingDeeplink(err.toString()); - }, - ); - } - } - - /// Handle the initial Uri - the one the app was started with - /// - /// **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. - /// - /// We handle all exceptions, since it is called from initState. - Future _handleInitialUri() async { - if (!SupabaseAuth.instance.shouldHandleInitialDeeplink()) return; - - try { - final uri = await getInitialUri(); - if (mounted && uri != null) { - handleDeeplink(uri); - } - } on PlatformException { - // Platform messages may fail but we ignore the exception - } on FormatException catch (err) { - if (!mounted) return; - onErrorReceivingDeeplink(err.message); - } - } - - /// Callback when deeplink receiving succeeds - void handleDeeplink(Uri uri); - - /// Callback when deeplink receiving throw error - void onErrorReceivingDeeplink(String message); -} diff --git a/lib/supabase_flutter.dart b/lib/supabase_flutter.dart index 0786cb63..4227cb20 100644 --- a/lib/supabase_flutter.dart +++ b/lib/supabase_flutter.dart @@ -7,4 +7,3 @@ export 'package:supabase/supabase.dart'; export 'src/local_storage.dart'; export 'src/supabase.dart'; export 'src/supabase_auth.dart'; -export 'src/supabase_auth_state.dart'; From 837eef263704bdf895edb6065b7afb646d214ae0 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 16 Jun 2022 15:45:31 +0900 Subject: [PATCH 08/25] breaking: fixed tests to not use SupabaseAuthState --- test/widget_test.dart | 3 ++- test/widget_test_stubs.dart | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 6fc34c4e..527d2268 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -18,7 +18,8 @@ void main() { ); }); - testWidgets('Signing out triggers onUnauthenticated()', (tester) async { + testWidgets('Signing out triggers AuthChangeEvent.signedOut event', + (tester) async { await tester.pumpWidget(const MaterialApp(home: MockWidget())); await tester.tap(find.text('Sign out')); await tester.pump(); diff --git a/test/widget_test_stubs.dart b/test/widget_test_stubs.dart index c1d7cca1..dcb9c7d5 100644 --- a/test/widget_test_stubs.dart +++ b/test/widget_test_stubs.dart @@ -8,7 +8,7 @@ class MockWidget extends StatefulWidget { _MockWidgetState createState() => _MockWidgetState(); } -class _MockWidgetState extends SupabaseAuthState { +class _MockWidgetState extends State { bool isSignedIn = true; @override @@ -22,6 +22,18 @@ class _MockWidgetState extends SupabaseAuthState { ) : const Text('You have signed out'); } + + @override + void initState() { + SupabaseAuth.instance.onAuthChange.listen((event) { + if (event == AuthChangeEvent.signedOut) { + setState(() { + isSignedIn = false; + }); + } + }); + super.initState(); + } } class MockLocalStorage extends LocalStorage { From d5cb468ff6124b485faae05f546e139658508201 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 16 Jun 2022 15:50:44 +0900 Subject: [PATCH 09/25] fix: lint error on Flutter 2.5 for WidgetsBinding.instance possibly null --- lib/src/supabase_auth.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 452a3e86..9ef52d7f 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -7,10 +7,12 @@ import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher.dart'; +// ignore_for_file: invalid_null_aware_operator + /// SupabaseAuth class SupabaseAuth with WidgetsBindingObserver { SupabaseAuth._() { - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance?.addObserver(this); } static final SupabaseAuth _instance = SupabaseAuth._(); @@ -104,7 +106,7 @@ class SupabaseAuth with WidgetsBindingObserver { _listenerController.close(); _authSubscription?.data?.unsubscribe(); stopDeeplinkObserver(); - WidgetsBinding.instance.removeObserver(this); + WidgetsBinding.instance?.removeObserver(this); } @override From 93f630a4d2a05d4b05edec827f0034ee24a703ba Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 16 Jun 2022 16:05:58 +0900 Subject: [PATCH 10/25] fix: calling ensureInitialized in tests --- test/supabase_flutter_test.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/supabase_flutter_test.dart b/test/supabase_flutter_test.dart index 5478facf..0250d1ac 100644 --- a/test/supabase_flutter_test.dart +++ b/test/supabase_flutter_test.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -9,6 +10,7 @@ void main() { const supabaseKey = ''; setUpAll(() async { + WidgetsFlutterBinding.ensureInitialized(); // Initialize the Supabase singleton await Supabase.initialize( url: supabaseUrl, @@ -17,12 +19,12 @@ void main() { ); }); - test('can access Supabase singleton', () async { + testWidgets('can access Supabase singleton', (tester) async { final client = Supabase.instance.client; expect(client, isNotNull); }); - test('can parse deeplink', () async { + testWidgets('can parse deeplink', (tester) async { final uri = Uri.parse( "io.supabase.flutterdemo://login-callback#access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=recovery", ); @@ -32,7 +34,7 @@ void main() { expect(uriParams['refresh_token'], equals('bbb')); }); - test('can parse flutter web redirect link', () async { + testWidgets('can parse flutter web redirect link', (tester) async { final uri = Uri.parse( "http://localhost:55510/#access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=magiclink", ); @@ -42,7 +44,8 @@ void main() { expect(uriParams['refresh_token'], equals('bbb')); }); - test('can parse flutter web custom page redirect link', () async { + testWidgets('can parse flutter web custom page redirect link', + (tester) async { final uri = Uri.parse( "http://localhost:55510/#/webAuth%23access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=magiclink", ); From debb82f9dd59f043d2987444ab720b5096c57b94 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Thu, 16 Jun 2022 21:12:59 +0900 Subject: [PATCH 11/25] fix: automatically take care of starting and ending deeplink observer --- lib/src/supabase_auth.dart | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 9ef52d7f..888f375e 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -11,9 +11,7 @@ import 'package:url_launcher/url_launcher.dart'; /// SupabaseAuth class SupabaseAuth with WidgetsBindingObserver { - SupabaseAuth._() { - WidgetsBinding.instance?.addObserver(this); - } + SupabaseAuth._(); static final SupabaseAuth _instance = SupabaseAuth._(); bool _initialized = false; @@ -97,6 +95,8 @@ class SupabaseAuth with WidgetsBindingObserver { } } } + WidgetsBinding.instance?.addObserver(_instance); + _instance._startDeeplinkObserver(); return _instance; } @@ -105,7 +105,7 @@ class SupabaseAuth with WidgetsBindingObserver { void dispose() { _listenerController.close(); _authSubscription?.data?.unsubscribe(); - stopDeeplinkObserver(); + _stopDeeplinkObserver(); WidgetsBinding.instance?.removeObserver(this); } @@ -185,7 +185,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// **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 shouldHandleInitialDeeplink() { + bool get shouldHandleInitialDeeplink { if (_initialDeeplinkIsHandled) { return false; } else { @@ -204,7 +204,7 @@ class SupabaseAuth with WidgetsBindingObserver { } /// Enable deep link observer to handle deep links - void startDeeplinkObserver() { + void _startDeeplinkObserver() { Supabase.instance.log('***** SupabaseDeepLinkingMixin startAuthObserver'); _handleIncomingLinks(); _handleInitialUri(); @@ -213,7 +213,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// Stop deep link observer /// /// Automatically called on dispose(). - void stopDeeplinkObserver() { + void _stopDeeplinkObserver() { Supabase.instance.log('***** SupabaseDeepLinkingMixin stopAuthObserver'); if (_deeplinkSubscription != null) _deeplinkSubscription?.cancel(); } @@ -245,7 +245,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// /// We handle all exceptions, since it is called from initState. Future _handleInitialUri() async { - if (!SupabaseAuth.instance.shouldHandleInitialDeeplink()) return; + if (!shouldHandleInitialDeeplink) return; try { final uri = await getInitialUri(); @@ -261,7 +261,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// Callback when deeplink receiving succeeds Future _handleDeeplink(Uri uri) async { - if (!SupabaseAuth.instance.isAuthCallbackDeeplink(uri)) return false; + if (!_instance.isAuthCallbackDeeplink(uri)) return false; Supabase.instance.log('***** SupabaseAuthState handleDeeplink $uri'); From 6eaae4a23f68bf3fd74fbe5c9d20203e19771e24 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Fri, 17 Jun 2022 18:44:59 +0900 Subject: [PATCH 12/25] fix: failing tests due to uniLink call --- test/supabase_flutter_test.dart | 3 +-- test/widget_test.dart | 2 ++ test/widget_test_stubs.dart | 13 +++++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/supabase_flutter_test.dart b/test/supabase_flutter_test.dart index 0250d1ac..6a391e7e 100644 --- a/test/supabase_flutter_test.dart +++ b/test/supabase_flutter_test.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -10,7 +9,7 @@ void main() { const supabaseKey = ''; setUpAll(() async { - WidgetsFlutterBinding.ensureInitialized(); + mockUniLink(); // Initialize the Supabase singleton await Supabase.initialize( url: supabaseUrl, diff --git a/test/widget_test.dart b/test/widget_test.dart index 527d2268..7acedae8 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -10,6 +10,8 @@ void main() { const supabaseKey = ''; setUpAll(() async { + mockUniLink(); + // Initialize the Supabase singleton await Supabase.initialize( url: supabaseUrl, diff --git a/test/widget_test_stubs.dart b/test/widget_test_stubs.dart index dcb9c7d5..51eeb8c6 100644 --- a/test/widget_test_stubs.dart +++ b/test/widget_test_stubs.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; class MockWidget extends StatefulWidget { @@ -49,3 +51,14 @@ class MockLocalStorage extends LocalStorage { hasAccessToken: () async => true, ); } + +// Register the mock handler for uni_links +void mockUniLink() { + const channel = MethodChannel('uni_links/messages'); + TestWidgetsFlutterBinding.ensureInitialized(); + + TestDefaultBinaryMessengerBinding.instance?.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) { + return null; + }); +} From 05daff94902c8d32304708e4dd7d58da5e262917 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 15:13:36 +0900 Subject: [PATCH 13/25] breaking: remove unused parseUriParameters --- lib/src/supabase_auth.dart | 38 ++++++--------------------------- test/supabase_flutter_test.dart | 31 --------------------------- 2 files changed, 7 insertions(+), 62 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 5351cf03..a140614d 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -160,33 +160,10 @@ class SupabaseAuth with WidgetsBindingObserver { } } - /// Parse Uri parameters from redirect url/deeplink - Map parseUriParameters(Uri uri) { - Uri _uri = uri; - if (_uri.hasQuery) { - final decoded = _uri.toString().replaceAll('#', '&'); - _uri = Uri.parse(decoded); - } else { - final uriStr = _uri.toString(); - String decoded; - // %23 is the encoded of #hash - // support custom redirect to on flutter web - if (uriStr.contains('/#%23')) { - decoded = uriStr.replaceAll('/#%23', '/?'); - } else if (uriStr.contains('/#/')) { - decoded = uriStr.replaceAll('/#/', '/').replaceAll('%23', '?'); - } else { - decoded = uriStr.replaceAll('#', '?'); - } - _uri = Uri.parse(decoded); - } - return _uri.queryParameters; - } - /// **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 get shouldHandleInitialDeeplink { + bool get _shouldHandleInitialDeeplink { if (_initialDeeplinkIsHandled) { return false; } else { @@ -196,7 +173,7 @@ class SupabaseAuth with WidgetsBindingObserver { } /// if _authCallbackUrlHost not init, we treat all deeplink as auth callback - bool isAuthCallbackDeeplink(Uri uri) { + bool _isAuthCallbackDeeplink(Uri uri) { if (_authCallbackUrlHostname == null) { return true; } else { @@ -246,7 +223,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// /// We handle all exceptions, since it is called from initState. Future _handleInitialUri() async { - if (!shouldHandleInitialDeeplink) return; + if (!_shouldHandleInitialDeeplink) return; try { final uri = await getInitialUri(); @@ -261,24 +238,23 @@ class SupabaseAuth with WidgetsBindingObserver { } /// Callback when deeplink receiving succeeds - Future _handleDeeplink(Uri uri) async { - if (!_instance.isAuthCallbackDeeplink(uri)) return false; + Future _handleDeeplink(Uri uri) async { + if (!_instance._isAuthCallbackDeeplink(uri)) return; Supabase.instance.log('***** SupabaseAuthState handleDeeplink $uri'); // notify auth deeplink received Supabase.instance.log('onReceivedAuthDeeplink uri: $uri'); - return _recoverSessionFromUrl(uri); + await _recoverSessionFromUrl(uri); } - Future _recoverSessionFromUrl(Uri uri) async { + Future _recoverSessionFromUrl(Uri uri) async { // recover session from deeplink final response = await Supabase.instance.client.auth.getSessionFromUrl(uri); if (response.error != null) { Supabase.instance.log(response.error!.message); } - return true; } /// Callback when deeplink receiving throw error diff --git a/test/supabase_flutter_test.dart b/test/supabase_flutter_test.dart index 6a391e7e..39cc6c99 100644 --- a/test/supabase_flutter_test.dart +++ b/test/supabase_flutter_test.dart @@ -22,35 +22,4 @@ void main() { final client = Supabase.instance.client; expect(client, isNotNull); }); - - testWidgets('can parse deeplink', (tester) 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 = SupabaseAuth.instance.parseUriParameters(uri); - expect(uriParams.length, equals(5)); - expect(uriParams['access_token'], equals('aaa')); - expect(uriParams['refresh_token'], equals('bbb')); - }); - - testWidgets('can parse flutter web redirect link', (tester) async { - final uri = Uri.parse( - "http://localhost:55510/#access_token=aaa&expires_in=3600&refresh_token=bbb&token_type=bearer&type=magiclink", - ); - final uriParams = SupabaseAuth.instance.parseUriParameters(uri); - expect(uriParams.length, equals(5)); - expect(uriParams['access_token'], equals('aaa')); - expect(uriParams['refresh_token'], equals('bbb')); - }); - - testWidgets('can parse flutter web custom page redirect link', - (tester) 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 = SupabaseAuth.instance.parseUriParameters(uri); - expect(uriParams.length, equals(5)); - expect(uriParams['access_token'], equals('aaa')); - expect(uriParams['refresh_token'], equals('bbb')); - }); } From d07f55be4d3692a43b4295969dd01c9e0cd21582 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 15:37:01 +0900 Subject: [PATCH 14/25] fix: consolidate _initialDeeplinkIsHandled and shouldHandleInitialDeeplink --- lib/src/supabase_auth.dart | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index a140614d..077d62aa 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -27,6 +27,9 @@ class SupabaseAuth with WidgetsBindingObserver { /// {@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; String? _authCallbackUrlHostname; @@ -160,18 +163,6 @@ class SupabaseAuth with WidgetsBindingObserver { } } - /// **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 get _shouldHandleInitialDeeplink { - if (_initialDeeplinkIsHandled) { - return false; - } else { - _initialDeeplinkIsHandled = true; - return true; - } - } - /// if _authCallbackUrlHost not init, we treat all deeplink as auth callback bool _isAuthCallbackDeeplink(Uri uri) { if (_authCallbackUrlHostname == null) { @@ -223,7 +214,8 @@ class SupabaseAuth with WidgetsBindingObserver { /// /// We handle all exceptions, since it is called from initState. Future _handleInitialUri() async { - if (!_shouldHandleInitialDeeplink) return; + if (_initialDeeplinkIsHandled) return; + _initialDeeplinkIsHandled = true; try { final uri = await getInitialUri(); From 0d70ab3e706b193a080153657964eb2dc8a35dca Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 15:55:10 +0900 Subject: [PATCH 15/25] feat: added initialSession to determine if user is signed in or not upon app launch --- lib/src/supabase_auth.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 077d62aa..8a418d0a 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -27,6 +27,12 @@ class SupabaseAuth with WidgetsBindingObserver { /// {@macro supabase.localstorage.accessToken} Future get accessToken => _localStorage.accessToken(); + /// Returns when the initial session recovery is done. + /// Can be used to determine whether a user is signed in or not uplon + /// initial app launch. + Future get initialSession => _initialSessionCompleter.future; + final Completer _initialSessionCompleter = Completer(); + /// **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. @@ -97,11 +103,17 @@ class SupabaseAuth with WidgetsBindingObserver { if (response.error != null) { Supabase.instance.log(response.error!.message); } + if (!_instance._initialSessionCompleter.isCompleted) { + _instance._initialSessionCompleter.complete(response.data); + } } } WidgetsBinding.instance?.addObserver(_instance); _instance._startDeeplinkObserver(); + if (!_instance._initialSessionCompleter.isCompleted) { + _instance._initialSessionCompleter.complete(null); + } return _instance; } From 7fa05728abebe4858a65b9612e0431934f736f4b Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 15:58:17 +0900 Subject: [PATCH 16/25] fix: remove unnecessary if statement --- lib/src/supabase_auth.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 8a418d0a..8d6e996a 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -196,7 +196,7 @@ class SupabaseAuth with WidgetsBindingObserver { /// Automatically called on dispose(). void _stopDeeplinkObserver() { Supabase.instance.log('***** SupabaseDeepLinkingMixin stopAuthObserver'); - if (_deeplinkSubscription != null) _deeplinkSubscription?.cancel(); + _deeplinkSubscription?.cancel(); } /// Handle incoming links - the ones that the app will recieve from the OS From da06da7ac217130738dee5d8a21d00c2906ed34c Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 16:11:43 +0900 Subject: [PATCH 17/25] fix: make sure the initialSessionCompleter completes --- lib/src/supabase_auth.dart | 67 +++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 8d6e996a..5f58fa3c 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -79,42 +79,51 @@ class SupabaseAuth with WidgetsBindingObserver { LocalStorage localStorage = const HiveLocalStorage(), String? authCallbackUrlHostname, }) async { - _instance._initialized = true; - _instance._localStorage = localStorage; - _instance._authCallbackUrlHostname = authCallbackUrlHostname; - - _instance._authSubscription = - Supabase.instance.client.auth.onAuthStateChange((event, session) { - _instance._onAuthStateChange(event, session); - if (!_instance._listenerController.isClosed) { - _instance._listenerController.add(event); - } - }); + try { + _instance._initialized = true; + _instance._localStorage = localStorage; + _instance._authCallbackUrlHostname = authCallbackUrlHostname; + + _instance._authSubscription = + Supabase.instance.client.auth.onAuthStateChange((event, session) { + _instance._onAuthStateChange(event, session); + if (!_instance._listenerController.isClosed) { + _instance._listenerController.add(event); + } + }); - await _instance._localStorage.initialize(); + await _instance._localStorage.initialize(); - final hasPersistedSession = await _instance._localStorage.hasAccessToken(); - if (hasPersistedSession) { - final persistedSession = await _instance._localStorage.accessToken(); - if (persistedSession != null) { - final response = await Supabase.instance.client.auth - .recoverSession(persistedSession); + final hasPersistedSession = + await _instance._localStorage.hasAccessToken(); + if (hasPersistedSession) { + final persistedSession = await _instance._localStorage.accessToken(); + if (persistedSession != null) { + final response = await Supabase.instance.client.auth + .recoverSession(persistedSession); - if (response.error != null) { - Supabase.instance.log(response.error!.message); - } - if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.complete(response.data); + if (response.error != null) { + Supabase.instance.log(response.error!.message); + } + if (!_instance._initialSessionCompleter.isCompleted) { + _instance._initialSessionCompleter.complete(response.data); + } } } - } - WidgetsBinding.instance?.addObserver(_instance); - _instance._startDeeplinkObserver(); + WidgetsBinding.instance?.addObserver(_instance); + _instance._startDeeplinkObserver(); - if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.complete(null); + if (!_instance._initialSessionCompleter.isCompleted) { + _instance._initialSessionCompleter.complete(null); + } + return _instance; + } catch (_) { + // completer will complete with null if an error occurs + if (!_instance._initialSessionCompleter.isCompleted) { + _instance._initialSessionCompleter.complete(null); + } + rethrow; } - return _instance; } /// Dispose the instance to free up resources From 4e5b56fe01c5c9fab9c6a0e67e5d62c4af39d5d2 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 17:55:53 +0900 Subject: [PATCH 18/25] fix: change initialSession from Session to GoTrueSessionResponse --- lib/src/supabase_auth.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 5f58fa3c..2acef9d2 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -30,8 +30,10 @@ class SupabaseAuth with WidgetsBindingObserver { /// Returns when the initial session recovery is done. /// Can be used to determine whether a user is signed in or not uplon /// initial app launch. - Future get initialSession => _initialSessionCompleter.future; - final Completer _initialSessionCompleter = Completer(); + Future get initialSession => + _initialSessionCompleter.future; + final Completer _initialSessionCompleter = + Completer(); /// **ATTENTION**: `getInitialLink`/`getInitialUri` should be handled /// ONLY ONCE in your app's lifetime, since it is not meant to change @@ -106,7 +108,7 @@ class SupabaseAuth with WidgetsBindingObserver { Supabase.instance.log(response.error!.message); } if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.complete(response.data); + _instance._initialSessionCompleter.complete(response); } } } @@ -114,13 +116,17 @@ class SupabaseAuth with WidgetsBindingObserver { _instance._startDeeplinkObserver(); if (!_instance._initialSessionCompleter.isCompleted) { + // Complete with null if the user did not have persisted session _instance._initialSessionCompleter.complete(null); } return _instance; - } catch (_) { - // completer will complete with null if an error occurs + } catch (e) { if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.complete(null); + _instance._initialSessionCompleter.complete( + GotrueSessionResponse( + error: GotrueError(e.toString()), + ), + ); } rethrow; } From 0a818b531c663a809641f9d7db98257d824a81b9 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 18:04:02 +0900 Subject: [PATCH 19/25] fix: initialSession completing with an error on error --- lib/src/supabase_auth.dart | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 2acef9d2..bacc85c6 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -30,10 +30,8 @@ class SupabaseAuth with WidgetsBindingObserver { /// Returns when the initial session recovery is done. /// Can be used to determine whether a user is signed in or not uplon /// initial app launch. - Future get initialSession => - _initialSessionCompleter.future; - final Completer _initialSessionCompleter = - Completer(); + Future get initialSession => _initialSessionCompleter.future; + final Completer _initialSessionCompleter = Completer(); /// **ATTENTION**: `getInitialLink`/`getInitialUri` should be handled /// ONLY ONCE in your app's lifetime, since it is not meant to change @@ -103,12 +101,16 @@ class SupabaseAuth with WidgetsBindingObserver { if (persistedSession != null) { final response = await Supabase.instance.client.auth .recoverSession(persistedSession); + final error = response.error; - if (response.error != null) { + if (error != null) { Supabase.instance.log(response.error!.message); + if (!_instance._initialSessionCompleter.isCompleted) { + _instance._initialSessionCompleter.completeError(error); + } } if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.complete(response); + _instance._initialSessionCompleter.complete(response.data); } } } @@ -122,11 +124,7 @@ class SupabaseAuth with WidgetsBindingObserver { return _instance; } catch (e) { if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.complete( - GotrueSessionResponse( - error: GotrueError(e.toString()), - ), - ); + _instance._initialSessionCompleter.completeError(e); } rethrow; } From 689d35f1573f5a9e99c646129638c8d8dd0f1df9 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 22:09:40 +0900 Subject: [PATCH 20/25] fix: typo --- lib/src/supabase_auth.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index bacc85c6..d80114ee 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -28,7 +28,7 @@ class SupabaseAuth with WidgetsBindingObserver { Future get accessToken => _localStorage.accessToken(); /// Returns when the initial session recovery is done. - /// Can be used to determine whether a user is signed in or not uplon + /// Can be used to determine whether a user is signed in or not upon /// initial app launch. Future get initialSession => _initialSessionCompleter.future; final Completer _initialSessionCompleter = Completer(); From 87d2ac5ecffa4d0a60fe1db86f7b1a15e6715a94 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 23:08:25 +0900 Subject: [PATCH 21/25] fix: updated readme --- README.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index eacfcfbd..e3fa944c 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Supabase is an open source Firebase alternative. We are a service to: | Platform | Email Auth | Provider Auth | Database | Realtime | Storage | | -------- | :--------: | :-----------: | :------: | :------: | :-----: | +| Web | ✅ | ✅ | ✅ | ✅ | ✅ | | Android | ✅ | ✅ | ✅ | ✅ | ✅ | | iOS | ✅ | ✅ | ✅ | ✅ | ✅ | | macOS | ✅ | | ✅ | ✅ | ✅ | @@ -69,7 +70,9 @@ void main() async { ## Authentication -Using authentication can be done easily. +Using authentication can be done easily. Using this package automatically persists the auth state on local storage. +It also helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter... + ### Email authentication @@ -86,15 +89,6 @@ Future signIn(String email, String password) async { } ``` -### SupabaseAuthState - -It persists the auth state on local storage. -It also helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter... - -For more details, take a look at the example [here](https://github.com/phamhieu/supabase-flutter-demo/blob/main/lib/components/auth_state.dart) - -> When using with a nested authentication flow, remember to call `startAuthObserver()` and `stopAuthObserver()` before/after navigation to new screen to prevent multiple observers running at the same time. Take a look at the example [here](https://github.com/phamhieu/supabase-flutter-demo/blob/026c6e8cbb05a5b1b76a50ce82d936016844ba1b/lib/screens/signin_screen.dart#L165-L170) - ### signInWithProvider This method will automatically launch the auth url and open a browser for user to sign in with 3rd party login. From de90849a5ae24e773c48cf34141536cb8d6cfa83 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sat, 18 Jun 2022 23:20:39 +0900 Subject: [PATCH 22/25] fix: added explanation of initialSession on readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index e3fa944c..bac14a48 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,22 @@ Using authentication can be done easily. Using this package automatically pers It also helps you handle authentication with deeplink from 3rd party service like Google, Github, Twitter... +### Getting initial auth state + +You might want to redirect users to different screens upon app launch. +For this, you can await `initialSession` of `SupabaseAuth` to get the initial session of the user. The future will complete once session recovery is done and will contain either the session if user had one or null if user had no session. + +```dart +Future getInitialAuthState() async { + try { + final initialSession = await SupabaseAuth.instance.initialSession; + // Redirect users to different screens depending on the initial session + } catch(e) { + // Handle initial auth state fetch error here + } +} +``` + ### Email authentication ```dart From 8306877c6c651fc49bdcc017b1a8a26a9bdb030a Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Wed, 20 Jul 2022 09:27:35 +0900 Subject: [PATCH 23/25] fix: handling more errors on initialDeeplink --- lib/src/supabase_auth.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index d80114ee..9d96904f 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -247,10 +247,13 @@ class SupabaseAuth with WidgetsBindingObserver { if (uri != null) { _handleDeeplink(uri); } - } on PlatformException { + } on PlatformException catch (err) { + _onErrorReceivingDeeplink(err.message ?? err.toString()); // Platform messages may fail but we ignore the exception } on FormatException catch (err) { _onErrorReceivingDeeplink(err.message); + } catch (err) { + _onErrorReceivingDeeplink(err.toString()); } } From 2978142474bfcb89ca670e66c972c8402251bf59 Mon Sep 17 00:00:00 2001 From: Tyler <18113850+dshukertjr@users.noreply.github.com> Date: Sun, 24 Jul 2022 11:59:28 +0900 Subject: [PATCH 24/25] Update lib/src/supabase_auth.dart Co-authored-by: Bruno D'Luka <45696119+bdlukaa@users.noreply.github.com> --- lib/src/supabase_auth.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 9d96904f..80d10b86 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -122,9 +122,9 @@ class SupabaseAuth with WidgetsBindingObserver { _instance._initialSessionCompleter.complete(null); } return _instance; - } catch (e) { + } catch (error, stacktrace) { if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.completeError(e); + _instance._initialSessionCompleter.completeError(error, stracktrace); } rethrow; } From 26caaa4bc291b0ca942c1a23ee6c95f9018398d7 Mon Sep 17 00:00:00 2001 From: dshukertjr <18113850+dshukertjr@users.noreply.github.com> Date: Sun, 24 Jul 2022 12:07:33 +0900 Subject: [PATCH 25/25] fix: removed analysis error --- lib/src/supabase_auth.dart | 2 +- test/supabase_flutter_test.dart | 30 ------------------------------ 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/lib/src/supabase_auth.dart b/lib/src/supabase_auth.dart index 80d10b86..1b8162a0 100644 --- a/lib/src/supabase_auth.dart +++ b/lib/src/supabase_auth.dart @@ -124,7 +124,7 @@ class SupabaseAuth with WidgetsBindingObserver { return _instance; } catch (error, stacktrace) { if (!_instance._initialSessionCompleter.isCompleted) { - _instance._initialSessionCompleter.completeError(error, stracktrace); + _instance._initialSessionCompleter.completeError(error, stacktrace); } rethrow; } diff --git a/test/supabase_flutter_test.dart b/test/supabase_flutter_test.dart index fd7a478c..6170a40f 100644 --- a/test/supabase_flutter_test.dart +++ b/test/supabase_flutter_test.dart @@ -33,34 +33,4 @@ void main() { .client; expect(client, isNot(newClient)); }); - - 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 = SupabaseAuth.instance.parseUriParameters(uri); - expect(uriParams.length, equals(5)); - expect(uriParams['access_token'], equals('aaa')); - expect(uriParams['refresh_token'], equals('bbb')); - }); - - 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 = SupabaseAuth.instance.parseUriParameters(uri); - expect(uriParams.length, equals(5)); - expect(uriParams['access_token'], equals('aaa')); - expect(uriParams['refresh_token'], equals('bbb')); - }); - - 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 = SupabaseAuth.instance.parseUriParameters(uri); - expect(uriParams.length, equals(5)); - expect(uriParams['access_token'], equals('aaa')); - expect(uriParams['refresh_token'], equals('bbb')); - }); }