This commit is contained in:
jjbrunton committed 2025-12-28 16:51:32 +00:00
1 parent 4d53d83769
commit 6ff5bc62f2
17 files changed
+2599 -1136

No files matched your search

+332 -262
View File
@@ -2,7 +2,6 @@ import { useState } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
@@ -12,9 +11,16 @@ import {
} from "react-native";
import { Redirect } from "expo-router";
import * as Linking from "expo-linking";
import Animated, {
FadeIn,
FadeInDown,
FadeInUp,
} from "react-native-reanimated";
import { Ionicons } from "@expo/vector-icons";
import { supabase } from "../../src/lib/supabase";
import { useAuthStore } from "../../src/store";
import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui";
import { colors, spacing, radius, typography } from "../../src/theme";
type AuthMode = "magic-link" | "password";
@@ -25,22 +31,32 @@ export default function LoginScreen() {
const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
const [authMode, setAuthMode] = useState<AuthMode>("magic-link");
const [isSignUp, setIsSignUp] = useState(false);
const [error, setError] = useState<string | null>(null);
const { isAuthenticated } = useAuthStore();
// Use declarative Redirect instead of imperative router.replace()
// This ensures navigation only happens after the Root Layout is mounted
const showError = (message: string) => {
setError(message);
// Also show native alert on mobile
if (Platform.OS !== 'web') {
Alert.alert("Error", message);
}
};
const clearError = () => setError(null);
if (isAuthenticated) {
return <Redirect href="/" />;
}
const handleMagicLink = async () => {
clearError();
if (!email.trim()) {
Alert.alert("Error", "Please enter your email");
showError("Please enter your email");
return;
}
setIsLoading(true);
const { error } = await supabase.auth.signInWithOtp({
const { error: authError } = await supabase.auth.signInWithOtp({
email: email.trim(),
options: {
emailRedirectTo: Linking.createURL("/(auth)/login"),
@@ -49,56 +65,66 @@ export default function LoginScreen() {
setIsLoading(false);
if (error) {
Alert.alert("Error", error.message);
if (authError) {
showError(authError.message);
} else {
setIsMagicLinkSent(true);
}
};
const handlePasswordAuth = async () => {
clearError();
if (!email.trim()) {
Alert.alert("Error", "Please enter your email");
showError("Please enter your email");
return;
}
if (!password) {
Alert.alert("Error", "Please enter your password");
showError("Please enter your password");
return;
}
if (isSignUp && password.length < 6) {
Alert.alert("Error", "Password must be at least 6 characters");
showError("Password must be at least 6 characters");
return;
}
setIsLoading(true);
if (isSignUp) {
const { error } = await supabase.auth.signUp({
const { error: signUpError } = await supabase.auth.signUp({
email: email.trim(),
password,
});
setIsLoading(false);
if (error) {
Alert.alert("Error", error.message);
if (signUpError) {
showError(signUpError.message);
} else {
Alert.alert(
"Check your email",
"We sent you a confirmation link to verify your email address.",
[{ text: "OK" }]
);
// Show success message
if (Platform.OS === 'web') {
setError(null);
Alert.alert(
"Check your email",
"We sent you a confirmation link to verify your email address."
);
} else {
Alert.alert(
"Check your email",
"We sent you a confirmation link to verify your email address.",
[{ text: "OK" }]
);
}
}
} else {
const { error } = await supabase.auth.signInWithPassword({
const { error: signInError } = await supabase.auth.signInWithPassword({
email: email.trim(),
password,
});
setIsLoading(false);
if (error) {
Alert.alert("Error", error.message);
if (signInError) {
showError(signInError.message);
}
}
};
@@ -131,290 +157,334 @@ export default function LoginScreen() {
if (isMagicLinkSent) {
return (
<View style={styles.container}>
<View style={styles.content}>
<Text style={styles.emoji}>✉️</Text>
<Text style={styles.title}>Check your email</Text>
<Text style={styles.subtitle}>
We sent a magic link to {email}
</Text>
<TouchableOpacity
style={styles.secondaryButton}
onPress={() => setIsMagicLinkSent(false)}
<GradientBackground showAccent>
<View style={styles.container}>
<Animated.View
entering={FadeInUp.duration(600).springify()}
style={styles.content}
>
<Text style={styles.secondaryButtonText}>Try a different email</Text>
</TouchableOpacity>
<View style={styles.iconContainer}>
<Ionicons name="mail" size={48} color={colors.primary} />
</View>
<Text style={styles.title}>Check your email</Text>
<Text style={styles.subtitle}>
We sent a magic link to{'\n'}
<Text style={styles.emailHighlight}>{email}</Text>
</Text>
<GlassButton
variant="ghost"
onPress={() => setIsMagicLinkSent(false)}
style={{ marginTop: spacing.lg }}
>
Try a different email
</GlassButton>
</Animated.View>
</View>
</View>
</GradientBackground>
);
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
<GradientBackground showAccent>
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View style={styles.content}>
<Text style={styles.emoji}>💕</Text>
<Text style={styles.title}>Sauci</Text>
<Text style={styles.subtitle}>
Explore intimacy together
</Text>
{/* Auth Mode Toggle */}
<View style={styles.modeToggleContainer}>
<TouchableOpacity
style={[
styles.modeToggle,
authMode === "magic-link" && styles.modeToggleActive,
]}
onPress={() => setAuthMode("magic-link")}
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
<View style={styles.content}>
{/* Logo/Brand */}
<Animated.View
entering={FadeInDown.delay(100).duration(600).springify()}
style={styles.brandContainer}
>
<Text
style={[
styles.modeToggleText,
authMode === "magic-link" && styles.modeToggleTextActive,
]}
>
Magic Link
<View style={styles.logoContainer}>
<Ionicons name="heart" size={40} color={colors.primary} />
</View>
<Text style={styles.title}>Sauci</Text>
<Text style={styles.subtitle}>
Explore intimacy together
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modeToggle,
authMode === "password" && styles.modeToggleActive,
]}
onPress={() => setAuthMode("password")}
</Animated.View>
{/* Auth Card */}
<Animated.View
entering={FadeInDown.delay(200).duration(600).springify()}
style={styles.formContainer}
>
<Text
style={[
styles.modeToggleText,
authMode === "password" && styles.modeToggleTextActive,
]}
>
Password
</Text>
</TouchableOpacity>
</View>
<GlassCard variant="elevated">
{/* Auth Mode Toggle */}
<View style={styles.modeToggleContainer}>
<TouchableOpacity
style={[
styles.modeToggle,
authMode === "magic-link" && styles.modeToggleActive,
]}
onPress={() => setAuthMode("magic-link")}
activeOpacity={0.7}
>
<Text
style={[
styles.modeToggleText,
authMode === "magic-link" && styles.modeToggleTextActive,
]}
>
Magic Link
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.modeToggle,
authMode === "password" && styles.modeToggleActive,
]}
onPress={() => setAuthMode("password")}
activeOpacity={0.7}
>
<Text
style={[
styles.modeToggleText,
authMode === "password" && styles.modeToggleTextActive,
]}
>
Password
</Text>
</TouchableOpacity>
</View>
<View style={styles.form}>
<TextInput
style={styles.input}
placeholder="Enter your email"
placeholderTextColor="#666"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
{/* Error Message */}
{error && (
<Animated.View
entering={FadeIn.duration(200)}
style={styles.errorContainer}
>
<Ionicons name="alert-circle" size={18} color={colors.error} />
<Text style={styles.errorText}>{error}</Text>
</Animated.View>
)}
{authMode === "password" && (
<TextInput
style={styles.input}
placeholder="Enter your password"
placeholderTextColor="#666"
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
/>
)}
{/* Form Fields */}
<View style={styles.form}>
<GlassInput
placeholder="Enter your email"
value={email}
onChangeText={(text) => { clearError(); setEmail(text); }}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
icon={<Ionicons name="mail-outline" size={20} color={colors.textTertiary} />}
/>
{authMode === "magic-link" ? (
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleMagicLink}
disabled={isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? "Sending..." : "Continue with Magic Link"}
</Text>
</TouchableOpacity>
) : (
<>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handlePasswordAuth}
disabled={isLoading}
{authMode === "password" && (
<GlassInput
placeholder="Enter your password"
value={password}
onChangeText={(text) => { clearError(); setPassword(text); }}
secureTextEntry
autoCapitalize="none"
icon={<Ionicons name="lock-closed-outline" size={20} color={colors.textTertiary} />}
/>
)}
{authMode === "magic-link" ? (
<GlassButton
onPress={handleMagicLink}
loading={isLoading}
disabled={isLoading}
fullWidth
>
Continue with Magic Link
</GlassButton>
) : (
<>
<GlassButton
onPress={handlePasswordAuth}
loading={isLoading}
disabled={isLoading}
fullWidth
>
{isSignUp ? "Create Account" : "Sign In"}
</GlassButton>
<TouchableOpacity
style={styles.toggleSignUp}
onPress={() => setIsSignUp(!isSignUp)}
>
<Text style={styles.toggleSignUpText}>
{isSignUp
? "Already have an account? Sign In"
: "Don't have an account? Sign Up"}
</Text>
</TouchableOpacity>
</>
)}
</View>
</GlassCard>
</Animated.View>
{/* Social Sign In */}
<Animated.View
entering={FadeInDown.delay(300).duration(600).springify()}
style={styles.socialContainer}
>
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or continue with</Text>
<View style={styles.dividerLine} />
</View>
<View style={styles.socialButtons}>
<GlassButton
variant="secondary"
onPress={handleGoogleSignIn}
icon={<Ionicons name="logo-google" size={20} color={colors.text} />}
style={styles.socialButton}
>
<Text style={styles.buttonText}>
{isLoading
? isSignUp
? "Creating Account..."
: "Signing In..."
: isSignUp
? "Create Account"
: "Sign In"}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.toggleSignUp}
onPress={() => setIsSignUp(!isSignUp)}
Google
</GlassButton>
<GlassButton
variant="secondary"
onPress={handleAppleSignIn}
icon={<Ionicons name="logo-apple" size={20} color={colors.text} />}
style={styles.socialButton}
>
<Text style={styles.toggleSignUpText}>
{isSignUp
? "Already have an account? Sign In"
: "Don't have an account? Sign Up"}
</Text>
</TouchableOpacity>
</>
)}
<View style={styles.divider}>
<View style={styles.dividerLine} />
<Text style={styles.dividerText}>or</Text>
<View style={styles.dividerLine} />
</View>
<TouchableOpacity
style={styles.socialButton}
onPress={handleGoogleSignIn}
>
<Text style={styles.socialButtonText}>Continue with Google</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.socialButton}
onPress={handleAppleSignIn}
>
<Text style={styles.socialButtonText}>Continue with Apple</Text>
</TouchableOpacity>
Apple
</GlassButton>
</View>
</Animated.View>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
</ScrollView>
</KeyboardAvoidingView>
</GradientBackground>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#1a1a2e",
},
content: {
flex: 1,
paddingHorizontal: 24,
paddingTop: 80,
alignItems: "center",
},
emoji: {
fontSize: 64,
marginBottom: 16,
},
title: {
fontSize: 32,
fontWeight: "bold",
color: "#fff",
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: "#888",
marginBottom: 48,
textAlign: "center",
},
form: {
width: "100%",
},
input: {
backgroundColor: "#16213e",
borderRadius: 12,
padding: 16,
fontSize: 16,
color: "#fff",
marginBottom: 16,
borderWidth: 1,
borderColor: "#0f3460",
},
button: {
backgroundColor: "#e94560",
borderRadius: 12,
padding: 16,
alignItems: "center",
},
buttonDisabled: {
opacity: 0.7,
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
divider: {
flexDirection: "row",
alignItems: "center",
marginVertical: 24,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: "#333",
},
dividerText: {
color: "#666",
paddingHorizontal: 16,
},
socialButton: {
backgroundColor: "#16213e",
borderRadius: 12,
padding: 16,
alignItems: "center",
marginBottom: 12,
borderWidth: 1,
borderColor: "#0f3460",
},
socialButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "500",
},
secondaryButton: {
marginTop: 24,
},
secondaryButtonText: {
color: "#e94560",
fontSize: 16,
},
scrollContent: {
flexGrow: 1,
},
content: {
flex: 1,
paddingHorizontal: spacing.lg,
paddingTop: 80,
paddingBottom: spacing.xl,
},
brandContainer: {
alignItems: "center",
marginBottom: spacing.xl,
},
logoContainer: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.primaryLight,
justifyContent: "center",
alignItems: "center",
marginBottom: spacing.md,
},
title: {
...typography.largeTitle,
color: colors.text,
marginBottom: spacing.xs,
},
subtitle: {
...typography.body,
color: colors.textSecondary,
textAlign: "center",
},
emailHighlight: {
color: colors.primary,
fontWeight: "600",
},
iconContainer: {
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: colors.primaryLight,
justifyContent: "center",
alignItems: "center",
marginBottom: spacing.lg,
},
formContainer: {
marginBottom: spacing.lg,
},
modeToggleContainer: {
flexDirection: "row",
backgroundColor: "#16213e",
borderRadius: 12,
backgroundColor: colors.glass.background,
borderRadius: radius.md,
padding: 4,
marginBottom: 24,
width: "100%",
marginBottom: spacing.lg,
},
modeToggle: {
flex: 1,
paddingVertical: 12,
borderRadius: 8,
borderRadius: radius.sm,
alignItems: "center",
},
modeToggleActive: {
backgroundColor: "#e94560",
backgroundColor: colors.primary,
},
modeToggleText: {
color: "#666",
fontSize: 14,
fontWeight: "500",
...typography.subhead,
color: colors.textTertiary,
fontWeight: "600",
},
modeToggleTextActive: {
color: "#fff",
color: colors.text,
},
errorContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.errorLight,
padding: spacing.md,
borderRadius: radius.md,
marginBottom: spacing.md,
gap: spacing.sm,
},
errorText: {
...typography.subhead,
color: colors.error,
flex: 1,
},
form: {
gap: spacing.sm,
},
toggleSignUp: {
marginTop: 16,
marginTop: spacing.md,
alignItems: "center",
},
toggleSignUpText: {
color: "#e94560",
fontSize: 14,
...typography.subhead,
color: colors.primary,
},
socialContainer: {
marginTop: spacing.md,
},
divider: {
flexDirection: "row",
alignItems: "center",
marginBottom: spacing.lg,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: colors.glass.border,
},
dividerText: {
...typography.caption1,
color: colors.textTertiary,
paddingHorizontal: spacing.md,
},
socialButtons: {
flexDirection: "row",
gap: spacing.md,
},
socialButton: {
flex: 1,
},
});