clean
This commit is contained in:
parent
6984c1be3d
commit
345641a615
|
|
@ -1,7 +1,107 @@
|
|||
import { Text,View } from "react-native";
|
||||
import {Button} from "@/components/common/Button";
|
||||
import { Redirect, router } from 'expo-router';
|
||||
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { Button } from '@/components/common/Button';
|
||||
import { Palette } from '@/constants/palette';
|
||||
import { Radius, Spacing } from '@/constants/styles';
|
||||
import { useAuth } from '@/providers/AuthProvider';
|
||||
import { logout } from '@/services/auth';
|
||||
|
||||
export default function Home() {
|
||||
return <View>
|
||||
<Button title="Button"/>
|
||||
</View>;
|
||||
const { initializing, user } = useAuth();
|
||||
|
||||
if (initializing) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator color={Palette.black} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Redirect href="/login" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.eyebrow}>Session active</Text>
|
||||
<Text style={styles.title}>Bienvenue</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{user.displayName || user.email || user.uid}
|
||||
</Text>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.label}>UID</Text>
|
||||
<Text style={styles.value}>{user.uid}</Text>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<Text style={styles.value}>{user.email || 'Aucun email'}</Text>
|
||||
</View>
|
||||
|
||||
<Button onPress={() => void logout()} title="Se deconnecter" />
|
||||
<Button
|
||||
onPress={() => router.push('/register')}
|
||||
style={styles.secondaryButton}
|
||||
textStyle={styles.secondaryButtonText}
|
||||
title="Creer un autre compte"
|
||||
variant="secondary"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centered: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: Palette.cream,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: Spacing.lg,
|
||||
gap: 20,
|
||||
backgroundColor: Palette.cream,
|
||||
},
|
||||
eyebrow: {
|
||||
color: Palette.gray,
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1.1,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
title: {
|
||||
color: Palette.black,
|
||||
fontSize: 34,
|
||||
fontWeight: '800',
|
||||
},
|
||||
subtitle: {
|
||||
color: Palette.gray,
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
},
|
||||
card: {
|
||||
gap: Spacing.sm,
|
||||
padding: 18,
|
||||
borderRadius: Radius.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.cream,
|
||||
backgroundColor: Palette.white,
|
||||
},
|
||||
label: {
|
||||
color: Palette.gray,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
value: {
|
||||
color: Palette.black,
|
||||
fontSize: 15,
|
||||
},
|
||||
secondaryButton: {
|
||||
borderColor: Palette.black,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
color: Palette.black,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
import { Stack } from "expo-router";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { AuthProvider } from "@/providers/AuthProvider";
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<Stack>
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="register" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</AuthProvider>
|
||||
</SafeAreaProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { Redirect } from 'expo-router';
|
||||
|
||||
import { AuthForm } from '@/components/auth/AuthForm';
|
||||
import { useAuth } from '@/providers/AuthProvider';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const { initializing, user } = useAuth();
|
||||
|
||||
if (!initializing && user) {
|
||||
return <Redirect href="/(tabs)" />;
|
||||
}
|
||||
|
||||
return <AuthForm mode="login" />;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { Redirect } from 'expo-router';
|
||||
|
||||
import { AuthForm } from '@/components/auth/AuthForm';
|
||||
import { useAuth } from '@/providers/AuthProvider';
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const { initializing, user } = useAuth();
|
||||
|
||||
if (!initializing && user) {
|
||||
return <Redirect href="/(tabs)" />;
|
||||
}
|
||||
|
||||
return <AuthForm mode="register" />;
|
||||
}
|
||||
7
bun.lock
7
bun.lock
|
|
@ -6,6 +6,7 @@
|
|||
"name": "expo-starter",
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
|
|
@ -509,6 +510,8 @@
|
|||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@react-native-async-storage/async-storage": ["@react-native-async-storage/async-storage@2.2.0", "", { "dependencies": { "merge-options": "^3.0.4" }, "peerDependencies": { "react-native": "^0.0.0-0 || >=0.65 <1.0" } }, "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw=="],
|
||||
|
||||
"@react-native/assets-registry": ["@react-native/assets-registry@0.81.5", "", {}, "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w=="],
|
||||
|
||||
"@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.81.5", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@react-native/codegen": "0.81.5" } }, "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ=="],
|
||||
|
|
@ -1151,6 +1154,8 @@
|
|||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
|
|
@ -1283,6 +1288,8 @@
|
|||
|
||||
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
||||
|
||||
"merge-options": ["merge-options@3.0.4", "", { "dependencies": { "is-plain-obj": "^2.1.0" } }, "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"metro": ["metro@0.83.3", "", { "dependencies": { "@babel/code-frame": "^7.24.7", "@babel/core": "^7.25.2", "@babel/generator": "^7.25.0", "@babel/parser": "^7.25.3", "@babel/template": "^7.25.0", "@babel/traverse": "^7.25.3", "@babel/types": "^7.25.2", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.32.0", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.3", "metro-cache": "0.83.3", "metro-cache-key": "0.83.3", "metro-config": "0.83.3", "metro-core": "0.83.3", "metro-file-map": "0.83.3", "metro-resolver": "0.83.3", "metro-runtime": "0.83.3", "metro-source-map": "0.83.3", "metro-symbolicate": "0.83.3", "metro-transform-plugins": "0.83.3", "metro-transform-worker": "0.83.3", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q=="],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,237 @@
|
|||
import { Link } from 'expo-router';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
|
||||
import { Button } from '@/components/common/Button';
|
||||
import { Palette } from '@/constants/palette';
|
||||
import { Radius, Spacing } from '@/constants/styles';
|
||||
import { mapFirebaseAuthError } from '@/lib/firebase-auth-errors';
|
||||
import { loginWithEmail, registerWithEmail } from '@/services/auth';
|
||||
|
||||
type AuthMode = 'login' | 'register';
|
||||
|
||||
type AuthFormProps = {
|
||||
mode: AuthMode;
|
||||
};
|
||||
|
||||
const copyByMode = {
|
||||
login: {
|
||||
title: 'Connexion',
|
||||
submitLabel: 'Se connecter',
|
||||
switchLabel: "Pas encore de compte ? S'inscrire",
|
||||
switchHref: '/register' as const,
|
||||
},
|
||||
register: {
|
||||
title: 'Inscription',
|
||||
submitLabel: "S'inscrire",
|
||||
switchLabel: 'Déjà un compte ? Se connecter',
|
||||
switchHref: '/login' as const,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function AuthForm({ mode }: AuthFormProps) {
|
||||
const copy = copyByMode[mode];
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const isFormValid = useMemo(() => {
|
||||
if (!email.trim() || !password.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mode === 'register' && !displayName.trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [displayName, email, mode, password]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!isFormValid || isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
if (mode === 'login') {
|
||||
await loginWithEmail(email, password);
|
||||
} else {
|
||||
await registerWithEmail({
|
||||
email,
|
||||
password,
|
||||
displayName,
|
||||
});
|
||||
}
|
||||
} catch (submitError) {
|
||||
setError(mapFirebaseAuthError(submitError));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
style={styles.keyboardView}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.hero}>
|
||||
<Text style={styles.title}>{copy.title}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
{mode === 'register' ? (
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Nom</Text>
|
||||
<TextInput
|
||||
autoCapitalize="words"
|
||||
onChangeText={setDisplayName}
|
||||
placeholder="Pierre"
|
||||
placeholderTextColor={Palette.black}
|
||||
style={styles.input}
|
||||
value={displayName}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Email</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
onChangeText={setEmail}
|
||||
placeholder="pierre@email.com"
|
||||
placeholderTextColor={Palette.black}
|
||||
style={styles.input}
|
||||
value={email}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Mot de passe</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
autoComplete={mode === 'login' ? 'password' : 'new-password'}
|
||||
onChangeText={setPassword}
|
||||
placeholder="••••••••"
|
||||
placeholderTextColor={Palette.black}
|
||||
secureTextEntry
|
||||
style={styles.input}
|
||||
value={password}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{error ? <Text style={styles.error}>{error}</Text> : null}
|
||||
|
||||
<Button
|
||||
disabled={!isFormValid || isSubmitting}
|
||||
onPress={handleSubmit}
|
||||
style={styles.submitButton}
|
||||
title={isSubmitting ? 'Chargement...' : copy.submitLabel}
|
||||
/>
|
||||
|
||||
{isSubmitting ? (
|
||||
<ActivityIndicator color={Palette.black} style={styles.loader} />
|
||||
) : null}
|
||||
|
||||
<Link href={copy.switchHref} style={styles.switchLink}>
|
||||
{copy.switchLabel}
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
backgroundColor: Palette.cream,
|
||||
},
|
||||
keyboardView: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: Spacing.lg,
|
||||
gap: Spacing.lg,
|
||||
backgroundColor: Palette.cream,
|
||||
},
|
||||
hero: {
|
||||
gap: Spacing.sm,
|
||||
},
|
||||
|
||||
title: {
|
||||
color: Palette.black,
|
||||
fontSize: 36,
|
||||
fontWeight: '800',
|
||||
},
|
||||
subtitle: {
|
||||
color: Palette.gray,
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
},
|
||||
|
||||
card: {
|
||||
backgroundColor: Palette.white,
|
||||
borderColor: Palette.cream,
|
||||
borderRadius: Radius.xl,
|
||||
borderWidth: 1,
|
||||
padding: 20,
|
||||
gap: Spacing.md,
|
||||
},
|
||||
field: {
|
||||
gap: Spacing.sm,
|
||||
},
|
||||
label: {
|
||||
color: Palette.black,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.gray,
|
||||
borderRadius: Radius.lg,
|
||||
backgroundColor: Palette.white,
|
||||
color: Palette.black,
|
||||
fontSize: 16,
|
||||
paddingHorizontal: Spacing.md,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
error: {
|
||||
color: Palette.danger,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
submitButton: {
|
||||
minHeight: 52,
|
||||
},
|
||||
loader: {
|
||||
marginTop: -4,
|
||||
},
|
||||
switchLink: {
|
||||
color: Palette.black,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TextStyle,
|
||||
} from "react-native";
|
||||
import {Palette} from "@/constants/palette";
|
||||
import { Radius, Spacing } from "@/constants/styles";
|
||||
|
||||
type ButtonVariant = "primary" | "secondary";
|
||||
|
||||
|
|
@ -24,7 +25,7 @@ const variantStyles = {
|
|||
primary: {
|
||||
container: {
|
||||
backgroundColor: Palette.black,
|
||||
borderColor: "transparent",
|
||||
borderColor: Palette.transparent,
|
||||
},
|
||||
text: {
|
||||
color: Palette.white,
|
||||
|
|
@ -32,7 +33,7 @@ const variantStyles = {
|
|||
},
|
||||
secondary: {
|
||||
container: {
|
||||
backgroundColor: "transparent",
|
||||
backgroundColor: Palette.transparent,
|
||||
borderColor: Palette.black,
|
||||
},
|
||||
text: {
|
||||
|
|
@ -80,8 +81,8 @@ export default Button;
|
|||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 12,
|
||||
paddingHorizontal: Spacing.md,
|
||||
borderRadius: Radius.md,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderWidth: 1,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
export const Palette = {
|
||||
white: '#FFF',
|
||||
black: '#000'
|
||||
black: '#000',
|
||||
transparent: 'transparent',
|
||||
cream: '#f5efe6',
|
||||
gray: '#5a5148',
|
||||
danger: '#9f2d2d',
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export const Spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
xxl: 40,
|
||||
} as const;
|
||||
|
||||
export const Radius = {
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 16,
|
||||
xl: 24,
|
||||
pill: 999,
|
||||
} as const;
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
const firebaseAuthErrorMessages: Record<string, string> = {
|
||||
'auth/email-already-in-use': 'Cet email est deja utilise.',
|
||||
'auth/invalid-credential': 'Email ou mot de passe incorrect.',
|
||||
'auth/invalid-email': "L'adresse email est invalide.",
|
||||
'auth/weak-password': 'Le mot de passe doit contenir au moins 6 caracteres.',
|
||||
};
|
||||
|
||||
const defaultFirebaseAuthErrorMessage =
|
||||
"Une erreur est survenue. Verifie ta configuration Firebase.";
|
||||
|
||||
export function mapFirebaseAuthError(error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
for (const [code, mappedMessage] of Object.entries(firebaseAuthErrorMessages)) {
|
||||
if (message.includes(code)) {
|
||||
return mappedMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultFirebaseAuthErrorMessage;
|
||||
}
|
||||
|
|
@ -1,7 +1,18 @@
|
|||
import { getApps, initializeApp } from
|
||||
'firebase/app';
|
||||
import { getAuth } from 'firebase/auth';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { getApps, initializeApp } from 'firebase/app';
|
||||
import { getAuth, type Auth } from 'firebase/auth';
|
||||
import { getFirestore } from 'firebase/firestore';
|
||||
import { Platform } from 'react-native';
|
||||
import type { FirebaseApp } from 'firebase/app';
|
||||
|
||||
type ReactNativeAuthModule = {
|
||||
getAuth(app?: FirebaseApp): Auth;
|
||||
initializeAuth(
|
||||
app: FirebaseApp,
|
||||
deps?: { persistence?: unknown }
|
||||
): Auth;
|
||||
getReactNativePersistence(storage: typeof AsyncStorage): unknown;
|
||||
};
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey:
|
||||
|
|
@ -17,8 +28,21 @@ const firebaseConfig = {
|
|||
appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
|
||||
};
|
||||
|
||||
const app = getApps().length ? getApps()[0] :
|
||||
initializeApp(firebaseConfig);
|
||||
const isAppInitialized = getApps().length > 0;
|
||||
const app = isAppInitialized ? getApps()[0] : initializeApp(firebaseConfig);
|
||||
const reactNativeAuth =
|
||||
Platform.OS === 'web'
|
||||
? null
|
||||
: (require('@firebase/auth') as ReactNativeAuthModule);
|
||||
|
||||
export const auth = getAuth(app);
|
||||
const auth =
|
||||
Platform.OS === 'web'
|
||||
? getAuth(app)
|
||||
: isAppInitialized
|
||||
? (reactNativeAuth as ReactNativeAuthModule).getAuth(app)
|
||||
: (reactNativeAuth as ReactNativeAuthModule).initializeAuth(app, {
|
||||
persistence: (reactNativeAuth as ReactNativeAuthModule).getReactNativePersistence(AsyncStorage),
|
||||
});
|
||||
|
||||
export { auth };
|
||||
export const db = getFirestore(app);
|
||||
|
|
@ -3,15 +3,19 @@ import {
|
|||
doc,
|
||||
type DocumentData,
|
||||
type FirestoreDataConverter,
|
||||
type PartialWithFieldValue,
|
||||
type QueryDocumentSnapshot,
|
||||
type SetOptions,
|
||||
type SnapshotOptions,
|
||||
type WithFieldValue,
|
||||
} from 'firebase/firestore';
|
||||
|
||||
import { db } from '@/lib/firebase';
|
||||
import type { UserDocument } from '@/types/firestore';
|
||||
|
||||
const converter = <T extends DocumentData>(): FirestoreDataConverter<T> => ({
|
||||
toFirestore: (value: T) => value,
|
||||
toFirestore: (value: WithFieldValue<T> | PartialWithFieldValue<T>, options?: SetOptions) =>
|
||||
value as DocumentData,
|
||||
fromFirestore: (
|
||||
snapshot: QueryDocumentSnapshot,
|
||||
options: SnapshotOptions
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||
"@react-navigation/elements": "^2.6.3",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type PropsWithChildren,
|
||||
} from 'react';
|
||||
import { onAuthStateChanged, type User } from 'firebase/auth';
|
||||
|
||||
import { auth } from '@/lib/firebase';
|
||||
|
||||
type AuthContextValue = {
|
||||
user: User | null;
|
||||
initializing: boolean;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: PropsWithChildren) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [initializing, setInitializing] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = onAuthStateChanged(auth, (nextUser) => {
|
||||
setUser(nextUser);
|
||||
setInitializing(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
initializing,
|
||||
}),
|
||||
[initializing, user]
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import {
|
||||
createUserWithEmailAndPassword,
|
||||
signInWithEmailAndPassword,
|
||||
signOut,
|
||||
updateProfile,
|
||||
} from 'firebase/auth';
|
||||
import { serverTimestamp, setDoc } from 'firebase/firestore';
|
||||
|
||||
import { auth } from '@/lib/firebase';
|
||||
import { userDoc } from '@/lib/firestore';
|
||||
|
||||
type RegisterParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
export async function loginWithEmail(email: string, password: string) {
|
||||
return signInWithEmailAndPassword(auth, email.trim(), password);
|
||||
}
|
||||
|
||||
export async function registerWithEmail({
|
||||
email,
|
||||
password,
|
||||
displayName,
|
||||
}: RegisterParams) {
|
||||
const credential = await createUserWithEmailAndPassword(
|
||||
auth,
|
||||
email.trim(),
|
||||
password
|
||||
);
|
||||
|
||||
const normalizedDisplayName = displayName.trim();
|
||||
|
||||
await updateProfile(credential.user, {
|
||||
displayName: normalizedDisplayName,
|
||||
});
|
||||
|
||||
await setDoc(userDoc(credential.user.uid), {
|
||||
uid: credential.user.uid,
|
||||
email: credential.user.email,
|
||||
displayName: normalizedDisplayName,
|
||||
photoURL: credential.user.photoURL,
|
||||
createdAt: serverTimestamp(),
|
||||
updatedAt: serverTimestamp(),
|
||||
});
|
||||
|
||||
return credential;
|
||||
}
|
||||
|
||||
export async function logout() {
|
||||
return signOut(auth);
|
||||
}
|
||||
|
|
@ -5,6 +5,6 @@ export type UserDocument = {
|
|||
email: string | null;
|
||||
displayName: string | null;
|
||||
photoURL: string | null;
|
||||
createdAt: Timestamp | null;
|
||||
updatedAt: Timestamp | null;
|
||||
createdAt: Timestamp;
|
||||
updatedAt: Timestamp;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue