Fix Spotify auth, playlist creation, and Expo config

This commit is contained in:
Eduardo Silva
2026-05-21 11:58:25 +01:00
parent 9222d3a483
commit a587b3a1bd
8 changed files with 377 additions and 136 deletions

View File

@@ -14,7 +14,8 @@
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true,
"bundleIdentifier": "com.epvc.roadtripdj"
}, },
"android": { "android": {
"package": "com.eduardo12345122.roadtripdj", "package": "com.eduardo12345122.roadtripdj",

BIN
screenshot_login.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

View File

@@ -45,3 +45,52 @@ export async function clearSpotifyTokens() {
console.error('Error clearing Spotify tokens:', error); console.error('Error clearing Spotify tokens:', error);
} }
} }
export async function refreshSpotifyToken() {
const refreshToken = await getSpotifyRefreshToken();
if (!refreshToken) {
console.log('[SpotifyToken] No refresh token found, cannot refresh.');
return null;
}
try {
console.log('[SpotifyToken] Refreshing Spotify access token...');
const SPOTIFY_CLIENT_ID = "7fa1e7acbf7e44f18bf28d74f14fb9cb";
// Construct urlencoded body safely
const bodyDetails = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: SPOTIFY_CLIENT_ID,
};
const formBody = Object.keys(bodyDetails)
.map(key => encodeURIComponent(key) + '=' + encodeURIComponent(bodyDetails[key as keyof typeof bodyDetails]))
.join('&');
const res = await fetch('https://accounts.spotify.com/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formBody,
});
if (res.ok) {
const data = await res.json();
if (data.access_token) {
console.log('[SpotifyToken] Spotify access token refreshed successfully!');
await setSpotifyToken(data.access_token);
if (data.refresh_token) {
await setSpotifyRefreshToken(data.refresh_token);
}
return data.access_token;
}
} else {
const errText = await res.text();
console.error('[SpotifyToken] Refresh request failed:', res.status, errText);
}
} catch (error) {
console.error('[SpotifyToken] Error refreshing Spotify token:', error);
}
return null;
}

View File

@@ -48,12 +48,28 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
supabase.auth.getSession().then(({ data: { session } }) => { supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
if (!session) {
setIsDemoMode(false);
setIsSpotifyAuthenticated(false);
} else {
const isSpotify = !!session.user?.user_metadata?.spotify_id;
setIsSpotifyAuthenticated(isSpotify);
setIsDemoMode(false);
}
setLoading(false); setLoading(false);
}); });
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session); setSession(session);
setUser(session?.user ?? null); setUser(session?.user ?? null);
if (!session) {
setIsDemoMode(false);
setIsSpotifyAuthenticated(false);
} else {
const isSpotify = !!session.user?.user_metadata?.spotify_id;
setIsSpotifyAuthenticated(isSpotify);
setIsDemoMode(false);
}
setLoading(false); setLoading(false);
}); });

View File

