import { useState, useCallback } from 'react'; import { View, Text, StyleSheet, Pressable, ActivityIndicator, Image, Dimensions } from 'react-native'; import Animated, { FadeIn, FadeInDown, FadeInUp } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import * as WebBrowser from 'expo-web-browser'; import * as Linking from 'expo-linking'; import { router } from 'expo-router'; import * as Haptics from 'expo-haptics'; import { Colors } from '../../constants/theme'; import { buildOAuthUrl, exchangeCode, saveTokens } from '../../services/api'; import { useAuth } from '../../store'; import { saveUser } from '../../hooks/useAuth'; WebBrowser.maybeCompleteAuthSession(); const { width, height } = Dimensions.get('window'); export default function AuthScreen() { const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const { setUser } = useAuth(); const handleLogin = useCallback(async () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {}); setLoading(true); setError(''); try { const state = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2); const url = buildOAuthUrl(state); const result = await WebBrowser.openAuthSessionAsync(url, 'burmalda://auth/callback'); if (result.type !== 'success') { setError('Авторизация отменена'); return; } const parsed = Linking.parse(result.url); const code = parsed.queryParams?.code as string | undefined; if (!code) { setError((parsed.queryParams?.error as string) || 'Ошибка авторизации'); return; } const data = await exchangeCode(code); await saveTokens(data.access_token, data.refresh_token); await saveUser(data.user); setUser(data.user); router.replace('/(tabs)'); } catch (e: any) { setError(e.message || 'Что-то пошло не так'); } finally { setLoading(false); } }, [setUser]); return ( {/* Sleek dark background */} {/* Modern gradient accent for nodeshiftmusic style */} NodeShift Музыкальная экосистема [ s.btn, pressed && s.btnPressed ]} onPress={handleLogin} disabled={loading} > {loading ? ( ) : ( Войти )} {!!error && ( {error} )} Продолжая, вы соглашаетесь с условиями NodeShift ); } const s = StyleSheet.create({ root: { flex: 1 }, content: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24, paddingBottom: 40 }, logoContainer: { width: 140, height: 140, marginBottom: 24, shadowColor: '#4361ee', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.3, shadowRadius: 24, elevation: 10, }, logo: { width: '100%', height: '100%', borderRadius: 35, }, brand: { fontSize: 36, fontWeight: '800', color: '#ffffff', letterSpacing: -1, marginBottom: 8, }, subtitle: { fontSize: 16, color: 'rgba(255, 255, 255, 0.5)', marginBottom: 56, fontWeight: '500', letterSpacing: 0.5, }, btnWrapper: { width: '100%', maxWidth: 320, shadowColor: '#4361ee', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 12, elevation: 5, }, btn: { height: 56, borderRadius: 16, overflow: 'hidden', }, btnGradient: { flex: 1, alignItems: 'center', justifyContent: 'center', flexDirection: 'row', }, btnPressed: { opacity: 0.9, transform: [{ scale: 0.97 }], }, btnText: { color: '#ffffff', fontSize: 17, fontWeight: '700', letterSpacing: 0.3, }, error: { color: '#ff4757', fontSize: 14, textAlign: 'center', marginTop: 20, fontWeight: '500', }, terms: { position: 'absolute', bottom: 40, color: 'rgba(255, 255, 255, 0.3)', fontSize: 12, textAlign: 'center', }, });