suna/apps/mobile/components/AuthOverlay.tsx

431 lines
16 KiB
TypeScript
Raw Normal View History

2025-06-20 05:23:50 +08:00
import { fontWeights } from '@/constants/Fonts';
import { supabase } from '@/constants/SupabaseConfig';
2025-08-29 22:42:05 +08:00
import { useTheme } from '@/hooks/useThemeColor';
2025-06-20 05:23:50 +08:00
import React, { useState } from 'react';
import {
Alert,
KeyboardAvoidingView,
2025-07-02 04:00:56 +08:00
Linking,
2025-06-20 05:23:50 +08:00
Modal,
Platform,
2025-08-29 22:42:05 +08:00
StyleSheet,
2025-06-20 05:23:50 +08:00
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
2025-08-29 22:42:05 +08:00
import { Card } from './ui/Card';
2025-06-20 05:23:50 +08:00
interface AuthOverlayProps {
visible: boolean;
onClose: () => void;
}
export const AuthOverlay: React.FC<AuthOverlayProps> = ({ visible, onClose }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
2025-07-02 04:00:56 +08:00
const [confirmPassword, setConfirmPassword] = useState('');
2025-06-20 05:23:50 +08:00
const [loading, setLoading] = useState(false);
2025-07-02 04:00:56 +08:00
const [mode, setMode] = useState<'signin' | 'signup' | 'forgot'>('signin');
const [showSuccess, setShowSuccess] = useState(false);
const [successEmail, setSuccessEmail] = useState('');
2025-06-20 05:23:50 +08:00
2025-08-29 22:42:05 +08:00
const theme = useTheme();
const styles = StyleSheet.create({
2025-06-20 05:23:50 +08:00
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
2025-08-29 22:42:05 +08:00
justifyContent: 'center',
alignItems: 'center',
2025-06-20 05:23:50 +08:00
padding: 20,
},
title: {
2025-08-29 22:42:05 +08:00
fontSize: 22,
2025-06-20 05:23:50 +08:00
fontFamily: fontWeights[600],
color: theme.foreground,
2025-08-29 22:42:05 +08:00
textAlign: 'center',
marginBottom: 16,
2025-06-20 05:23:50 +08:00
},
input: {
borderWidth: 1,
borderColor: theme.border,
2025-08-29 22:42:05 +08:00
borderRadius: 12,
2025-06-20 05:23:50 +08:00
padding: 12,
2025-08-29 22:42:05 +08:00
height: 48,
marginBottom: 0,
2025-06-20 05:23:50 +08:00
fontSize: 16,
color: theme.foreground,
backgroundColor: theme.background,
},
button: {
backgroundColor: theme.primary,
2025-08-29 22:42:05 +08:00
borderRadius: 12,
height: 48,
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
marginBottom: 16,
2025-06-20 05:23:50 +08:00
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: theme.background,
fontSize: 16,
fontFamily: fontWeights[600],
},
switchButton: {
2025-08-29 22:42:05 +08:00
alignItems: 'center',
padding: 6,
2025-06-20 05:23:50 +08:00
},
switchText: {
color: theme.primary,
fontSize: 14,
fontFamily: fontWeights[500],
},
closeButton: {
2025-08-29 22:42:05 +08:00
position: 'absolute',
top: 12,
right: 12,
padding: 6,
zIndex: 1,
2025-06-20 05:23:50 +08:00
},
closeText: {
color: theme.mutedForeground,
fontSize: 18,
fontFamily: fontWeights[500],
},
2025-07-02 04:00:56 +08:00
successContainer: {
2025-08-29 22:42:05 +08:00
alignItems: 'center',
2025-07-02 04:00:56 +08:00
},
successIcon: {
2025-08-29 22:42:05 +08:00
width: 64,
height: 64,
borderRadius: 32,
2025-07-02 04:00:56 +08:00
backgroundColor: '#10B981',
2025-08-29 22:42:05 +08:00
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
2025-07-02 04:00:56 +08:00
},
successIconText: {
2025-08-29 22:42:05 +08:00
fontSize: 28,
2025-07-02 04:00:56 +08:00
color: '#ffffff',
},
successTitle: {
2025-08-29 22:42:05 +08:00
fontSize: 24,
2025-07-02 04:00:56 +08:00
fontFamily: fontWeights[600],
color: theme.foreground,
2025-08-29 22:42:05 +08:00
textAlign: 'center',
marginBottom: 8,
2025-07-02 04:00:56 +08:00
},
successDescription: {
2025-08-29 22:42:05 +08:00
fontSize: 15,
2025-07-02 04:00:56 +08:00
color: theme.mutedForeground,
2025-08-29 22:42:05 +08:00
textAlign: 'center',
marginBottom: 6,
lineHeight: 20,
2025-07-02 04:00:56 +08:00
},
successEmail: {
2025-08-29 22:42:05 +08:00
fontSize: 15,
2025-07-02 04:00:56 +08:00
fontFamily: fontWeights[600],
color: theme.foreground,
2025-08-29 22:42:05 +08:00
textAlign: 'center',
marginBottom: 16,
2025-07-02 04:00:56 +08:00
},
successNote: {
backgroundColor: '#10B98120',
borderColor: '#10B98140',
borderWidth: 1,
borderRadius: 8,
2025-08-29 22:42:05 +08:00
padding: 12,
marginBottom: 20,
2025-07-02 04:00:56 +08:00
},
successNoteText: {
2025-08-29 22:42:05 +08:00
fontSize: 13,
2025-07-02 04:00:56 +08:00
color: '#059669',
2025-08-29 22:42:05 +08:00
textAlign: 'center',
lineHeight: 18,
2025-07-02 04:00:56 +08:00
},
successButtonContainer: {
2025-08-29 22:42:05 +08:00
width: '100%',
gap: 8,
2025-07-02 04:00:56 +08:00
},
legalText: {
2025-08-29 22:42:05 +08:00
fontSize: 11,
2025-07-02 04:00:56 +08:00
color: theme.mutedForeground,
2025-08-29 22:42:05 +08:00
textAlign: 'center',
marginTop: 12,
lineHeight: 14,
paddingHorizontal: 4,
2025-07-02 04:00:56 +08:00
},
legalLink: {
color: theme.primary,
},
2025-08-29 22:42:05 +08:00
});
2025-06-20 05:23:50 +08:00
const handleAuth = async () => {
2025-07-02 04:00:56 +08:00
if (!email.trim()) {
Alert.alert('Error', 'Please enter your email');
return;
}
if (mode !== 'forgot' && !password.trim()) {
Alert.alert('Error', 'Please enter your password');
2025-06-20 05:23:50 +08:00
return;
}
setLoading(true);
try {
2025-07-02 04:00:56 +08:00
if (mode === 'signup') {
if (password !== confirmPassword) {
Alert.alert('Error', 'Passwords do not match');
return;
}
if (password.length < 6) {
Alert.alert('Error', 'Password must be at least 6 characters');
return;
}
2025-06-20 05:23:50 +08:00
const { error } = await supabase.auth.signUp({
email: email.trim(),
password: password.trim(),
});
if (error) {
Alert.alert('Sign Up Error', error.message);
} else {
2025-07-02 04:00:56 +08:00
setSuccessEmail(email.trim());
setShowSuccess(true);
}
} else if (mode === 'forgot') {
const { error } = await supabase.auth.resetPasswordForEmail(email.trim());
if (error) {
Alert.alert('Error', error.message);
} else {
Alert.alert('Success', 'Check your email for a password reset link');
setMode('signin');
2025-06-20 05:23:50 +08:00
}
} else {
const { error } = await supabase.auth.signInWithPassword({
email: email.trim(),
password: password.trim(),
});
if (error) {
2025-07-02 04:00:56 +08:00
if (error.message === 'Email not confirmed') {
Alert.alert(
'Email Not Confirmed',
'Please check your email and click the confirmation link before signing in.',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Resend Email',
onPress: async () => {
const { error: resendError } = await supabase.auth.resend({
type: 'signup',
email: email.trim()
});
if (resendError) {
Alert.alert('Error', resendError.message);
} else {
Alert.alert('Success', 'Confirmation email sent! Please check your inbox.');
}
}
}
]
);
} else {
Alert.alert('Sign In Error', error.message);
}
2025-06-20 05:23:50 +08:00
} else {
onClose();
}
}
} catch (error) {
Alert.alert('Error', 'An unexpected error occurred');
} finally {
setLoading(false);
}
};
const resetForm = () => {
setEmail('');
setPassword('');
2025-07-02 04:00:56 +08:00
setConfirmPassword('');
setMode('signin');
setShowSuccess(false);
setSuccessEmail('');
2025-06-20 05:23:50 +08:00
};
const handleClose = () => {
resetForm();
onClose();
};
2025-07-02 04:00:56 +08:00
const handleBackToSignIn = () => {
setShowSuccess(false);
setSuccessEmail('');
setMode('signin');
setEmail('');
setPassword('');
setConfirmPassword('');
};
2025-06-20 05:23:50 +08:00
return (
<Modal visible={visible} transparent animationType="fade">
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.overlay}
>
2025-08-29 22:42:05 +08:00
<Card
style={{
width: '100%',
maxWidth: 400,
position: 'relative',
padding: 16,
gap: 16,
}}
bordered
elevated
>
2025-06-20 05:23:50 +08:00
<TouchableOpacity style={styles.closeButton} onPress={handleClose}>
<Text style={styles.closeText}></Text>
</TouchableOpacity>
2025-07-02 04:00:56 +08:00
{showSuccess ? (
<View style={styles.successContainer}>
<View style={styles.successIcon}>
<Text style={styles.successIconText}></Text>
</View>
2025-06-20 05:23:50 +08:00
2025-07-02 04:00:56 +08:00
<Text style={styles.successTitle}>Check your email</Text>
<Text style={styles.successDescription}>
We've sent a confirmation link to:
</Text>
<Text style={styles.successEmail}>{successEmail}</Text>
<View style={styles.successNote}>
<Text style={styles.successNoteText}>
Click the link in the email to activate your account. If you don't see the email, check your spam folder.
</Text>
</View>
<View style={styles.successButtonContainer}>
<TouchableOpacity style={styles.button} onPress={handleBackToSignIn}>
<Text style={styles.buttonText}>Back to Sign In</Text>
</TouchableOpacity>
<TouchableOpacity
2025-08-29 22:42:05 +08:00
style={[styles.button, { backgroundColor: 'transparent', borderWidth: 1, borderColor: theme.primary }]}
2025-07-02 04:00:56 +08:00
onPress={handleClose}
>
2025-08-29 22:42:05 +08:00
<Text style={[styles.buttonText, { color: theme.primary }]}>Close</Text>
2025-07-02 04:00:56 +08:00
</TouchableOpacity>
</View>
</View>
) : (
<>
<Text style={styles.title}>
{mode === 'signup' ? 'Create Account' : mode === 'forgot' ? 'Reset Password' : 'Sign In'}
</Text>
<TextInput
style={styles.input}
placeholder="Email"
2025-08-29 22:42:05 +08:00
placeholderTextColor={theme.mutedForeground}
2025-07-02 04:00:56 +08:00
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
{mode !== 'forgot' && (
<TextInput
style={styles.input}
placeholder="Password"
2025-08-29 22:42:05 +08:00
placeholderTextColor={theme.mutedForeground}
2025-07-02 04:00:56 +08:00
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
/>
)}
{mode === 'signup' && (
<TextInput
style={styles.input}
placeholder="Confirm Password"
2025-08-29 22:42:05 +08:00
placeholderTextColor={theme.mutedForeground}
2025-07-02 04:00:56 +08:00
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
autoCapitalize="none"
autoCorrect={false}
/>
)}
<TouchableOpacity
style={[styles.button, loading && styles.buttonDisabled]}
onPress={handleAuth}
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Loading...' : mode === 'signup' ? 'Sign Up' : mode === 'forgot' ? 'Send Reset Link' : 'Sign In'}
</Text>
</TouchableOpacity>
{mode === 'signin' && (
<TouchableOpacity
style={styles.switchButton}
onPress={() => setMode('forgot')}
>
<Text style={styles.switchText}>Forgot Password?</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.switchButton}
onPress={() => setMode(mode === 'signup' ? 'signin' : 'signup')}
>
<Text style={styles.switchText}>
{mode === 'signup'
? 'Already have an account? Sign In'
: mode === 'forgot'
? 'Back to Sign In'
: 'Need an account? Sign Up'}
</Text>
</TouchableOpacity>
<View>
<Text style={styles.legalText}>
By continuing, you agree to our{' '}
<Text
style={styles.legalLink}
onPress={() => Linking.openURL('https://www.suna.so/legal?tab=terms')}
>
Terms of Service
</Text>
{' '}and{' '}
<Text
style={styles.legalLink}
onPress={() => Linking.openURL('https://www.suna.so/legal?tab=privacy')}
>
Privacy Policy
</Text>
</Text>
</View>
</>
)}
2025-08-29 22:42:05 +08:00
</Card>
2025-06-20 05:23:50 +08:00
</KeyboardAvoidingView>
</Modal>
);
};