Work on supabase auth

This commit is contained in:
Arthur Belleville 2025-05-05 09:11:10 +02:00
parent aee1104a1b
commit 9cb341ed11
No known key found for this signature in database
12 changed files with 1101 additions and 46 deletions

View file

@ -1 +1,3 @@
EXPO_PUBLIC_STREAM_CHAT_API_KEY="t5vvvddteapa"
EXPO_PUBLIC_STREAM_CHAT_API_KEY="t5vvvddteapa"
EXPO_PUBLIC_SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM

View file

@ -0,0 +1,15 @@
import { Redirect, Stack } from "expo-router";
import { useAuth } from "@/stores/auth";
import { ActivityIndicator } from "react-native";
export default function AuthLayout() {
const { user, loading } = useAuth();
console.log("user", user);
if (loading) {
return <ActivityIndicator />;
}
if (user) {
return <Redirect href="/(home)/(tabs)" />;
}
return <Stack />;
}

View file

@ -0,0 +1,143 @@
import React, { useState } from "react";
import { Alert, StyleSheet, View, AppState, Text } from "react-native";
import { supabase } from "@/lib/supabase";
import { Button, Input } from "@rneui/themed";
// Tells Supabase Auth to continuously refresh the session automatically if
// the app is in the foreground. When this is added, you will continue to receive
// `onAuthStateChange` events with the `TOKEN_REFRESHED` or `SIGNED_OUT` event
// if the user's session is terminated. This should only be registered once.
AppState.addEventListener("change", (state) => {
if (state === "active") {
supabase.auth.startAutoRefresh();
} else {
supabase.auth.stopAutoRefresh();
}
});
export default function Auth() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
async function signInWithEmail() {
setLoading(true);
const { error } = await supabase.auth.signInWithPassword({
email: email,
password: password,
});
if (error) Alert.alert(error.message);
setLoading(false);
}
async function signUpWithEmail() {
setLoading(true);
const {
data: { session },
error,
} = await supabase.auth.signUp({
email: email,
password: password,
});
if (error) Alert.alert(error.message);
if (!session)
Alert.alert("Please check your inbox for email verification!");
setLoading(false);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome!</Text>
<Text style={styles.subtitle}>Sign in or create an account</Text>
<View style={[styles.verticallySpaced, styles.mt40]}>
<Input
label="Email"
leftIcon={{ type: "font-awesome", name: "envelope" }}
onChangeText={(text) => setEmail(text)}
value={email}
placeholder="email@address.com"
autoCapitalize={"none"}
/>
</View>
<View style={styles.verticallySpaced}>
<Input
label="Password"
leftIcon={{ type: "font-awesome", name: "lock" }}
onChangeText={(text) => setPassword(text)}
value={password}
secureTextEntry={true}
placeholder="Password"
autoCapitalize={"none"}
/>
</View>
<View style={[styles.verticallySpaced, styles.mt20]}>
<Button
title="Sign in"
disabled={loading}
onPress={() => signInWithEmail()}
buttonStyle={styles.button}
titleStyle={styles.buttonTitle}
/>
</View>
<View style={styles.verticallySpaced}>
<Button
title="Sign up"
disabled={loading}
onPress={() => signUpWithEmail()}
buttonStyle={[styles.button, styles.signUpButton]}
titleStyle={styles.signUpButtonTitle}
type="outline"
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
padding: 24,
backgroundColor: "#f5f5f5", // Light grey background
},
title: {
fontSize: 32,
fontWeight: "bold",
textAlign: "center",
marginBottom: 10,
color: "#333",
},
subtitle: {
fontSize: 16,
textAlign: "center",
marginBottom: 40,
color: "#666",
},
verticallySpaced: {
paddingTop: 8, // Increased padding
paddingBottom: 8, // Increased padding
alignSelf: "stretch",
},
mt20: {
marginTop: 20,
},
mt40: {
marginTop: 40, // Increased top margin for the first input
},
button: {
paddingVertical: 12,
borderRadius: 8, // Rounded corners
},
buttonTitle: {
fontWeight: "bold",
},
signUpButton: {
// Specific styles for sign up button if needed (e.g., different color)
// Example: borderColor: '#007bff',
},
signUpButtonTitle: {
// Example: color: '#007bff',
},
});

View file

