Project Structure Overview and Login Flow
In part one, we gained an overview of the directory structure and the packages we will install for the project. Now, we’ll discuss the login flow, which will be implemented as follows:
Login Flow
- Opening the App:
- If the user is not logged in:
Display the Login Screen.
- The user taps “Login with Apple” to initiate the login process on their iPhone.
2. Searching for the User based on the account:
- If the user does not exist:
Create a new user and proceed.
- If the user exists and is valid:
Retrieve the valid user information and proceed.
- If the user exists but is invalid:
Display an error.
3. Returning to the App:
- Store the login information in Context.
If the user is valid, navigate to the Home Screen.
If the user is invalid, navigate to the Error Screen.
4. When the User is already logged in:
Open the app and check for the latest user data.
If the user does not exist:
Create a new user and proceed.
- If the user exists and is valid:
Retrieve the valid user information and proceed.
- If the user exists but is invalid:
Display an error
App Structure:
The app can have multiple Context Providers serving different purposes, such as theme settings or authentication information.
-
An index file for the providers needs to be created to manage them.
export function AppContextProvider({ children }: { children: React.ReactNode }): JSX.Element {
return (
{children}
)
}
AppConfigProvider is an example of other contexts, which we won’t discuss here.
Now, let’s create the AuthProvider.tsx file with the following content:
// import files
...
type LoginAction = {
type: 'LOGIN',
user: User
}
type LogoutAction = {
type: 'LOGOUT'
}
type ChangeAction = {
type: 'CHANGE',
user: User
}
type OnUserChangedAction = {
type: 'OnUserChangedAction',
user: User
}
type AuthDispatchAction = LoginAction | LogoutAction | ChangeAction | OnUserChangedAction
export type AuthContextState = User | null
export const AuthContext = createContext<AuthContextState>(null);
export const AuthDispatchContext = createContext<Dispatch<AuthDispatchAction> | null>(null)
export function authReducers(preAuthState: AuthContextState, action: AuthDispatchAction): AuthContextState {
switch (action.type) {
case 'LOGIN':
case 'CHANGE':
return action.user
case 'OnUserChangedAction':
return action.user;
case 'LOGOUT':
return null
default:
throw Error('Unknown action: ' + JSON.stringify(action));
}
}
export function AuthProvider({ children }: PropsWithChildren<any>) {
const [initializing, setInitializing] = useState(true);
const [authState, authDispatch] = useReducer<Reducer<AuthContextState, AuthDispatchAction>>(authReducer, defaultUserState,)
const handleAuthStateChanged = useCallback(async (user: FirebaseAuthUser | null) => {
if (!user) {
authDispatch({ type: 'LOGOUT' })
}
else {
const authUser = await getAuthenticatedUser(user.uid);
if (authUser)
authDispatch({ type: 'CHANGE', user: authUser, });
}
if (initializing) {
setInitializing(false);
}
}, [initializing])
useEffect(() => {
// ...
}, [])
return (
<AuthContext.Provider value={authState}>
<AuthDispatchContext.Provider value={authDispatch}>
{!initializing && children}
</AuthDispatchContext.Provider>
</AuthContext.Provider>
)
}
It’s important to note that in the useEffect, we will call the onAuthStateChanged service to update the state. When the app receives the user information, it will call the reducers to update the context:
Add the following to the useEffect:
import { onAuthStateChanged } from "../services/Firebase/users/userManagement";
//...
useEffect(() => {
return onAuthStateChanged(handleAuthStateChanged);
}, [handleAuthStateChanged])
In the services/Firebase/users/ directory, we will create three files:
userManagement.ts: Contains interfaces to call functions from outside, helping to define types so that other hooks/services can understand them. This is where we declare onAuthStateChanged as mentioned earlier.
userManagement.native.ts and userManagement.web.ts: These files implement onAuthStateChanged for each platform. Since web and app use different libraries, they cannot share the same source code.
Let’s continue:
First, declare the interface for userManagement in the userManagement.ts file. When the user information is updated, Firebase will call this function, returning a callback with the user information for further processing:
interface UserManagement {
onAuthStateChanged: <T>(callback: (user: T) => void) => () => void;
// getUserByDevice: () => Promise<User | undefined>;
// getUserById: (userId: string) => Promise<User | undefined>;
// updateUser: (userId: string, userData: any) => Promise<void>;
// subscribeOnUserChanged: (userId: string, callback: (user: User | undefined) => void) => () => void;
// signOut: () => Promise<void>;
// getCurrentUser: () => Promise<User | undefined>;
// signInWithCredential: <T, G>(credential: T) => Promise<G>;
// signInAnonymously: <T>() => Promise<T>;
}
Next, we need to define where we call the function. In the same userManagement.ts file, add the following:
import { Platform } from "react-native";
const userManagement = Platform.select({
web: () => require('./userManagement.web'),
default: () => require('./userManagement.native'),
});
export const {
onAuthStateChanged,
// getUserByDevice,
// getUserById,
// updateUser,
// subscribeOnUserChanged,
// signOut,
// getCurrentUser,
// signInWithCredential,
// signInAnonymously
} = userManagement as UserManagement;
The userManagement.native.ts version uses the package @react-native-firebase/auth, while the web version calls the firebase/auth library to invoke the onAuthStateChanged function.
This function is responsible for:
Listening for changes in the user’s authentication state (logging in and out).
This method returns an unsubscribe function to stop listening to events.
-
Always ensure you unsubscribe from the listener when it is no longer needed to prevent updates to components that are no longer in use.
userManagement.native.ts
import auth, { FirebaseAuthTypes } from "@react-native-firebase/auth";
export type FirebaseAuthUser = FirebaseAuthTypes.User;
export function onAuthStateChanged(callback: (user: FirebaseAuthUser | null) => void): () => void {
return auth().onAuthStateChanged(callback);
}
The native version requires the configuration of the GoogleService-Info.plist file for the Apple app and/or the google-services.json file for the Android app to function properly.
userManagement.web.ts
import { auth, db } from '../config';
import { User as FirebaseUser } from "firebase/auth";
export type FirebaseAuthUser = FirebaseUser;
export function onAuthStateChanged(
callback: (user: FirebaseAuthUser | null) => void
): () => void {
return auth.onAuthStateChanged(callback);
}
Unlike the native version, the web version requires a configuration to call Firebase through the initializeAuth function. Therefore, we need to add a config file to the Firebase service as follows:
Folder Structure Updated:
...
services/
Firebase/
config/
index.ts
index.native.ts
index.web.ts
users/
index.ts
userManagement.ts
userManagement.native.ts
userManagement.web.ts
Similar to the userManagement.ts file, the index.ts file for Firebase config only needs to export the functions from both web and native versions.
config/index.ts
import { Platform } from "react-native";
const app = Platform.select({
web: require('./index.web'),
default: require('./index.native'),
});
export const { auth, firebaseApp } = app;
The files in the config are primarily intended for Firebase on the web; therefore, the content in index.native.ts does not need to return any data.
config/index.native.ts
let firebaseApp
let db
let auth
let analytics
export { analytics, auth, db, firebaseApp };
The index.web.ts file contains the main logic that allows us to access Firebase services on the web. The content of the file is as follows:
config/index.web.ts
import { initializeApp } from 'firebase/app';
import { initializeAuth } from 'firebase/auth';
// Firebase JS SDK work with expo web
// for more information: https://docs.expo.dev/guides/using-firebase/#using-firebase-js-sdk
// Initialize Firebase
const firebaseConfig = {
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
// databaseURL: process.env.EXPO_PUBLIC_FIREBASE_DATABASE_URL,
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,
};
let firebaseApp: ReturnType<typeof initializeApp>;
let auth: ReturnType<typeof initializeAuth>;
try {
firebaseApp = initializeApp(firebaseConfig);
auth = initializeAuth(firebaseApp);
auth.useDeviceLanguage();
} catch (error) {
console.error("Error initializing Firestore:", (error as any).message);
console.error("Error Stack:", (error as any).stack);
console.error("Error Details:", error);
}
export { auth, firebaseApp };
// To apply the default browser preference instead of explicitly setting it.
// For more information on how to access Firebase in your project,
// see the Firebase documentation: https://firebase.google.com/docs/web/setup#access-firebase
firebase/app and firebase/auth are separate releases of the Firebase JS SDK. TODO: Update from the Firebase blog to provide further explanation for this section.
firebase/app and firebase/auth are package dependencies of @react-native-firebase/app. There is no need to install them separately, and they can be called directly.
If you attempt to manually install these packages, you will encounter the following error:
RNFNFirestoreDocumentModule.getDocument got 4 arguments, expected 2 ...
This error occurs due to a version conflict between the Firebase JS SDK and the Firebase JS SDK, which is a dependency of @react-native-firebase.
About this snippet:
const firebaseConfig = {
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
// databaseURL: process.env.EXPO_PUBLIC_FIREBASE_DATABASE_URL,
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,
};
Returning to the Firebase Config in the Google Console, create a new app for the web. Firebase will send you the JS SDK configuration.
Add the configuration to the .env file of your project. In this example, I am using Expo, so the environment variables have the prefix EXPO_PUBLIC_. You can use a library like react-native-dotenv to manage these environment variables.
Thank you for joining me on this comprehensive article. If you found this tutorial helpful, don’t forget to give this post *50 claps👏 **and **follow *🚀 if you enjoyed this post and want to see more. Your enthusiasm and support fuel my passion for sharing knowledge in the tech community.
You can find more such articles on my profile -> https://medium.com/@khuepm
https://github.com/khuepm
Stay tuned for more in-depth tutorials and insights on Javascript and React Native development.
And that’s it! Place a console.log() statement in the callback handleAuthStateChanged within the AuthProvider.tsx file to check the returned data. In the next part (if anyone purpose for this), I will return to the Login section. Happy coding! 🦄
Here the Part 01 of this story: https://dev.to/khuepm/react-native-web-firebase-part-01-why-we-use-react-native-to-develop-both-native-and-web-fdg
Top comments (0)