DEV Community

Jules Sarah
Jules Sarah

Posted on

The auth-screen UX playbook for Expo apps

Auth is the most scrutinised surface in a mobile app. People decide whether it looks legitimate almost immediately, and whether they're willing to sign up shortly after. Everything downstream is gated behind that.

Mobile OAuth in particular is where otherwise good apps leak users quietly, because the failure isn't an error. It's a browser tab the user has to close themselves.

The legitimacy scan

On first open, people scan for a small set of signals:

  • A logo that isn't pixelated
  • Provider buttons using the official brand assets, not lookalikes
  • Copy that doesn't read as machine-translated
  • No layout shift as the screen mounts

All four have to land. Any one being off loses users who will never tell you why.

Use the platform packages rather than rolling your own buttons. expo-apple-authentication renders Apple's official button, and @react-native-google-signin/google-signin gives you Google's. Both handle the brand requirements you'd otherwise have to read the guidelines for.

Keep the OAuth callback in-app

This is the difference between OAuth working and OAuth converting.

The naive flow opens a browser, the user signs in, the browser shows a success page, and then the user has to work out that they should return to the app themselves. Some don't.

const { data } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: Linking.createURL('/'),
    skipBrowserRedirect: true,
  },
});

const result = await WebBrowser.openAuthSessionAsync(
  data.url,
  Linking.createURL('/'),
);

if (result.type === 'success') {
  // parse access_token and refresh_token from the returned URL fragment
  await supabase.auth.setSession({ access_token, refresh_token });
}
Enter fullscreen mode Exit fullscreen mode

openAuthSessionAsync uses ASWebAuthenticationSession on iOS and Custom Tabs on Android. Both close themselves when they see the return scheme, so the user lands back in the app with a session already established and no visible handoff.

The token parsing is the fiddly part and the snippet leaves it as a comment deliberately: the tokens arrive in the URL fragment rather than the query string, so a naive URLSearchParams on the whole URL returns nothing and you'll spend twenty minutes wondering why.

Password reset is a funnel nobody revisits

It gets designed once and never looked at again, which means when it breaks, it breaks silently. Nobody files a bug about a password reset that didn't arrive. They just leave.

The flow that works:

  1. Email input screen
  2. resetPasswordForEmail(email, { redirectTo: 'myapp://reset' })
  3. The emailed link deep-links into the app with a session already established
  4. New password via updateUser({ password }), then a success screen with a sign-in action

The breakage to know about: the emailed link won't open the app unless the scheme is registered under Additional Redirect URLs in your Supabase auth settings. Add it once. This accounts for most "reset doesn't work" reports, and it produces no error anywhere in your code.

Error copy that doesn't alarm people

Never surface raw auth errors. AuthApiError: Invalid login credentials reads as something being broken. "Email or password doesn't match" reads as a typo.

Map every one:

const AUTH_ERRORS: Record<string, string> = {
  'Invalid login credentials': "Email or password doesn't match. Try again.",
  'User already registered': 'You already have an account. Try signing in.',
  'Password should be at least 6 characters': 'Password needs to be 6+ characters.',
};
Enter fullscreen mode Exit fullscreen mode

Plus a network fallback, because "can't reach the server, check your connection" is a completely different message from "your details are wrong" and users act on them differently.

And put errors inline, under the field that failed. A red banner at the top of the screen is a web pattern that mobile inherited, and on a phone it means the user is looking at the wrong part of the screen to find out what went wrong.

The general shape

Every item here is the same class of problem: nothing throws, nothing appears in your error tracking, and the user simply doesn't complete. A browser tab left open. A reset email that goes nowhere. An error message that reads as a system failure rather than a typo.

Auth either works or it doesn't, technically. The gap between working and converting is entirely in these details.

The Applighter Expo templates ship auth wired this way, though everything above is a few hours of work in any project and worth doing before your first round of user testing.


What's the auth bug that took you longest to find? Mine was the redirect URL, and it took two days because nothing anywhere reported an error.

Top comments (0)