Skip to content

Commit

Permalink
Merge pull request supabase#124 from dshukertjr/breaking/rework
Browse files Browse the repository at this point in the history
BREAKING: Remove SupabaseAuthRequiredState as well as overriding methods in SupabaseAuthState
  • Loading branch information
dshukertjr committed Jul 24, 2022
2 parents 3093f70 + 26caaa4 commit 21babeb
Show file tree
Hide file tree
Showing 11 changed files with 246 additions and 458 deletions.
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ||||
Expand Down Expand Up @@ -278,7 +279,25 @@ class _MyWidgetState extends State<MyWidget> {

## 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...


### 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<void> 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

Expand All @@ -295,20 +314,6 @@ Future<void> signIn(String email, String password) async {
}
```

### SupabaseAuthState

It 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.
Expand Down
20 changes: 9 additions & 11 deletions lib/src/supabase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class Supabase {
/// This must be called only once. If called more than once, an
/// [AssertionError] is thrown
static Future<Supabase> initialize({
String? url,
String? anonKey,
required String url,
required String anonKey,
String? authCallbackUrlHostname,
bool? debug,
LocalStorage? localStorage,
Expand All @@ -48,16 +48,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;
}
Expand Down
240 changes: 185 additions & 55 deletions lib/src/supabase_auth.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
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';

// ignore_for_file: invalid_null_aware_operator

/// SupabaseAuth
class SupabaseAuth {
class SupabaseAuth with WidgetsBindingObserver {
SupabaseAuth._();

static final SupabaseAuth _instance = SupabaseAuth._();
Expand All @@ -21,12 +27,23 @@ class SupabaseAuth {
/// {@macro supabase.localstorage.accessToken}
Future<String?> 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 upon
/// initial app launch.
Future<Session?> get initialSession => _initialSessionCompleter.future;
final Completer<Session?> _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.
bool _initialDeeplinkIsHandled = false;
String? _authCallbackUrlHostname;

GotrueSubscription? _authSubscription;
final _listenerController = StreamController<AuthChangeEvent>.broadcast();

StreamSubscription<Uri?>? _deeplinkSubscription;

/// Listen to auth change events.
///
/// ```dart
Expand Down Expand Up @@ -62,40 +79,103 @@ class SupabaseAuth {
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);
final error = response.error;

if (response.error != null) {
Supabase.instance.log(response.error!.message);
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.data);
}
}
}
}
WidgetsBinding.instance?.addObserver(_instance);
_instance._startDeeplinkObserver();

return _instance;
if (!_instance._initialSessionCompleter.isCompleted) {
// Complete with null if the user did not have persisted session
_instance._initialSessionCompleter.complete(null);
}
return _instance;
} catch (error, stacktrace) {
if (!_instance._initialSessionCompleter.isCompleted) {
_instance._initialSessionCompleter.completeError(error, stacktrace);
}
rethrow;
}
}

/// Dispose the instance to free up resources
void dispose() {
_listenerController.close();
_authSubscription?.data?.unsubscribe();
_stopDeeplinkObserver();
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<bool> _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) {
Expand All @@ -108,49 +188,99 @@ class SupabaseAuth {
}
}

/// Parse Uri parameters from redirect url/deeplink
Map<String, String> parseUriParameters(Uri uri) {
Uri _uri = uri;
if (_uri.hasQuery) {
final decoded = _uri.toString().replaceAll('#', '&');
_uri = Uri.parse(decoded);
/// if _authCallbackUrlHost not init, we treat all deeplink as auth callback
bool _isAuthCallbackDeeplink(Uri uri) {
if (_authCallbackUrlHostname == null) {
return true;
} 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 _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');
_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());
},
);
}
return _uri.queryParameters;
}

/// 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.
bool shouldHandleInitialDeeplink() {
if (_initialDeeplinkIsHandled) {
return false;
} else {
_initialDeeplinkIsHandled = true;
return true;
///
/// We handle all exceptions, since it is called from initState.
Future<void> _handleInitialUri() async {
if (_initialDeeplinkIsHandled) return;
_initialDeeplinkIsHandled = true;

try {
final uri = await getInitialUri();
if (uri != null) {
_handleDeeplink(uri);
}
} 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());
}
}

/// if _authCallbackUrlHost not init, we treat all deeplink as auth callback
bool isAuthCallbackDeeplink(Uri uri) {
if (_authCallbackUrlHostname == null) {
return true;
} else {
return _authCallbackUrlHostname == uri.host;
/// Callback when deeplink receiving succeeds
Future<void> _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');

await _recoverSessionFromUrl(uri);
}

Future<void> _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);
}
}

/// Callback when deeplink receiving throw error
void _onErrorReceivingDeeplink(String message) {
Supabase.instance.log('onErrorReceivingDeppLink message: $message');
}
}

extension GoTrueClientSignInProvider on GoTrueClient {
Expand Down
Loading

0 comments on commit 21babeb

Please sign in to comment.