// apps/mobile/app/login.tsx // 로그인 화면 — 디자인: docs/v3/designs/login.html import { useState } from 'react' import { View, Text, TextInput, StyleSheet, Alert, ScrollView, Pressable, Platform } from 'react-native' import * as WebBrowser from 'expo-web-browser' import * as Linking from 'expo-linking' import { d3roNativePalette, d3roNativeFonts, Led } from '@d3ro/ui-native' import { supabase, isSupabaseConfigured } from '../lib/supabase' import { useAuth } from '../lib/auth-context' import { useRouter } from 'expo-router' WebBrowser.maybeCompleteAuthSession() export default function LoginScreen(): React.ReactElement { const [busy, setBusy] = useState(false) const [email, setEmail] = useState('') const [password, setPassword] = useState('') const configured = isSupabaseConfigured() const { devBypass } = useAuth() const router = useRouter() function handleDevSkip(): void { devBypass() router.replace('/(tabs)/dash') } async function signInWithProvider(provider: 'google' | 'github' | 'apple'): Promise { if (!configured) { Alert.alert('Not Configured', 'Supabase environment variables are not set.') return } setBusy(true) try { const redirectTo = Linking.createURL('auth-callback') const { data, error } = await supabase.auth.signInWithOAuth({ provider, options: { redirectTo, skipBrowserRedirect: true } }) if (error || !data.url) { Alert.alert('Login Failed', error?.message ?? 'Could not get OAuth URL') return } const result = await WebBrowser.openAuthSessionAsync(data.url, redirectTo) if (result.type === 'success' && result.url) { const url = new URL(result.url) const code = url.searchParams.get('code') if (code) { const { error: exchangeErr } = await supabase.auth.exchangeCodeForSession(code) if (exchangeErr) { Alert.alert('Session Error', exchangeErr.message) } } } } finally { setBusy(false) } } async function signInWithEmail(): Promise { if (!configured) { Alert.alert('Not Configured', 'Supabase environment variables are not set.') return } if (!email.trim() || !password.trim()) return setBusy(true) try { const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { Alert.alert('Login Failed', error.message) } } finally { setBusy(false) } } const monoFont = d3roNativeFonts.mono return ( {/* Logo & Branding */} D3RO-VOICE PRECISION VOICE INTELLIGENCE {/* OAuth Buttons */} [styles.oauthBtn, pressed && styles.oauthBtnPressed]} onPress={() => void signInWithProvider('google')} disabled={busy} > G Google로 로그인 [styles.oauthBtn, pressed && styles.oauthBtnPressed]} onPress={() => void signInWithProvider('apple')} disabled={busy} > Apple로 로그인 [styles.oauthBtn, pressed && styles.oauthBtnPressed]} onPress={() => void signInWithProvider('github')} disabled={busy} > GitHub로 로그인 {/* Divider */} 또는 {/* Email/Password Fields */} EMAIL PASSWORD {/* Login Button */} [styles.loginBtn, pressed && { opacity: 0.9 }]} onPress={() => void signInWithEmail()} disabled={busy} > 로그인 {/* Sign Up Link */} 계정이 없으신가요? 회원가입 {/* DEV Skip (개발 전용) */} {__DEV__ && ( ⚡ DEV SKIP LOGIN )} {/* Supabase Warning */} {!configured && ( ⚠ Supabase not configured. Set EXPO_PUBLIC_SUPABASE_URL in .env )} ) } const P = d3roNativePalette const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: P.bg.app }, content: { flexGrow: 1, justifyContent: 'center', paddingHorizontal: 24, paddingBottom: 40, paddingTop: 60 }, // Branding branding: { alignItems: 'center', marginBottom: 40 }, leds: { flexDirection: 'row', gap: 10, marginBottom: 20 }, title: { fontSize: 22, letterSpacing: 5, color: P.text.primary }, subtitle: { fontSize: 9, letterSpacing: 2, color: P.text.muted, marginTop: 8, textTransform: 'uppercase' }, // OAuth oauthSection: { gap: 12, marginBottom: 24 }, oauthBtn: { backgroundColor: P.bg.card, borderWidth: 1, borderColor: P.border.default, borderRadius: 12, paddingVertical: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12 }, oauthBtnPressed: { backgroundColor: P.bg.cardHover, borderColor: 'rgba(113, 113, 122, 0.4)' }, oauthIcon: { fontSize: 16, color: P.text.primary }, oauthLabel: { fontSize: 14, fontWeight: '500', color: P.text.primary }, // Divider divider: { flexDirection: 'row', alignItems: 'center', gap: 16, marginBottom: 24 }, dividerLine: { flex: 1, height: 1, backgroundColor: P.border.default }, dividerText: { fontSize: 10, color: P.text.muted, letterSpacing: 2 }, // Form formSection: { gap: 16, marginBottom: 24 }, fieldGroup: {}, fieldLabel: { fontSize: 10, color: P.text.muted, letterSpacing: 4, marginBottom: 6, marginLeft: 4 }, input: { backgroundColor: P.bg.inset, borderWidth: 1, borderColor: P.border.default, borderRadius: 12, paddingVertical: 14, paddingHorizontal: 16, fontSize: 14, color: P.text.primary, fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto' }, // Login Button loginBtn: { backgroundColor: P.accent.amber, borderRadius: 12, paddingVertical: 14, alignItems: 'center', shadowColor: P.accent.amber, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.3, shadowRadius: 10, elevation: 6, marginBottom: 24 }, loginBtnText: { fontSize: 14, fontWeight: '600', color: P.bg.app, letterSpacing: 1 }, // Sign Up signupRow: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 6, marginBottom: 16 }, signupText: { fontSize: 11, color: P.text.muted }, signupLink: { fontSize: 11, color: P.accent.amber, fontWeight: '500' }, // Warning warningBox: { borderWidth: 1, borderColor: 'rgba(255, 92, 53, 0.3)', backgroundColor: P.accent.amberDim, padding: 12, borderRadius: 8, marginTop: 8 }, warningText: { fontSize: 10, color: P.accent.amber, textAlign: 'center' }, // DEV Skip devSkipBtn: { borderWidth: 1, borderColor: P.accent.green, borderRadius: 8, paddingVertical: 10, alignItems: 'center', marginBottom: 12, backgroundColor: 'rgba(74, 222, 128, 0.08)' }, devSkipText: { fontSize: 10, color: P.accent.green, letterSpacing: 2 } })