diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 0a2a9a7..12a8ed5 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -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
-
- ;
+ const { initializing, user } = useAuth();
+
+ if (initializing) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ return (
+
+ Session active
+ Bienvenue
+
+ {user.displayName || user.email || user.uid}
+
+
+
+ UID
+ {user.uid}
+ Email
+ {user.email || 'Aucun email'}
+
+
+
+ );
}
+
+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,
+ },
+});
diff --git a/app/_layout.tsx b/app/_layout.tsx
index 2b999d7..c514d7a 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -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 (
-
-
-
+
+
+
+
+
+
+
+
+
);
}
diff --git a/app/login.tsx b/app/login.tsx
new file mode 100644
index 0000000..fb19bd8
--- /dev/null
+++ b/app/login.tsx
@@ -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 ;
+ }
+
+ return ;
+}
diff --git a/app/register.tsx b/app/register.tsx
new file mode 100644
index 0000000..0901f1c
--- /dev/null
+++ b/app/register.tsx
@@ -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 ;
+ }
+
+ return ;
+}
diff --git a/bun.lock b/bun.lock
index b00eda1..9c13c5d 100644
--- a/bun.lock
+++ b/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=="],
diff --git a/components/auth/AuthForm.tsx b/components/auth/AuthForm.tsx
new file mode 100644
index 0000000..95714c6
--- /dev/null
+++ b/components/auth/AuthForm.tsx
@@ -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(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 (
+
+
+
+
+ {copy.title}
+
+
+
+ {mode === 'register' ? (
+
+ Nom
+
+
+ ) : null}
+
+
+ Email
+
+
+
+
+ Mot de passe
+
+
+
+ {error ? {error} : null}
+
+
+
+ {isSubmitting ? (
+
+ ) : null}
+
+
+ {copy.switchLabel}
+
+
+
+
+
+ );
+}
+
+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',
+ },
+});
diff --git a/components/common/Button.tsx b/components/common/Button.tsx
index b98c098..39877c7 100644
--- a/components/common/Button.tsx
+++ b/components/common/Button.tsx
@@ -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,
@@ -92,4 +93,4 @@ const styles = StyleSheet.create({
disabled: {
opacity: 0.5,
},
-});
\ No newline at end of file
+});
diff --git a/constants/palette.ts b/constants/palette.ts
index 40112c1..1d3d0bd 100644
--- a/constants/palette.ts
+++ b/constants/palette.ts
@@ -1,4 +1,8 @@
export const Palette = {
white: '#FFF',
- black: '#000'
-};
\ No newline at end of file
+ black: '#000',
+ transparent: 'transparent',
+ cream: '#f5efe6',
+ gray: '#5a5148',
+ danger: '#9f2d2d',
+};
diff --git a/constants/styles.ts b/constants/styles.ts
index e69de29..f1e039a 100644
--- a/constants/styles.ts
+++ b/constants/styles.ts
@@ -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;
+
diff --git a/lib/firebase-auth-errors.ts b/lib/firebase-auth-errors.ts
new file mode 100644
index 0000000..994bb52
--- /dev/null
+++ b/lib/firebase-auth-errors.ts
@@ -0,0 +1,21 @@
+const firebaseAuthErrorMessages: Record = {
+ '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;
+}
diff --git a/lib/firebase.ts b/lib/firebase.ts
index 6bb906b..756a51f 100644
--- a/lib/firebase.ts
+++ b/lib/firebase.ts
@@ -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:
@@ -14,11 +25,24 @@ const firebaseConfig = {
process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
messagingSenderId:
process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
- appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
+ 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);
-export const db = getFirestore(app);
\ No newline at end of file
+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);
diff --git a/lib/firestore.ts b/lib/firestore.ts
index d852362..38f95c0 100644
--- a/lib/firestore.ts
+++ b/lib/firestore.ts
@@ -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 = (): FirestoreDataConverter => ({
- toFirestore: (value: T) => value,
+ toFirestore: (value: WithFieldValue | PartialWithFieldValue, options?: SetOptions) =>
+ value as DocumentData,
fromFirestore: (
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
diff --git a/package.json b/package.json
index 6442634..3460c17 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/providers/AuthProvider.tsx b/providers/AuthProvider.tsx
new file mode 100644
index 0000000..d33734b
--- /dev/null
+++ b/providers/AuthProvider.tsx
@@ -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(undefined);
+
+export function AuthProvider({ children }: PropsWithChildren) {
+ const [user, setUser] = useState(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 {children};
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+
+ return context;
+}
diff --git a/services/auth.ts b/services/auth.ts
new file mode 100644
index 0000000..547f76b
--- /dev/null
+++ b/services/auth.ts
@@ -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);
+}
diff --git a/types/firestore.ts b/types/firestore.ts
index df6a924..e097480 100644
--- a/types/firestore.ts
+++ b/types/firestore.ts
@@ -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;
};