@@ -4,7 +4,6 @@ import { Car, Music } from 'lucide-react-native';
import { colors } from '../../utils/colors'; import { colors } from '../../utils/colors';
import { supabase } from '../../services/supabase'; import { supabase } from '../../services/supabase';
import * as WebBrowser from 'expo-web-browser'; import * as WebBrowser from 'expo-web-browser';
import * as Linking from 'expo-linking';
import { makeRedirectUri, useAuthRequest, exchangeCodeAsync, DiscoveryDocument } from 'expo-auth-session'; import { makeRedirectUri, useAuthRequest, exchangeCodeAsync, DiscoveryDocument } from 'expo-auth-session';
import { clearSpotifyTokens, setSpotifyToken, setSpotifyRefreshToken } from '../../auth/spotifyToken'; import { clearSpotifyTokens, setSpotifyToken, setSpotifyRefreshToken } from '../../auth/spotifyToken';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
@@ -24,11 +23,14 @@ export default function LoginScreen({ navigation }) {
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Temporary Emergency Fix: Hardcoded Client ID // Direct Spotify App Client ID
const SPOTIFY_CLIENT_ID = "7fa1e7acbf7e44f18bf28d74f14fb9cb"; const SPOTIFY_CLIENT_ID = "7fa1e7acbf7e44f18bf28d74f14fb9cb";
// Configure Direct Spotify OAuth // Configure Dynamic Redirect URI
const redirectUri = "exp://192.168.1.7:8081/--/auth/callback"; const redirectUri = makeRedirectUri({
scheme: 'roadtripdj',
path: 'auth/callback',
});
const [request, response, promptAsync] = useAuthRequest( const [request, response, promptAsync] = useAuthRequest(
{ {
@@ -79,69 +81,88 @@ export default function LoginScreen({ navigation }) {
await setSpotifyRefreshToken(tokenResult.refreshToken); await setSpotifyRefreshToken(tokenResult.refreshToken);
} }
// 1 & 2. Check for Supabase session or create an anonymous one // Fetch real Spotify Profile
const { data: sessionData } = await supabase.auth.getSession();
let supabaseUserId = sessionData?.session?.user?.id;
console.log("SUPABASE_SESSION_EXISTS:", !!sessionData?.session);
if (!sessionData?.session) {
const { data, error } = await supabase.auth.signInAnonymously();
if (error) {
console.error("Error creating anonymous session:", error);
throw error;
}
supabaseUserId = data.user?.id;
}
console.log("SUPABASE_USER_ID:", supabaseUserId);
console.log("TRIP_INSERT_USER_ID:", supabaseUserId);
// 7 & 8. Fetch Spotify Profile and update Supabase User Metadata
try {
console.log("SPOTIFY_PROFILE_FETCH_START"); console.log("SPOTIFY_PROFILE_FETCH_START");
const profileRes = await fetch('https://api.spotify.com/v1/me', { const profileRes = await fetch('https://api.spotify.com/v1/me', {
headers: { Authorization: `Bearer ${tokenResult.accessToken}` } headers: { Authorization: `Bearer ${tokenResult.accessToken}` }
}); });
if (profileRes.ok) { if (!profileRes.ok) {
throw new Error(`Failed to fetch Spotify profile: ${profileRes.statusText}`);
}
const profile = await profileRes.json(); const profile = await profileRes.json();
console.log("SPOTIFY_PROFILE_NAME:", profile.display_name); console.log("SPOTIFY_PROFILE_NAME:", profile.display_name);
console.log("SPOTIFY_PROFILE_EMAIL_EXISTS:", !!profile.email); console.log("SPOTIFY_PROFILE_EMAIL_EXISTS:", !!profile.email);
console.log("SPOTIFY_USER_ID:", profile.id); console.log("SPOTIFY_USER_ID:", profile.id);
console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!tokenResult.accessToken); console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!tokenResult.accessToken);
// Force session refresh to ensure AuthContext picks up the new metadata instantly const email = profile.email || `spotify_${profile.id}@roadtripdj.local`;
const { data: updatedUser } = await supabase.auth.updateUser({ const password = `SpotifySecure_${profile.id}`;
// Attempt sign in or automatic registration
let { data: sessionData, error: signInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (signInError) {
console.log("Spotify user does not exist or login failed, signing up...", signInError.message);
// Sign up user with Spotify metadata
const { error: signUpError } = await supabase.auth.signUp({
email,
password,
options: {
data: { data: {
display_name: profile.display_name, display_name: profile.display_name || 'Viajante',
name: profile.display_name, name: profile.display_name || 'Viajante',
spotify_id: profile.id, spotify_id: profile.id,
email: profile.email, email: profile.email || null,
avatar_url: profile.images?.[0]?.url avatar_url: profile.images?.[0]?.url || null,
}
} }
}); });
// Explicitly refresh session if needed if (signUpError) {
if (updatedUser) { console.error("SignUp error:", signUpError);
throw signUpError;
}
// Log in again after sign up
const { data: newSession, error: newSignInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (newSignInError) throw newSignInError;
sessionData = newSession;
} else {
// Update metadata with latest Spotify data
await supabase.auth.updateUser({
data: {
display_name: profile.display_name || 'Viajante',
name: profile.display_name || 'Viajante',
spotify_id: profile.id,
email: profile.email || null,
avatar_url: profile.images?.[0]?.url || null,
}
});
}
// Force session refresh for app state
await supabase.auth.refreshSession(); await supabase.auth.refreshSession();
}
}
} catch (profileError) {
console.error("Error fetching Spotify profile:", profileError);
}
console.log("LOGIN_MODE:", "spotify"); console.log("LOGIN_MODE:", "spotify");
console.log("ENTERING_SPOTIFY_MODE"); console.log("ENTERING_SPOTIFY_MODE");
// Trigger app main flow as Spotify authenticated user // Trigger app main flow
enableSpotifyMode(); enableSpotifyMode();
} else { } else {
Alert.alert('Erro de Autenticação', 'Não foi possível obter o token de acesso do Spotify.'); Alert.alert('Erro de Autenticação', 'Não foi possível obter o token de acesso do Spotify.');
} }
} catch (e: any) { } catch (e: any) {
console.error('🚀 [LoginScreen] Token Exchange Error:', e); console.error('🚀 [LoginScreen] Token Exchange Error:', e);
Alert.alert('Erro de Autenticação', 'Não foi possível trocar o código pelo token.'); Alert.alert('Erro de Autenticação', e.message || 'Não foi possível trocar o código pelo token.');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -165,10 +186,6 @@ export default function LoginScreen({ navigation }) {
setLoading(false); setLoading(false);
}; };
const handleAuthDebug = async () => {
// Placeholder function for debug logic
};
const handleResetAuth = async () => { const handleResetAuth = async () => {
await supabase.auth.signOut(); await supabase.auth.signOut();
await clearSpotifyTokens(); await clearSpotifyTokens();
@@ -178,23 +195,7 @@ export default function LoginScreen({ navigation }) {
const handleSpotifyLogin = async () => { const handleSpotifyLogin = async () => {
try { try {
console.log("SPOTIFY_CLIENT_ID_EXISTS:", !!SPOTIFY_CLIENT_ID); console.log("SPOTIFY_CLIENT_ID_EXISTS:", !!SPOTIFY_CLIENT_ID);
console.log("SPOTIFY_CLIENT_ID_LENGTH:", SPOTIFY_CLIENT_ID.length);
console.log("SPOTIFY_CLIENT_ID_FIRST_6:", SPOTIFY_CLIENT_ID.slice(0, 6));
console.log("SPOTIFY_REDIRECT_URI:", redirectUri); console.log("SPOTIFY_REDIRECT_URI:", redirectUri);
console.log("FULL_SPOTIFY_AUTH_URL:", request?.url);
if (
!SPOTIFY_CLIENT_ID ||
SPOTIFY_CLIENT_ID.includes("PASTE_") ||
SPOTIFY_CLIENT_ID.includes("HERE")
) {
Alert.alert(
'Aviso',
'Cole o Client ID real no arquivo LoginScreen.tsx.'
);
return;
}
await promptAsync(); await promptAsync();
} catch (e: any) { } catch (e: any) {
console.error('🚀 [LoginScreen] OAuth Error:', e); console.error('🚀 [LoginScreen] OAuth Error:', e);
@@ -205,10 +206,13 @@ export default function LoginScreen({ navigation }) {
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={styles.keyboardView} style={styles.keyboardView}
> >
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Header Section */} {/* Header Section */}
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<View style={styles.iconWrapper}> <View style={styles.iconWrapper}>
@@ -265,9 +269,6 @@ export default function LoginScreen({ navigation }) {
style={[styles.primaryButton, { backgroundColor: '#333', marginBottom: 24 }]} style={[styles.primaryButton, { backgroundColor: '#333', marginBottom: 24 }]}
onPress={() => { onPress={() => {
console.log("DEMO_BYPASS_PRESSED"); console.log("DEMO_BYPASS_PRESSED");
console.log("LOGIN_MODE:", "demo");
console.log("ENTERING_DEMO_MODE");
console.log("AVAILABLE_ROUTE_NAMES", navigation.getState()?.routeNames);
enableDemoMode(); enableDemoMode();
}} }}
> >
@@ -275,12 +276,8 @@ export default function LoginScreen({ navigation }) {
</TouchableOpacity> </TouchableOpacity>
)} )}
<TouchableOpacity style={{ padding: 10, alignItems: 'center' }} onPress={handleAuthDebug}>
<Text style={{ color: colors.textSecondary }}>Auth Debug</Text>
</TouchableOpacity>
<TouchableOpacity style={{ padding: 10, alignItems: 'center', marginBottom: 10 }} onPress={handleResetAuth}> <TouchableOpacity style={{ padding: 10, alignItems: 'center', marginBottom: 10 }} onPress={handleResetAuth}>
<Text style={{ color: 'red' }}>Reset Auth</Text> <Text style={{ color: 'red', fontWeight: '600' }}>Reset Auth</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.footerContainer}> <View style={styles.footerContainer}>
@@ -306,15 +303,13 @@ const styles = StyleSheet.create({
}, },
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
justifyContent: 'center', justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
paddingTop: 60, paddingTop: 60,
paddingBottom: 40,
}, },
headerContainer: { headerContainer: {
alignItems: 'center', alignItems: 'center',
marginBottom: 40, marginBottom: 30,
paddingHorizontal: 20,
}, },
iconWrapper: { iconWrapper: {
position: 'relative', position: 'relative',
@@ -357,10 +352,16 @@ const styles = StyleSheet.create({
card: { card: {
backgroundColor: colors.white, backgroundColor: colors.white,
width: '100%', width: '100%',
borderRadius: 24, borderTopLeftRadius: 32,
borderTopRightRadius: 32,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
padding: 24, padding: 24,
paddingBottom: Platform.OS === 'ios' ? 40 : 24,
flexGrow: 1,
justifyContent: 'center',
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 12, shadowRadius: 12,
elevation: 5, elevation: 5,
@@ -407,6 +408,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginTop: 10,
}, },
footerText: { footerText: {
color: colors.textSecondary, color: colors.textSecondary,

View File

@@ -1,21 +1,171 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert, ActivityIndicator } from 'react-native'; import { View, Text, TextInput, TouchableOpacity, StyleSheet, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert, ActivityIndicator } from 'react-native';
import { Car, Music } from 'lucide-react-native'; import { Car, Music } from 'lucide-react-native';
import { colors } from '../../utils/colors'; import { colors } from '../../utils/colors';
import { supabase } from '../../services/supabase'; import { supabase } from '../../services/supabase';
import { makeRedirectUri } from 'expo-auth-session';
import * as WebBrowser from 'expo-web-browser'; import * as WebBrowser from 'expo-web-browser';
import * as QueryParams from 'expo-auth-session/build/QueryParams'; import { makeRedirectUri, useAuthRequest, exchangeCodeAsync, DiscoveryDocument } from 'expo-auth-session';
import { clearSpotifyTokens, setSpotifyToken, setSpotifyRefreshToken } from '../../auth/spotifyToken';
import { useAuth } from '../../contexts/AuthContext';
WebBrowser.maybeCompleteAuthSession(); WebBrowser.maybeCompleteAuthSession();
// Direct Spotify OAuth Endpoints
const discovery: DiscoveryDocument = {
authorizationEndpoint: 'https://accounts.spotify.com/authorize',
tokenEndpoint: 'https://accounts.spotify.com/api/token',
};
// @ts-ignore // @ts-ignore
export default function RegisterScreen({ navigation }) { export default function RegisterScreen({ navigation }) {
const { enableSpotifyMode } = useAuth();
const [name, setName] = useState(''); const [name, setName] = useState('');
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
// Direct Spotify App Client ID
const SPOTIFY_CLIENT_ID = "7fa1e7acbf7e44f18bf28d74f14fb9cb";
// Configure Dynamic Redirect URI
const redirectUri = makeRedirectUri({
scheme: 'roadtripdj',
path: 'auth/callback',
});
const [request, response, promptAsync] = useAuthRequest(
{
clientId: SPOTIFY_CLIENT_ID,
scopes: ['user-read-email', 'user-read-private', 'playlist-modify-public', 'playlist-modify-private'],
usePKCE: true,
redirectUri,
},
discovery
);
// Handle Spotify OAuth Response
useEffect(() => {
if (response) {
console.log("SPOTIFY_AUTH_RESULT (Register):", response);
if (response.type === 'success') {
const { code } = response.params;
console.log("SPOTIFY_CODE_EXISTS (Register):", !!code);
exchangeCodeForTokens(code);
} else if (response.type === 'error') {
Alert.alert('Erro de Autenticação', response.error?.message || 'Falha ao logar com o Spotify');
}
}
}, [response]);
const exchangeCodeForTokens = async (code: string) => {
if (!request?.codeVerifier) return;
try {
setLoading(true);
const tokenResult = await exchangeCodeAsync(
{
clientId: SPOTIFY_CLIENT_ID,
code,
redirectUri,
extraParams: {
code_verifier: request.codeVerifier,
},
},
discovery
);
console.log("SPOTIFY_TOKEN_RECEIVED (Register):", !!tokenResult.accessToken);
if (tokenResult.accessToken) {
await setSpotifyToken(tokenResult.accessToken);
if (tokenResult.refreshToken) {
await setSpotifyRefreshToken(tokenResult.refreshToken);
}
// Fetch Spotify Profile
console.log("SPOTIFY_PROFILE_FETCH_START (Register)");
const profileRes = await fetch('https://api.spotify.com/v1/me', {
headers: { Authorization: `Bearer ${tokenResult.accessToken}` }
});
if (!profileRes.ok) {
throw new Error(`Failed to fetch Spotify profile: ${profileRes.statusText}`);
}
const profile = await profileRes.json();
console.log("SPOTIFY_PROFILE_NAME (Register):", profile.display_name);
console.log("SPOTIFY_PROFILE_EMAIL_EXISTS (Register):", !!profile.email);
console.log("SPOTIFY_USER_ID (Register):", profile.id);
const email = profile.email || `spotify_${profile.id}@roadtripdj.local`;
const password = `SpotifySecure_${profile.id}`;
// Attempt sign in or automatic registration
let { data: sessionData, error: signInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (signInError) {
console.log("Spotify user does not exist or login failed, signing up (Register)...", signInError.message);
// Sign up user with Spotify metadata
const { error: signUpError } = await supabase.auth.signUp({
email,
password,
options: {
data: {
display_name: profile.display_name || 'Viajante',
name: profile.display_name || 'Viajante',
spotify_id: profile.id,
email: profile.email || null,
avatar_url: profile.images?.[0]?.url || null,
}
}
});
if (signUpError) {
console.error("SignUp error:", signUpError);
throw signUpError;
}
// Log in again after sign up
const { data: newSession, error: newSignInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (newSignInError) throw newSignInError;
sessionData = newSession;
} else {
// Update metadata with latest Spotify data
await supabase.auth.updateUser({
data: {
display_name: profile.display_name || 'Viajante',
name: profile.display_name || 'Viajante',
spotify_id: profile.id,
email: profile.email || null,
avatar_url: profile.images?.[0]?.url || null,
}
});
}
// Force session refresh for app state
await supabase.auth.refreshSession();
// Trigger app main flow
enableSpotifyMode();
} else {
Alert.alert('Erro de Autenticação', 'Não foi possível obter o token de acesso do Spotify.');
}
} catch (e: any) {
console.error('🚀 [RegisterScreen] Token Exchange Error:', e);
Alert.alert('Erro de Autenticação', e.message || 'Não foi possível trocar o código pelo token.');
} finally {
setLoading(false);
}
};
const handleRegister = async () => { const handleRegister = async () => {
if (!name || !email || !password || !confirmPassword) { if (!name || !email || !password || !confirmPassword) {
Alert.alert('Erro', 'Por favor preenche todos os campos.'); Alert.alert('Erro', 'Por favor preenche todos os campos.');
@@ -39,48 +189,34 @@ export default function RegisterScreen({ navigation }) {
if (error) { if (error) {
Alert.alert('Erro no registo', error.message); Alert.alert('Erro no registo', error.message);
} else {
Alert.alert('Sucesso', 'Conta criada com sucesso! Faça login.');
navigation.navigate('Login');
} }
setLoading(false); setLoading(false);
}; };
const handleSpotifyAuth = async () => { const handleSpotifyAuth = async () => {
try { try {
const redirectUri = makeRedirectUri(); console.log("SPOTIFY_CLIENT_ID_EXISTS (Register):", !!SPOTIFY_CLIENT_ID);
const { data, error } = await supabase.auth.signInWithOAuth({ console.log("SPOTIFY_REDIRECT_URI (Register):", redirectUri);
provider: 'spotify', await promptAsync();
options: { } catch (e: any) {
redirectTo: redirectUri, console.error('🚀 [RegisterScreen] OAuth Error:', e);
}, Alert.alert('Erro de Autenticação', e.message);
});
if (error) throw error;
if (data?.url) {
const res = await WebBrowser.openAuthSessionAsync(data.url, redirectUri);
if (res.type === 'success') {
const { url } = res;
const { params, errorCode } = QueryParams.getQueryParams(url);
if (errorCode) throw new Error(errorCode);
if (params.access_token && params.refresh_token) {
await supabase.auth.setSession({
access_token: params.access_token,
refresh_token: params.refresh_token,
});
}
}
}
} catch (err: any) {
Alert.alert('Erro', err?.message || 'Ocorreu um erro no Spotify Auth.');
} }
}; };
return ( return (
<SafeAreaView style={styles.container}> <SafeAreaView style={styles.container}>
<KeyboardAvoidingView <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={styles.keyboardView} style={styles.keyboardView}
> >
<ScrollView contentContainerStyle={styles.scrollContent}> <ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Header Section */} {/* Header Section */}
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<View style={styles.iconWrapper}> <View style={styles.iconWrapper}>
@@ -144,9 +280,8 @@ export default function RegisterScreen({ navigation }) {
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyAuth}> <TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyAuth}>
{/* Note: Placeholder Spotify logo */}
<Music color={colors.white} size={20} style={styles.spotifyIcon} /> <Music color={colors.white} size={20} style={styles.spotifyIcon} />
<Text style={styles.spotifyButtonText}>Registar com Spotify</Text> <Text style={styles.spotifyButtonText}>Criar conta com Spotify</Text>
</TouchableOpacity> </TouchableOpacity>
<View style={styles.footerContainer}> <View style={styles.footerContainer}>
@@ -172,15 +307,13 @@ const styles = StyleSheet.create({
}, },
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
justifyContent: 'center', justifyContent: 'space-between',
alignItems: 'center', paddingTop: 60,
paddingHorizontal: 20,
paddingTop: 40,
paddingBottom: 40,
}, },
headerContainer: { headerContainer: {
alignItems: 'center', alignItems: 'center',
marginBottom: 30, marginBottom: 30,
paddingHorizontal: 20,
}, },
iconWrapper: { iconWrapper: {
position: 'relative', position: 'relative',
@@ -223,10 +356,16 @@ const styles = StyleSheet.create({
card: { card: {
backgroundColor: colors.white, backgroundColor: colors.white,
width: '100%', width: '100%',
borderRadius: 24, borderTopLeftRadius: 32,
borderTopRightRadius: 32,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
padding: 24, padding: 24,
paddingBottom: Platform.OS === 'ios' ? 40 : 24,
flexGrow: 1,
justifyContent: 'center',
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.1, shadowOpacity: 0.1,
shadowRadius: 12, shadowRadius: 12,
elevation: 5, elevation: 5,
@@ -273,6 +412,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginTop: 10,
}, },
footerText: { footerText: {
color: colors.textSecondary, color: colors.textSecondary,

View File

@@ -4,7 +4,8 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { X, MapPin, ArrowRight, Navigation } from 'lucide-react-native'; import { X, MapPin, ArrowRight, Navigation } from 'lucide-react-native';
import { colors } from '../../utils/colors'; import { colors } from '../../utils/colors';
import { supabase } from '../../services/supabase'; import { supabase } from '../../services/supabase';
import { getSpotifyAccessToken } from '../../auth/spotifyToken'; import { getSpotifyAccessToken, refreshSpotifyToken } from '../../auth/spotifyToken';
import { OLLAMA_API_URL } from '../../services/ollama';
// @ts-ignore // @ts-ignore
export default function NewTripScreen({ navigation }) { export default function NewTripScreen({ navigation }) {
@@ -56,20 +57,52 @@ export default function NewTripScreen({ navigation }) {
console.log(`PLAYLIST_API_CONTENT_TYPE [${label}]:`, res.headers.get("content-type")); console.log(`PLAYLIST_API_CONTENT_TYPE [${label}]:`, res.headers.get("content-type"));
console.log(`PLAYLIST_API_RAW_RESPONSE [${label}]:`, rawText.substring(0, 300) + (rawText.length > 300 ? "..." : "")); console.log(`PLAYLIST_API_RAW_RESPONSE [${label}]:`, rawText.substring(0, 300) + (rawText.length > 300 ? "..." : ""));
if (!res.ok) {
throw new Error(`Spotify API returned status ${res.status} for [${label}]: ${rawText.substring(0, 150)}`);
}
const contentType = res.headers.get("content-type") || "";
if (!contentType.includes("application/json")) {
throw new Error(`Playlist API returned non-JSON response for [${label}]: ${rawText.substring(0, 150)}`);
}
try { try {
return JSON.parse(rawText); return JSON.parse(rawText);
} catch (e) { } catch (e) {
throw new Error(`Playlist API returned non-JSON response [${label}]: ${rawText}`); throw new Error(`Failed to parse JSON response for [${label}]: ${rawText.substring(0, 150)}`);
} }
}; };
// A. Get provider token // A. Get provider token
const providerToken = await getSpotifyAccessToken(); let providerToken = await getSpotifyAccessToken();
console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken); console.log("SPOTIFY_ACCESS_TOKEN_EXISTS:", !!providerToken);
if (providerToken) {
// Proactively check if token is valid, or refresh it
console.log("Validating Spotify token...");
let testRes = await fetch('https://api.spotify.com/v1/me', {
headers: { Authorization: `Bearer ${providerToken}` }
});
if (testRes.status === 401) {
console.log("Spotify token is invalid/expired (401), attempting to refresh...");
const newToken = await refreshSpotifyToken();
if (newToken) {
providerToken = newToken;
} else {
console.log("Failed to refresh Spotify token.");
providerToken = null;
}
} else if (!testRes.ok) {
const testErr = await testRes.text();
console.error("Spotify validation request failed:", testRes.status, testErr);
providerToken = null;
}
}
if (!providerToken) { if (!providerToken) {
console.log("Spotify token missing, skipping playlist generation."); console.log("Spotify token missing or expired, skipping playlist generation.");
Alert.alert('Aviso', 'Spotify token missing, please login again'); Alert.alert('Spotify Desligado', 'O token do Spotify expirou ou está em falta. Por favor reconecte o Spotify no Perfil.');
} else { } else {
// B. Fetch Spotify User ID // B. Fetch Spotify User ID
const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', { const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', {
@@ -83,7 +116,7 @@ export default function NewTripScreen({ navigation }) {
const spotifyUserId = spotifyUserData.id; const spotifyUserId = spotifyUserData.id;
// C. Call Ollama server // C. Call Ollama server
const ollamaRes = await fetch("http://89.114.196.110:11434/api/chat", { const ollamaRes = await fetch(`${OLLAMA_API_URL}/api/chat`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({

View File

@@ -1,5 +1,5 @@
// Placeholder for Ollama API logic // Placeholder for Ollama API logic
export const OLLAMA_API_URL = "https://apichat.epvc.pt/"; export const OLLAMA_API_URL = "https://apichat.epvc.pt";
export const generateTripGuide = async (origin: string, destination: string, waypoints: string[], duration: string) => { export const generateTripGuide = async (origin: string, destination: string, waypoints: string[], duration: string) => {
// Logic to call Ollama // Logic to call Ollama