App Store Rejection Due to Web Login – Issue with Phone Passwordless + react-native-auth0 and SFSafariViewController

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.


:warning: 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, the login() 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.