Universal Auth with Firebase and Expo Router

Auth is very important for any app these days.

Folder setup

install @react-native-firebase/app , @react-native-firebase/auth and firebase

src/firebase/firebase.tsx

import { Platform } from 'react-native';
import { getAuth, Auth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'firebase/auth';
import  auth from '@react-native-firebase/auth';
// Define the WebAuth interface
interface WebAuth {
  signInWithEmailAndPassword: (email: string, password: string) => Promise<any>;
  createUserWithEmailAndPassword: (email: string, password: string) => Promise<any>;
}

let firebaseApp;
let webAuth: WebAuth | undefined;
let getWebAuth: Auth;

if (Platform.OS === 'web') {
  // Import Firebase modules for web
  const { initializeApp } = require('firebase/app');

  const firebaseConfig = {
    apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
    authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
    projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
    storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
    appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
    measurementId: process.env.EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID
  };

  // Initialize Firebase for web
  firebaseApp = initializeApp(firebaseConfig);
  getWebAuth = getAuth(firebaseApp);
  webAuth = {
    signInWithEmailAndPassword: (email: string, password: string) => 
    signInWithEmailAndPassword(getWebAuth, email, password),
    createUserWithEmailAndPassword: (email: string, password: string) => 
    createUserWithEmailAndPassword(getWebAuth, email, password)
  };
} else {
  // Initialize Firebase for native platforms
  const firebase = require('@react-native-firebase/app').default;

  if (!firebase.apps.length) {
    firebaseApp = firebase.initializeApp({});
  } else {
    firebaseApp = firebase.app();
  }

  webAuth = undefined;
}

export { firebaseApp, auth, webAuth, getWebAuth };

app/index.tsx

import { useState } from 'react';
import {
    Text,
    View,
    StyleSheet,
    KeyboardAvoidingView,
    TextInput,
    Button,
    ActivityIndicator,
    Platform,
    Pressable,
} from 'react-native';
import { FirebaseError } from 'firebase/app';
import { auth, webAuth } from '../src/firebase/firebase';
import { Ionicons } from '@expo/vector-icons';

export default function Index() {
    const [email, setEmail] = useState('');
    const [password, setPassword] = useState('');
    const [showPassword, setShowPassword] = useState(false);
    const [loading, setLoading] = useState(false);

    const signUp = async () => {
        setLoading(true);
        try {
            if (Platform.OS === 'web' && webAuth) {
                await webAuth.createUserWithEmailAndPassword(email, password);
            } else {
                await auth().createUserWithEmailAndPassword(email, password);
            }
            alert('Check your emails!');
        } catch (e: any) {
            const err = e as FirebaseError;
            alert('Registration failed: ' + err.message);
        } finally {
            setLoading(false);
        }
    };

    const signIn = async () => {
        setLoading(true);
        try {
            if (Platform.OS === 'web' && webAuth) {
                await webAuth.signInWithEmailAndPassword(email, password);
            } else {
                await auth().signInWithEmailAndPassword(email, password);
            }
        } catch (e: any) {
            const err = e as FirebaseError;
            alert('Sign in failed: ' + err.message);
        } finally {
            setLoading(false);
        }
    };

    return (
        <View style={styles.container}>
            <KeyboardAvoidingView behavior="padding">
                <TextInput
                    style={styles.input}
                    value={email}
                    onChangeText={setEmail}
                    autoCapitalize="none"
                    keyboardType="email-address"
                    placeholder="Email"
                />
                <View style={styles.passwordContainer}>
                    <TextInput
                        style={[styles.input, styles.passwordInput]}
                        value={password}
                        onChangeText={setPassword}
                        secureTextEntry={!showPassword}
                        placeholder="Password"
                    />
                    <Pressable 
                        onPress={() => setShowPassword(!showPassword)}
                        style={styles.eyeIcon}
                    >
                        <Ionicons 
                            name={showPassword ? "eye-off" : "eye"} 
                            size={24} 
                            color="#666"
                        />
                    </Pressable>
                </View>
                {loading ? (
                    <ActivityIndicator size={'small'} style={{ margin: 28 }} />
                ) : (
                    <>
                        <Button onPress={signIn} title="Login" />
                        <Button onPress={signUp} title="Create account" />
                    </>
                )}
            </KeyboardAvoidingView>
        </View>
    );
}

const styles = StyleSheet.create({
    container: {
        marginHorizontal: 20,
        flex: 1,
        justifyContent: 'center'
    },
    input: {
        marginVertical: 4,
        height: 50,
        borderWidth: 1,
        borderRadius: 4,
        padding: 10,
        backgroundColor: '#fff'
    },
    passwordContainer: {
        position: 'relative',
        flexDirection: 'row',
        alignItems: 'center',
    },
    passwordInput: {
        flex: 1,
        paddingRight: 45,
    },
    eyeIcon: {
        position: 'absolute',
        right: 10,
        height: 50,
        justifyContent: 'center',
    },
});

app/_layout.tsx

 import { Stack, useRouter, useSegments } from 'expo-router';
import { useEffect, useState } from 'react';
import { View, ActivityIndicator, Platform } from 'react-native';
import { auth , getWebAuth} from '../src/firebase/firebase';

type User = {
  uid: string;
  email: string | null;
} | null;

export default function RootLayout() {
  const [initializing, setInitializing] = useState(true);
  const [user, setUser] = useState<User>(null);
  const router = useRouter();
  const segments = useSegments();

  const onAuthStateChanged = (user: User) => {
    console.log('onAuthStateChanged', user);
    setUser(user);
    if (initializing) setInitializing(false);
  };

  useEffect(() => {
    let unsubscribe;
    if (Platform.OS === 'web') {
      unsubscribe = getWebAuth.onAuthStateChanged(onAuthStateChanged);
    } else {
      // For native platforms, auth is already initialized
      unsubscribe = auth().onAuthStateChanged(onAuthStateChanged);
    }
    return unsubscribe;
  }, []);

  useEffect(() => {
    if (initializing) return;

    const inAuthGroup = segments[0] === '(auth)';

    if (user && !inAuthGroup) {
      router.replace('/(auth)/home');
    } else if (!user && inAuthGroup) {
      router.replace('/');
    }
  }, [user, segments, initializing]);

  if (initializing) {
    return (
      <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  return (
    <Stack
      screenOptions={{
        headerShown: false,
      }}
    >
      <Stack.Screen name="index" options={{ title: 'Login' }} />
      <Stack.Screen name="(auth)" options={{ headerShown: false }} />
    </Stack>
  );
}

app/(auth)/_layout.tsx

import { Stack } from 'expo-router';
const Layout = () => {
    return <Stack />;
};
export default Layout;

app/(auth)/home.tsx

import { View, Text, Button, Platform } from 'react-native';
import { auth,getWebAuth } from '../../src/firebase/firebase';

const Page = () => {
    const user = Platform.OS === 'web' ? getWebAuth.currentUser : auth().currentUser;

    const handleSignOut = async () => {
        try {
            if (Platform.OS === 'web') {
                await getWebAuth.signOut();
            } else {
                await auth().signOut();
            }
        } catch (error) {
            console.error('Sign out error:', error);
        }
    };

    return (
        <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
            <Text style={{ fontSize: 18, marginBottom: 20 }}>
                Welcome back {user?.email}
            </Text>
            <Button title="Sign out" onPress={handleSignOut} />
        </View>
    );
};

export default Page;

app.json

    "plugins": [
      "expo-router",
      [
        "expo-splash-screen",
        {
          "image": "./assets/images/splash-icon.png",
          "imageWidth": 200,
          "resizeMode": "contain",
          "backgroundColor": "#ffffff"
        }
      ],
      "@react-native-firebase/app",
      "@react-native-firebase/auth",
      "@react-native-firebase/crashlytics",
      [
        "expo-build-properties",
        {
          "ios": {
            "useFrameworks": "static"
          }
        }
      ]