Skip to content

Commit

Permalink
feat: firebase auth implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
AnkushSarkar10 committed Feb 7, 2025
1 parent 55cce5f commit ac559e6
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 55 deletions.
20 changes: 17 additions & 3 deletions src/api/common/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Env } from '@env';
import { initializeApp } from 'firebase/app';
import {
connectAuthEmulator,
getReactNativePersistence,
initializeAuth,
} from 'firebase/auth';
import {
connectFirestoreEmulator,
initializeFirestore,
} from 'firebase/firestore';
import { connectFunctionsEmulator, getFunctions } from 'firebase/functions';
import { Platform } from 'react-native';

import { reactNativeAsyncStorage } from '../../core/storage';
const firebaseConfig = {
apiKey: Env.EXPO_PUBLIC_FIREBASE_API_KEY,
authDomain: Env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
Expand All @@ -21,12 +27,20 @@ export const app = initializeApp(firebaseConfig);
export const db = initializeFirestore(app, {
experimentalForceLongPolling: true,
});
const functions = getFunctions(app);
export const functions = getFunctions(app);
export const auth = initializeAuth(app, {
persistence: getReactNativePersistence(reactNativeAsyncStorage),
});

// Connect to emulator
// 10.0.2.2 is a special IP address to connect to the 'localhost' of the host computer from an Android emulator
const emulatorHost = Platform.OS === 'ios' ? '127.0.0.1' : '10.0.2.2';
const FIRESTORE_PORT = 8080;
const FUNCTIONS_PORT = 5001;
const AUTH_PORT = 9099;

if (__DEV__) {
connectFirestoreEmulator(db, emulatorHost, 8080);
connectFunctionsEmulator(functions, emulatorHost, 5001);
connectFirestoreEmulator(db, emulatorHost, FIRESTORE_PORT);
connectFunctionsEmulator(functions, emulatorHost, FUNCTIONS_PORT);
connectAuthEmulator(auth, `http://${emulatorHost}:${AUTH_PORT}`);
}
25 changes: 19 additions & 6 deletions src/app/(tabs)/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
/* eslint-disable react/react-in-jsx-scope */
import { Env } from '@env';
import { useState } from 'react';
import { Linking } from 'react-native';

import { Item } from '@/components/settings/item';
import { ItemsContainer } from '@/components/settings/items-container';
import { ThemeItem } from '@/components/settings/theme-item';
import { useAuth } from '@/core';
import { Header, ScreenContainer, ScrollView, View } from '@/ui';
import { Button, Header, ScreenContainer, ScrollView, View } from '@/ui';

export default function Settings() {
const signOut = useAuth.use.signOut();
// const { colorScheme } = useColorScheme();
// const iconColor = colorScheme === 'dark' ? colors.neutral[400] : colors.neutral[500];
const [isLoading, setIsLoading] = useState(false);
const handleLogout = async () => {
setIsLoading(true);
try {
await signOut();
} catch (error) {
console.error('Logout error:', error);
} finally {
setIsLoading(false);
}
};
return (
<ScreenContainer>
<Header title="Settings" />
Expand Down Expand Up @@ -83,9 +93,12 @@ export default function Settings() {
</ItemsContainer>

<View className="my-8">
<ItemsContainer>
<Item text="Logout" onPress={signOut} />
</ItemsContainer>
<Button
label="Logout"
onPress={handleLogout}
loading={isLoading}
variant="item"
/>
</View>
</View>
</ScrollView>
Expand Down
37 changes: 28 additions & 9 deletions src/app/auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'expo-router';
import { httpsCallable } from 'firebase/functions';
import { useColorScheme } from 'nativewind';
import React from 'react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
import * as z from 'zod';

import { functions } from '@/api/common/firebase';
import Logo from '@/components/logo';
import { Button, ControlledInput, ScreenContainer, Text, View } from '@/ui';

const userExistsSchema = z.object({
exists: z.boolean(),
});

const emailSchema = z.object({
email: z
.string({
Expand All @@ -21,10 +27,11 @@ type EmailFormType = z.infer<typeof emailSchema>;

export default function Auth() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const { handleSubmit, control } = useForm<EmailFormType>({
resolver: zodResolver(emailSchema),
defaultValues: {
email: 'test@gmail.com',
email: 'blaze@mail.com',
},
});

Expand All @@ -33,19 +40,30 @@ export default function Auth() {
const appleIconBlack = require('/assets/apple-icon-black.png');

// eslint-disable-next-line unused-imports/no-unused-vars
const checkUserExists = async (email: string) => {
return true; // Change as needed
const checkUserExists = async (email: string): Promise<boolean> => {
try {
const checkUser = httpsCallable(functions, 'checkUserExistsInAuth');
const response = await checkUser({ email });
const data = userExistsSchema.parse(response.data);
return data.exists;
} catch (error) {
console.error('Error checking user existence:', error);
return false;
}
};

const onSubmit = async (data: EmailFormType) => {
const userExists = await checkUserExists(data.email);
if (userExists) {
setIsLoading(true);
try {
const userExists = await checkUserExists(data.email);
router.push({
pathname: '/auth/login',
pathname: userExists ? '/auth/login' : '/auth/sign-up',
params: { email: data.email },
});
} else {
router.push({ pathname: '/auth/sign-up', params: { email: data.email } });
} catch (error) {
console.error(error);
} finally {
setIsLoading(false);
}
};

Expand All @@ -71,6 +89,7 @@ export default function Auth() {
/>
<Button
label="Continue With Email"
loading={isLoading}
onPress={handleSubmit(onSubmit)}
/>
<View className="flex-row items-center gap-3">
Expand Down
30 changes: 20 additions & 10 deletions src/app/auth/login.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useLocalSearchParams, useRouter } from 'expo-router';
import React from 'react';
import { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
Expand Down Expand Up @@ -31,21 +31,27 @@ export default function Login() {
const { email } = useLocalSearchParams<{ email: string }>();
const router = useRouter();
const signIn = useAuth.use.signIn();
const [isLoading, setIsLoading] = useState(false);

const { handleSubmit, control } = useForm<PasswordFormType>({
const { handleSubmit, control, setError } = useForm<PasswordFormType>({
resolver: zodResolver(passwordSchema),
defaultValues: {
email,
password: '123456',
password: '',
},
});

const onSubmit: SubmitHandler<PasswordFormType> = (data) => {
console.log(data);
signIn({ access: 'access-token', refresh: 'refresh-token' });
router.push('/');
const onSubmit: SubmitHandler<PasswordFormType> = async (data) => {
setIsLoading(true);
try {
await signIn(data.email, data.password);
router.push('/');
} catch (error: any) {
console.error('Login Error:', error);
setError('password', { message: 'Invalid email or password' });
} finally {
setIsLoading(false);
}
};

return (
<ScreenContainer>
<KeyboardAvoidingView
Expand Down Expand Up @@ -73,7 +79,11 @@ export default function Login() {
>
Forgot Password?
</Text>
<Button label="Login" onPress={handleSubmit(onSubmit)} />
<Button
label="Login"
loading={isLoading}
onPress={handleSubmit(onSubmit)}
/>
</View>
</KeyboardAvoidingView>
</ScreenContainer>
Expand Down
27 changes: 23 additions & 4 deletions src/components/settings/item.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@
import * as React from 'react';
import { ActivityIndicator } from 'react-native';

import { Pressable, Text, View } from '@/ui';

type ItemProps = {
text: string;
loading?: boolean;
value?: string;
onPress?: () => void;
icon?: React.ReactNode;
};

export const Item = ({ text, value, icon, onPress }: ItemProps) => {
export const Item = ({
text,
loading = false,
value,
icon,
onPress,
}: ItemProps) => {
const isPressable = onPress !== undefined;

return (
<Pressable
disabled={loading} // Disable button while loading
onPress={onPress}
pointerEvents={isPressable ? 'auto' : 'none'}
className="flex-1 flex-row items-center justify-between px-4 py-2"
>
<View className="flex-row items-center">
{icon && <View className="pr-2">{icon}</View>}
<Text>{text}</Text>
{loading ? (
<ActivityIndicator size="small" color="gray" />
) : (
<Text>{text}</Text>
)}
</View>

<View className="flex-row items-center">
<Text className="text-neutral-600 dark:text-white">{value}</Text>
{isPressable && <View className="pl-2">{/* <ArrowRight /> */}</View>}
{!loading && (
<Text className="text-neutral-600 dark:text-white">{value}</Text>
)}
{isPressable && !loading && (
<View className="pl-2">{/* <ArrowRight /> */}</View>
)}
</View>
</Pressable>
);
Expand Down
68 changes: 45 additions & 23 deletions src/core/auth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,67 @@
import {
onAuthStateChanged,
signInWithEmailAndPassword,
signOut as firebaseSignOut,
type User,
} from 'firebase/auth';
import { create } from 'zustand';

import { auth } from '../../api/common/firebase'; // Ensure this imports your Firebase instance
import { createSelectors } from '../utils';
import type { TokenType } from './utils';
import { getToken, removeToken, setToken } from './utils';

interface AuthState {
token: TokenType | null;
user: User | null;
status: 'idle' | 'signOut' | 'signIn';
signIn: (data: TokenType) => void;
signOut: () => void;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
hydrate: () => void;
}

const _useAuth = create<AuthState>((set, get) => ({
const _useAuth = create<AuthState>((set) => ({
user: null,
status: 'idle',
token: null,
signIn: (token) => {
setToken(token);
set({ status: 'signIn', token });

signIn: async (email, password) => {
try {
const userCredential = await signInWithEmailAndPassword(
auth,
email,
password,
);
set({ user: userCredential.user, status: 'signIn' });
} catch (error) {
console.error('SignIn Error:', error);
throw error;
}
},
signOut: () => {
removeToken();
set({ status: 'signOut', token: null });

signOut: async () => {
try {
await firebaseSignOut(auth);
set({ user: null, status: 'signOut' });
} catch (error) {
console.error('SignOut Error:', error);
}
},

hydrate: () => {
try {
const userToken = getToken();
if (userToken !== null) {
get().signIn(userToken);
set({ status: 'idle' });

// Subscribe to Firebase auth state changes
onAuthStateChanged(auth, (user) => {
if (user) {
set({ user, status: 'signIn' });
} else {
get().signOut();
set({ user: null, status: 'signOut' });
}
} catch (e) {
// catch error here
// Maybe sign_out user!
}
});
},
}));

export const useAuth = createSelectors(_useAuth);

// Functions for easier usage in components
export const signOut = () => _useAuth.getState().signOut();
export const signIn = (token: TokenType) => _useAuth.getState().signIn(token);
export const signIn = (email: string, password: string) =>
_useAuth.getState().signIn(email, password);
export const hydrateAuth = () => _useAuth.getState().hydrate();
15 changes: 15 additions & 0 deletions src/core/storage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import { MMKV } from 'react-native-mmkv';

export const storage = new MMKV();

export const reactNativeAsyncStorage = {
setItem: (key: string, value: string): Promise<void> => {
storage.set(key, value);
return Promise.resolve();
},
getItem: (key: string): Promise<string | null> => {
const value = storage.getString(key);
return Promise.resolve(value ?? null);
},
removeItem: (key: string): Promise<void> => {
storage.delete(key);
return Promise.resolve();
},
};

export function getItem<T>(key: string): T {
const value = storage.getString(key);
return value ? JSON.parse(value) || null : null;
Expand Down
Loading

0 comments on commit ac559e6

Please sign in to comment.