@ -1,4 +1,4 @@
import { StyleSheet, Image, Platform } from "react-native";
import { StyleSheet, Image, Platform, Button } from "react-native";
import { Collapsible } from "@/components/Collapsible";
import { ExternalLink } from "@/components/ExternalLink";
@ -6,8 +6,10 @@ import ParallaxScrollView from "@/components/ParallaxScrollView";
import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
import { IconSymbol } from "@/components/ui/IconSymbol";
import { useAuth } from "@/stores/auth";
export default function ProfileScreen() {
const { signOut } = useAuth();
return (
<ParallaxScrollView
headerBackgroundColor={{ light: "#D0D0D0", dark: "#353636" }}
@ -23,6 +25,7 @@ export default function ProfileScreen() {
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Explore</ThemedText>
</ThemedView>
<Button title="Disconnect" onPress={signOut} />
<ThemedText>
This app includes example code to help you get started.
</ThemedText>
@ -125,5 +128,6 @@ const styles = StyleSheet.create({
titleContainer: {
flexDirection: "row",
gap: 8,
alignItems: "center",
},
});

View file

@ -1,11 +1,15 @@
import ChatProvider from "@/providers/ChatProvider";
import { Stack } from "expo-router";
import { useAuth } from "@/stores/auth";
import { Redirect, Stack } from "expo-router";
export default function HomeLayout() {
const { user } = useAuth();
if (!user) {
return <Redirect href="/(auth)/login" />;
}
return (
<ChatProvider>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="channel" options={{ headerShown: false }} />
</Stack>

View file

@ -1,13 +1,13 @@
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { Link, Stack } from "expo-router";
import { StyleSheet } from "react-native";
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { ThemedText } from "@/components/ThemedText";
import { ThemedView } from "@/components/ThemedView";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<Stack.Screen options={{ title: "Oops!" }} />
<ThemedView style={styles.container}>
<ThemedText type="title">This screen doesn't exist.</ThemedText>
<Link href="/" style={styles.link}>
@ -21,8 +21,8 @@ export default function NotFoundScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
alignItems: "center",
justifyContent: "center",
padding: 20,
},
link: {

View file

@ -36,6 +36,7 @@ export default function RootLayout() {
<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(home)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<StatusBar style="auto" />

View file

@ -0,0 +1,19 @@
import "react-native-url-polyfill/auto";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL as string;
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error("Missing Supabase environment variables");
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false,
},
});

View file

@ -0,0 +1,9 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
config.resolver.unstable_enablePackageExports = false;
module.exports = config;

File diff suppressed because it is too large Load diff

View file

@ -15,9 +15,13 @@
"preset": "jest-expo"
},
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@expo/vector-icons": "^14.0.0",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@rneui/base": "^4.0.0-rc.7",
"@rneui/themed": "^4.0.0-rc.7",
"@supabase/supabase-js": "^2.49.4",
"expo": "^53.0.0",
"expo-av": "~15.1.4",
"expo-blur": "~14.1.4",
@ -50,7 +54,7 @@
"@babel/core": "^7.25.2",
"@types/jest": "^29.5.12",
"@types/react": "~19.0.10",
"@types/react-test-renderer": "^18.3.0",
"@types/react-test-renderer": "^19.0.0",
"jest": "^29.2.1",
"jest-expo": "~53.0.4",
"react-test-renderer": "18.3.1",

View file

@ -0,0 +1,36 @@
import { create } from "zustand";
import { Session, User } from "@supabase/supabase-js";
import { supabase } from "@/lib/supabase";
interface AuthState {
session: Session | null;
user: User | null;
loading: boolean;
initialize: () => Promise<void>;
setSession: (session: Session | null) => void;
signOut: () => Promise<void>;
}
export const useAuth = create<AuthState>((set) => ({
session: null,
user: null,
loading: true,
initialize: async () => {
const {
data: { session },
} = await supabase.auth.getSession();
set({ session, user: session?.user, loading: false });
},
setSession: (session) => set({ session, user: session?.user }),
signOut: async () => {
await supabase.auth.signOut();
},
}));
// Set up auth state listeners
supabase.auth.onAuthStateChange((_event, session) => {
useAuth.getState().setSession(session);
});
// Initialize auth state
useAuth.getState().initialize();