Hi @vlad.murarasu,
Thanks a lot for sharing all this info — really appreciate it.
After spending quite some time trying to get things working, I ended up with a workaround that seems to do the trick. I’m not entirely sure it’s the correct way to handle authentication with SFSafariViewController
, but I wanted to share it here in case it helps others facing the same issue.
Let’s break it down step by step:
1. Update app.json
(or ideally rename to app.config.ts
):
Make sure your config includes the following:
export default {
expo: {
scheme: "com.myapp", // you can skip the `auth0` suffix here
plugins: [
[
'react-native-auth0',
{
domain: process.env.EXPO_PUBLIC_AUTH0_DOMAIN,
},
],
],
},
};
2. Handle native deep links in +native-intent.tsx
:
Create a file at the root of your app
directory. This will intercept all deep links — including the Auth0 callback URL — and prevent Expo Router from throwing an “Unmatched route” error.
import { ERoutes } from '@/types/routes.enum';
import { AUTH0_SCHEME_SUFFIX } from '@/constants/auth'; // e.g. 'auth0://'
export function redirectSystemPath({ path }: { path: string }) {
if (path.includes(AUTH0_SCHEME_SUFFIX)) {
return ERoutes.SIGN_IN;
}
return path;
}
3. Implement login logic:
Here’s how to use authorize
with SFSafariViewController
:
import { Redirect } from 'expo-router';
import { useAuth0 } from 'react-native-auth0';
import { Button } from '@/components/Button';
import { ERoutes } from '@/types/routes.enum';
import { AUTH0_CUSTOM_SCHEME } from '@/constants/auth'; // e.g. 'com.myapp.auth0'
export const LoginButton = () => {
const { user, authorize, isLoading } = useAuth0();
const onLoginPress = async () => {
await authorize(
{
audience: process.env.EXPO_PUBLIC_AUTH0_AUDIENCE,
additionalParameters: { prompt: 'login' }, // optional but useful (explained below)
},
{
useSFSafariViewController: true, // important!
ephemeralSession: true, // optional, see docs
customScheme: AUTH0_CUSTOM_SCHEME,
},
);
};
if (!isLoading && user) {
return <Redirect href={ERoutes.EVENTS} />;
}
return <Button fullWidth disabled={isLoading} title="Continue" onPress={onLoginPress} />;
};
Why use prompt: 'login'
?
SFSafariViewController may not clear session data fully after logout. Without prompt: 'login'
, trying to log in again immediately after logout might not present the login screen. Adding prompt: 'login'
ensures the login prompt is always shown.
Why does “Unmatched route” happen?
Not 100% sure, but based on what I’ve seen in the source code of react-native-auth0
, here’s what’s happening:
- In
node_modules/react-native-auth0/webauth/agent.ts
, thelogin()
method uses:
Linking.addEventListener('url', handler);
- This allows the Auth0 SDK to intercept the deep link (e.g.,
com.myapp.auth0://...
) and exchange the code for an access token. - Even though we intercept this route in
+native-intent.tsx
, it still reaches the SDK — and that’s what we want.
So technically, Expo Router gets a chance to intercept the link (to avoid unmatched routes), but the SDK still receives it and proceeds with the token exchange.
In conclusion, there’s no bug in Auth0 here — but clearer documentation around useSFSafariViewController
and recommended setup for deep link handling in React Native would be super helpful.
Until then, I hope this helps others struggling with similar issues. And if you’re ever unsure, digging into the source code and asking AI to explain it line by line is honestly a great approach.