Work on supabase auth
This commit is contained in:
parent
aee1104a1b
commit
9cb341ed11
12 changed files with 1101 additions and 46 deletions
|
|
@ -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
|
||||
15
xtablo-expo/app/(auth)/_layout.tsx
Normal file
15
xtablo-expo/app/(auth)/_layout.tsx
Normal 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 />;
|
||||
}
|
||||
143
xtablo-expo/app/(auth)/login.tsx
Normal file
143
xtablo-expo/app/(auth)/login.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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",
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
19
xtablo-expo/lib/supabase.ts
Normal file
19
xtablo-expo/lib/supabase.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
9
xtablo-expo/metro.config.js
Normal file
9
xtablo-expo/metro.config.js
Normal 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;
|
||||
884
xtablo-expo/package-lock.json
generated
884
xtablo-expo/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
36
xtablo-expo/stores/auth.tsx
Normal file
36
xtablo-expo/stores/auth.tsx
Normal 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();
|
||||
Loading…
Reference in a new issue