diff --git a/src/api/common/firebase.ts b/src/api/common/firebase.ts
index 197ea058..46963120 100644
--- a/src/api/common/firebase.ts
+++ b/src/api/common/firebase.ts
@@ -1,5 +1,10 @@
import { Env } from '@env';
import { initializeApp } from 'firebase/app';
+import {
+ connectAuthEmulator,
+ getReactNativePersistence,
+ initializeAuth,
+} from 'firebase/auth';
import {
connectFirestoreEmulator,
initializeFirestore,
@@ -7,6 +12,7 @@ import {
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,
@@ -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}`);
}
diff --git a/src/app/(tabs)/settings.tsx b/src/app/(tabs)/settings.tsx
index c2a4fd80..5a8a61c2 100644
--- a/src/app/(tabs)/settings.tsx
+++ b/src/app/(tabs)/settings.tsx
@@ -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 (
@@ -83,9 +93,12 @@ export default function Settings() {
-
-
-
+
diff --git a/src/app/auth/index.tsx b/src/app/auth/index.tsx
index bfb6923a..b4b4099d 100644
--- a/src/app/auth/index.tsx
+++ b/src/app/auth/index.tsx
@@ -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({
@@ -21,10 +27,11 @@ type EmailFormType = z.infer;
export default function Auth() {
const router = useRouter();
+ const [isLoading, setIsLoading] = useState(false);
const { handleSubmit, control } = useForm({
resolver: zodResolver(emailSchema),
defaultValues: {
- email: 'test@gmail.com',
+ email: 'blaze@mail.com',
},
});
@@ -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 => {
+ 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);
}
};
@@ -71,6 +89,7 @@ export default function Auth() {
/>
diff --git a/src/app/auth/login.tsx b/src/app/auth/login.tsx
index dd66c227..f695625f 100644
--- a/src/app/auth/login.tsx
+++ b/src/app/auth/login.tsx
@@ -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';
@@ -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({
+ const { handleSubmit, control, setError } = useForm({
resolver: zodResolver(passwordSchema),
defaultValues: {
email,
- password: '123456',
+ password: '',
},
});
-
- const onSubmit: SubmitHandler = (data) => {
- console.log(data);
- signIn({ access: 'access-token', refresh: 'refresh-token' });
- router.push('/');
+ const onSubmit: SubmitHandler = 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 (
Forgot Password?
-
+
diff --git a/src/components/settings/item.tsx b/src/components/settings/item.tsx
index fcaccf4d..2c84f427 100644
--- a/src/components/settings/item.tsx
+++ b/src/components/settings/item.tsx
@@ -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 (
{icon && {icon}}
- {text}
+ {loading ? (
+
+ ) : (
+ {text}
+ )}
+
- {value}
- {isPressable && {/* */}}
+ {!loading && (
+ {value}
+ )}
+ {isPressable && !loading && (
+ {/* */}
+ )}
);
diff --git a/src/core/auth/index.tsx b/src/core/auth/index.tsx
index 0e0d426d..2a873dfd 100644
--- a/src/core/auth/index.tsx
+++ b/src/core/auth/index.tsx
@@ -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;
+ signOut: () => Promise;
hydrate: () => void;
}
-const _useAuth = create((set, get) => ({
+const _useAuth = create((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();
diff --git a/src/core/storage.tsx b/src/core/storage.tsx
index f7018093..ab93319b 100644
--- a/src/core/storage.tsx
+++ b/src/core/storage.tsx
@@ -2,6 +2,21 @@ import { MMKV } from 'react-native-mmkv';
export const storage = new MMKV();
+export const reactNativeAsyncStorage = {
+ setItem: (key: string, value: string): Promise => {
+ storage.set(key, value);
+ return Promise.resolve();
+ },
+ getItem: (key: string): Promise => {
+ const value = storage.getString(key);
+ return Promise.resolve(value ?? null);
+ },
+ removeItem: (key: string): Promise => {
+ storage.delete(key);
+ return Promise.resolve();
+ },
+};
+
export function getItem(key: string): T {
const value = storage.getString(key);
return value ? JSON.parse(value) || null : null;
diff --git a/src/ui/button.tsx b/src/ui/button.tsx
index 8c1efaf7..8d872f9e 100644
--- a/src/ui/button.tsx
+++ b/src/ui/button.tsx
@@ -48,6 +48,12 @@ const button = tv({
label: 'text-black',
indicator: 'text-black',
},
+ item: {
+ container:
+ 'rounded-md border border-neutral-200 dark:border-neutral-700 dark:bg-neutral-800',
+ label: 'text-black dark:text-neutral-100',
+ indicator: 'text-black dark:text-neutral-100',
+ },
},
size: {
default: {
diff --git a/tsconfig.json b/tsconfig.json
index ff341b3a..36adad50 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -9,6 +9,9 @@
],
"@env": [
"./src/core/env.js"
+ ],
+ "@firebase/auth": [
+ "./node_modules/@firebase/auth/dist/index.rn.d.ts"
]
},
"esModuleInterop": true,