Closed bruiken closed 1 week ago
After a bit further investigation in how we can do this, it seems this issue could be resolved by using AppLinks.allUriLinkStream
instead of AppLinks.uriLinkStream
. The former includes the initial link. This is different from the current implementation because this initial link will not be awaited and can thus be caught by our listeners.
A different solution for us would be if the deeplink integration had a bit more customisability. Allowing e.g. an enum option such as enum DeeplinkHandlingOption { all, none, nonInitial }
. I don't really like this because this would just be duplicate implementation.
So, to summarise, you are saying that the AuthChangeEvent.passwordRecovery
event never fires when the app is launched from the deep link?
The initial link for password recovery events is handled like other events and should fire without issues. I have tested it with app_links v6.1.1 and supabase_flutter supabase_flutter v2.5.3, and can confirm I get a AuthChangeEvent.passwordRecovery
event right after the AuthChangeEvent.initialSession
event upon app launch.
The initial link for password recovery events is handled like other events and should fire without issues. I have tested it with app_links v6.1.1 and supabase_flutter supabase_flutter v2.5.3, and can confirm I get a
AuthChangeEvent.passwordRecovery
event right after theAuthChangeEvent.initialSession
event upon app launch.
@dshukertjr thanks for the reply! I indeed see the event getting fired. It is just that I see no way to actually receive that AuthChangeEvent.passwordRecovery
in a listener, if it is caused by the initial deeplink handling. It seems impossible to create the subscription to the steam in time to get the event surfaced to our app logic.
Hmm, you should be able to catch the passwordRecovery
event within your app. I was able to listen to it with the following code:
Future<void> main() async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
);
runApp(const MyApp());
}
final supabase = Supabase.instance.client;
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Playground',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String? _userId;
late final RealtimeChannel channel;
@override
void initState() {
super.initState();
supabase.auth.onAuthStateChange.listen((event) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(event.event.name),
));
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(_userId ?? 'no user'),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () async {
await supabase.auth.resetPasswordForEmail(
'email@example.com',
redirectTo: redirectTo,
);
},
child: const Text('Press'),
),
],
),
),
);
}
}
Hmm, you should be able to catch the
passwordRecovery
event within your app. I was able to listen to it with the following code:
@dshukertjr interestingly, I am running the same versions and do not see the events with the same code. I want to once again verify, I don't just mean starting the app from a paused/minimised state. I am only having issues with the app starting up fully (from a cold state) from a deeplink. This is tricky to test to legitimate flow because I don't see an easy way to set the initial deeplink when booting the app (I just set it manually in SupabaseAuth
).
If the app is running in the background, I see the passwordRecovery
event as expected. If the app is fully shut down I only get initialSession
.
I'm putting the listener immediately after the supabase initialisation as follows:
Future<void> main() async {
await Supabase.initialize(
url: dotenv.get('SUPABASE_URL'),
anonKey: dotenv.get('SUPABASE_ANON_KEY'),
authOptions: const FlutterAuthClientOptions(
authFlowType: AuthFlowType.pkce,
),
realtimeClientOptions: const RealtimeClientOptions(
logLevel: RealtimeLogLevel.debug,
),
);
supabase.auth.onAuthStateChange.listen(
(event) {
log(event.event.name);
},
onError: (e) {
log(e.toString());
},
);
runApp(...);
}
With this, I test handling an initial deeplink as follows:
Reset password in app
Click link in browser on non-simulator
Copy the redirect uri to the app
Paste it as the result of SupabaseAuth::_handleInitialUri()
as something like this:
Uri? uri;
try {
// before app_links 6.0.0
uri = await (_appLinks as dynamic).getInitialAppLink();
} on NoSuchMethodError catch (_) {
// Needed to keep compatible with 5.0.0 and 6.0.0
// https://pub.dev/packages/app_links/changelog
// after app_links 6.0.0
uri = await (_appLinks as dynamic).getInitialLink();
}
// temp hack to test password forget link that is not already used
uri = Uri.parse('REDIRECT LINK FROM (3) HERE');
if (uri != null) {
await _handleDeeplink(uri);
}
stop and start the app
Then the output of my logging from the earlier snippet is as follows:
Xcode build done. 20.9s
Connecting to VM Service at ws://127.0.0.1:55185/LpjTqO-S8r8=/ws
flutter: ***** Supabase init completed Instance of 'Supabase'
flutter: ***** SupabaseDeepLinkingMixin startAuthObserver
flutter: ***** SupabaseAuthState handleDeeplink deeplink.test:?code=54cc8f0e-5292-46fa-8ae8-61cce09b4225
flutter: onReceivedAuthDeeplink uri: deeplink.test:?code=54cc8f0e-5292-46fa-8ae8-61cce09b4225
flutter: **** onAuthStateChange: AuthChangeEvent.passwordRecovery
flutter: {"access_token": ...
[log] initialSession
flutter: **** onAuthStateChange: AuthChangeEvent.initialSession
flutter: {"access_token":"...
I see the supabase event being fired, but it's not coming up in the listener. This makes sense as the listener subscribes after the whole processing of the deeplink is done (this seems to be the problem to me). Somehow it needs to be possible to get access to that stream during the initial deeplink handling.
If I were to keep the app paused in the background and then do the password reset redirection logic I see the following:
flutter: ***** SupabaseAuthState handleDeeplink deeplink.test:?code=4800a17f-b5ae-46ae-97cb-1153d44106d9
flutter: onReceivedAuthDeeplink uri: deeplink.test:?code=4800a17f-b5ae-46ae-97cb-1153d44106d9
flutter: **** onAuthStateChange: AuthChangeEvent.passwordRecovery
flutter: {"access_token"...
[log] passwordRecovery
Which makes sense to me.
All of this is just for the legimate flow (when the password recovery link is valid). If the link is NOT valid, all of the errors are catched by the SupabaseAuth
logic, so we cannot even catch those errors in the auth stream subscription. This is an equally big problem to me, as we can then show nothing to the user.
With this, I test handling an initial deeplink as follows:
I believe the reason why you are not seeing the password recovery event is because you are not testing it with the actual flow that the user goes through.
Just run through the password recovery flow like usual and the password recovery event will show up whether the app is terminated or not.
resetPasswordForEmail()
from the app
Is your feature request related to a problem? Please describe. When implementing the password recovery flow, we noticed that the initial deeplink handling does not yield for much customisability. The function
_handleInitialUri()
is called as await from the initialisation function of theSupabase
client. If that initial deeplink is concerning a password recovery, then it seems to be impossible to catch that event in thesupabase.auth.onAuthStateChange
listener. The same holds for the error cases, which are never even brought to that stream (this has been pointed out by #664 before too). Yet in that case it wasn't about a password recovery but a magic link. For magic links this all is not that big of an issue, at the end of the day the user is authenticated and there is a session. For password recovery it to us seems impossible to direct the user to the "reset your password" screen when the app was fully closed.Describe the solution you'd like We'd like to see a solution where it is made possible to detect password recovery and possible errors from initial deeplink handling. A solution in this most definitely extends to any event, not just password recovery.
Describe alternatives you've considered
Additional context Besides this, it's also quite difficult to catch errors specific to deeplink handling, our code now contains a number of checks based on the
AuthException.message
field. It would seem like a good idea to create more different error subclasses ofAuthException
so it is possible to do checks that are not reliant on error message changes.If directions are given for how to possibly fix this, I'd be okay with submitting a PR. One possible avenue could be to allow giving the
supabase.auth.onAuthStateChange
in theSupabase.initialize
function. I don't think that would be nice behaviour as listeners in different places get access to different events. Maybe a more specific event handler for the initial deeplink handling? Or then some way to get access to past events.