Fix Spotify auth, playlist creation, and Expo config
This commit is contained in:
3
app.json
3
app.json
@@ -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
BIN
screenshot_login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 188 KiB |
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user