Fix Supabase OAuth flow and various UI updates
This commit is contained in:
25
App.tsx
25
App.tsx
@@ -1,11 +1,34 @@
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import AppNavigator from './src/navigation/AppNavigator';
|
import AppNavigator from './src/navigation/AppNavigator';
|
||||||
import { AuthProvider } from './src/contexts/AuthContext';
|
import { AuthProvider } from './src/contexts/AuthContext';
|
||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
||||||
|
import { supabase } from './src/services/supabase';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
const catchTokenOnLoad = async () => {
|
||||||
|
const url = await Linking.getInitialURL();
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
// Supabase sends tokens after a '#' which Expo might ignore, replace it with '?'
|
||||||
|
const cleanUrl = url.replace('#', '?');
|
||||||
|
const { params, errorCode } = QueryParams.getQueryParams(cleanUrl);
|
||||||
|
|
||||||
|
if (params?.access_token) {
|
||||||
|
console.log('🔥 TOKEN APANHADO NO ARRANQUE!');
|
||||||
|
await supabase.auth.setSession({
|
||||||
|
access_token: params.access_token,
|
||||||
|
refresh_token: params.refresh_token || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
catchTokenOnLoad();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
1
app.json
1
app.json
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "roadtrip-dj",
|
"name": "roadtrip-dj",
|
||||||
|
"scheme": "roadtripdj",
|
||||||
"slug": "roadtrip-dj",
|
"slug": "roadtrip-dj",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
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 { makeRedirectUri } from 'expo-auth-session';
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default function LoginScreen({ navigation }) {
|
export default function LoginScreen({ navigation }) {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
Alert.alert('Erro', 'Por favor preenche todos os campos.');
|
Alert.alert('Erro', 'Por favor preenche todos os campos.');
|
||||||
@@ -31,28 +33,14 @@ export default function LoginScreen({ navigation }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSpotifyAuth = async () => {
|
const handleSpotifyLogin = async () => {
|
||||||
try {
|
const redirectUrl = Linking.createURL('/');
|
||||||
const redirectUri = makeRedirectUri();
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
provider: 'spotify',
|
||||||
provider: 'spotify',
|
options: { redirectTo: redirectUrl }
|
||||||
options: {
|
});
|
||||||
redirectTo: redirectUri,
|
if (data?.url) {
|
||||||
},
|
await Linking.openURL(data.url);
|
||||||
});
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
if (data?.url) {
|
|
||||||
const res = await WebBrowser.openAuthSessionAsync(data.url, redirectUri);
|
|
||||||
if (res.type === 'success') {
|
|
||||||
const { url } = res;
|
|
||||||
// Ensure Supabase catches the session from the returned URL
|
|
||||||
await supabase.auth.getSessionFromUrl(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
Alert.alert('Erro', err?.message || 'Ocorreu um erro no Spotify Auth.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,7 +97,7 @@ export default function LoginScreen({ navigation }) {
|
|||||||
)}
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyAuth}>
|
<TouchableOpacity style={styles.spotifyButton} onPress={handleSpotifyLogin}>
|
||||||
{/* Note: In a real app we would use an actual Spotify SVG logo. Using Music icon for now as a placeholder for the Spotify logo. */}
|
{/* Note: In a real app we would use an actual Spotify SVG logo. Using Music icon for now as a placeholder for the Spotify logo. */}
|
||||||
<Music color={colors.white} size={20} style={styles.spotifyIcon} />
|
<Music color={colors.white} size={20} style={styles.spotifyIcon} />
|
||||||
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { colors } from '../../utils/colors';
|
|||||||
import { supabase } from '../../services/supabase';
|
import { supabase } from '../../services/supabase';
|
||||||
import { makeRedirectUri } from 'expo-auth-session';
|
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';
|
||||||
|
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -58,8 +59,14 @@ export default function RegisterScreen({ navigation }) {
|
|||||||
const res = await WebBrowser.openAuthSessionAsync(data.url, redirectUri);
|
const res = await WebBrowser.openAuthSessionAsync(data.url, redirectUri);
|
||||||
if (res.type === 'success') {
|
if (res.type === 'success') {
|
||||||
const { url } = res;
|
const { url } = res;
|
||||||
// Ensure Supabase catches the session from the returned URL
|
const { params, errorCode } = QueryParams.getQueryParams(url);
|
||||||
await supabase.auth.getSessionFromUrl(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) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,15 +1,179 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, TextInput, ImageBackground, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
|
import { View, Text, TextInput, TouchableOpacity, StyleSheet, KeyboardAvoidingView, ScrollView, Platform, ActivityIndicator, Alert, Linking, StatusBar } from 'react-native';
|
||||||
import { X, MapPin, ArrowRight } from 'lucide-react-native';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { X, MapPin, ArrowRight, Navigation } from 'lucide-react-native';
|
||||||
import { colors } from '../../utils/colors';
|
import { colors } from '../../utils/colors';
|
||||||
|
import { supabase } from '../../services/supabase';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default function NewTripScreen({ navigation }) {
|
export default function NewTripScreen({ navigation }) {
|
||||||
|
const [tripName, setTripName] = useState('');
|
||||||
|
const [origin, setOrigin] = useState('');
|
||||||
|
const [destination, setDestination] = useState('');
|
||||||
|
const [distance, setDistance] = useState('');
|
||||||
|
const [duration, setDuration] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleCreateRoute = () => {
|
const handleCalculateTrip = async () => {
|
||||||
// In a real app, this would trigger the orchestration flow (Ollama -> Spotify -> Firebase)
|
if (!origin || !destination) {
|
||||||
// For now, we mock success and navigate to the Trip Details
|
Alert.alert('Erro', 'Por favor preenche ambos os campos de partida e destino.');
|
||||||
navigation.replace('TripDetails');
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tripName) {
|
||||||
|
Alert.alert('Erro', 'Por favor dá um nome à tua viagem.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const apiKey = 'AIzaSyDocu-PEHAyrdV8OUEPMXye9A_rpYzOA34';
|
||||||
|
const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}&key=${apiKey}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.status === 'OK') {
|
||||||
|
const leg = data.routes[0].legs[0];
|
||||||
|
const finalDistance = leg.distance.text;
|
||||||
|
const finalDuration = leg.duration.text;
|
||||||
|
const tripDurationMs = leg.duration.value * 1000;
|
||||||
|
|
||||||
|
setDistance(finalDistance);
|
||||||
|
setDuration(finalDuration);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A. Get provider token
|
||||||
|
const { data: sessionData, error: sessionError } = await supabase.auth.getSession();
|
||||||
|
if (sessionError || !sessionData?.session) throw new Error('Session not found.');
|
||||||
|
const providerToken = sessionData.session.provider_token;
|
||||||
|
if (!providerToken) throw new Error('Spotify provider token missing.');
|
||||||
|
|
||||||
|
// B. Fetch Spotify User ID
|
||||||
|
const spotifyUserRes = await fetch('https://api.spotify.com/v1/me', {
|
||||||
|
headers: { 'Authorization': `Bearer ${providerToken}` }
|
||||||
|
});
|
||||||
|
const spotifyUserData = await spotifyUserRes.json();
|
||||||
|
if (!spotifyUserData.id) throw new Error('Could not fetch Spotify User ID');
|
||||||
|
const spotifyUserId = spotifyUserData.id;
|
||||||
|
|
||||||
|
// C. Call Ollama server
|
||||||
|
const ollamaRes = await fetch("http://89.114.196.110:11434/api/chat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: "qwen3-coder:30b",
|
||||||
|
messages: [{ "role": "user", "content": `I am taking a roadtrip from ${origin} to ${destination}. Reply ONLY with 3 Spotify genre seeds separated by commas (e.g., pop,rock,indie) that fit this journey. No other text.` }],
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const ollamaData = await ollamaRes.json();
|
||||||
|
const seed_genres = ollamaData.message.content.trim().replace(/\s+/g, '').toLowerCase();
|
||||||
|
|
||||||
|
// D. Create empty playlist
|
||||||
|
const createPlaylistRes = await fetch(`https://api.spotify.com/v1/users/${spotifyUserId}/playlists`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${providerToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: tripName,
|
||||||
|
description: `Roadtrip from ${origin} to ${destination}. Genres: ${seed_genres}`,
|
||||||
|
public: false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const playlistData = await createPlaylistRes.json();
|
||||||
|
if (!playlistData.id) throw new Error('Could not create playlist');
|
||||||
|
const playlistId = playlistData.id;
|
||||||
|
const generatedPlaylistUrl = playlistData.external_urls.spotify;
|
||||||
|
|
||||||
|
// E. Fetch Spotify track recommendations
|
||||||
|
let accumulatedDurationMs = 0;
|
||||||
|
let trackUris: string[] = [];
|
||||||
|
let attempts = 0;
|
||||||
|
while (accumulatedDurationMs < tripDurationMs && attempts < 10) {
|
||||||
|
const recommendationsRes = await fetch(`https://api.spotify.com/v1/recommendations?seed_genres=${encodeURIComponent(seed_genres)}&limit=50`, {
|
||||||
|
headers: { 'Authorization': `Bearer ${providerToken}` }
|
||||||
|
});
|
||||||
|
if (!recommendationsRes.ok) break;
|
||||||
|
const recommendationsData = await recommendationsRes.json();
|
||||||
|
if (!recommendationsData.tracks || recommendationsData.tracks.length === 0) break;
|
||||||
|
|
||||||
|
for (const track of recommendationsData.tracks) {
|
||||||
|
if (!trackUris.includes(track.uri)) {
|
||||||
|
trackUris.push(track.uri);
|
||||||
|
accumulatedDurationMs += track.duration_ms;
|
||||||
|
if (accumulatedDurationMs >= tripDurationMs) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackUris.length > 0) {
|
||||||
|
// F. Add tracks to playlist
|
||||||
|
const chunkSize = 100;
|
||||||
|
for (let i = 0; i < trackUris.length; i += chunkSize) {
|
||||||
|
const chunk = trackUris.slice(i, i + chunkSize);
|
||||||
|
await fetch(`https://api.spotify.com/v1/playlists/${playlistId}/tracks`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${providerToken}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ uris: chunk })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No tracks found for genres:", seed_genres);
|
||||||
|
}
|
||||||
|
|
||||||
|
// G. Save to Supabase
|
||||||
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
|
if (user) {
|
||||||
|
const { error: dbError } = await supabase.from('trips').insert({
|
||||||
|
user_id: user.id,
|
||||||
|
title: tripName,
|
||||||
|
origin,
|
||||||
|
destination,
|
||||||
|
distance: finalDistance,
|
||||||
|
duration: finalDuration,
|
||||||
|
playlist_url: generatedPlaylistUrl
|
||||||
|
});
|
||||||
|
if (dbError) {
|
||||||
|
console.log("DB Insert error:", dbError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Alert.alert('Sucesso!', 'Viagem e Playlist Criadas!');
|
||||||
|
} catch (playlistError) {
|
||||||
|
console.log("Error generating playlist:", playlistError);
|
||||||
|
Alert.alert('Erro Playlist', 'A viagem foi calculada, mas ocorreu um erro a criar a playlist.');
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// O NOSSO DETETIVE ENTRA AQUI!
|
||||||
|
console.log("ERRO DA GOOGLE:", data);
|
||||||
|
Alert.alert(
|
||||||
|
'Culpado Encontrado',
|
||||||
|
`Motivo: ${data.status}\nDetalhe: ${data.error_message || 'Vê o terminal preto do PC'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Erro', 'Ocorreu um erro ao comunicar com a API da Google.');
|
||||||
|
console.log("Erro de código:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenGoogleMaps = () => {
|
||||||
|
if (!origin || !destination) {
|
||||||
|
Alert.alert('Erro', 'Por favor preenche ambos os campos de partida e destino.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const url = `https://www.google.com/maps/dir/?api=1&origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}`;
|
||||||
|
Linking.openURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -34,11 +198,11 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
<View style={styles.mapArea}>
|
<View style={styles.mapArea}>
|
||||||
{/* Using a solid light gray color instead of a complex map image to keep it clean */}
|
{/* Using a solid light gray color instead of a complex map image to keep it clean */}
|
||||||
<View style={styles.mockRouteVisual}>
|
<View style={styles.mockRouteVisual}>
|
||||||
<View style={styles.routeDotLarge} />
|
<View style={styles.routeDotLarge} />
|
||||||
<View style={styles.routeLineDashed} />
|
<View style={styles.routeLineDashed} />
|
||||||
<View style={styles.routePinLarge}>
|
<View style={styles.routePinLarge}>
|
||||||
<MapPin color={colors.white} size={14} />
|
<MapPin color={colors.white} size={14} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -51,6 +215,8 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
style={styles.textInput}
|
style={styles.textInput}
|
||||||
placeholder="Ex: Fim de semana no Algarve"
|
placeholder="Ex: Fim de semana no Algarve"
|
||||||
placeholderTextColor={colors.textSecondary}
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
value={tripName}
|
||||||
|
onChangeText={setTripName}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -67,8 +233,10 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
<Text style={styles.inputLabel}>PARTIDA</Text>
|
<Text style={styles.inputLabel}>PARTIDA</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, styles.routeTextInput]}
|
style={[styles.textInput, styles.routeTextInput]}
|
||||||
value="Lisboa, Portugal"
|
placeholder="Ex: Lisboa, Portugal"
|
||||||
placeholderTextColor={colors.textMain}
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
value={origin}
|
||||||
|
onChangeText={setOrigin}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -76,25 +244,59 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
<Text style={styles.inputLabel}>DESTINO</Text>
|
<Text style={styles.inputLabel}>DESTINO</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[styles.textInput, styles.routeTextInput]}
|
style={[styles.textInput, styles.routeTextInput]}
|
||||||
value="Porto, Portugal"
|
placeholder="Ex: Porto, Portugal"
|
||||||
placeholderTextColor={colors.textMain}
|
placeholderTextColor={colors.textSecondary}
|
||||||
|
value={destination}
|
||||||
|
onChangeText={setDestination}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Results Section */}
|
||||||
|
{(distance || duration) ? (
|
||||||
|
<View style={styles.resultsContainer}>
|
||||||
|
<View style={styles.resultItem}>
|
||||||
|
<Text style={styles.resultLabel}>Distância</Text>
|
||||||
|
<Text style={styles.resultValue}>{distance}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.resultDivider} />
|
||||||
|
<View style={styles.resultItem}>
|
||||||
|
<Text style={styles.resultLabel}>Duração</Text>
|
||||||
|
<Text style={styles.resultValue}>{duration}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Bottom Actions */}
|
{/* Bottom Actions */}
|
||||||
<View style={styles.bottomActions}>
|
<View style={styles.bottomActions}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.primaryButton}
|
style={styles.primaryButton}
|
||||||
onPress={handleCreateRoute}
|
onPress={handleCalculateTrip}
|
||||||
|
disabled={loading}
|
||||||
>
|
>
|
||||||
<Text style={styles.primaryButtonText}>Criar Rota & Playlist</Text>
|
{loading ? (
|
||||||
<ArrowRight color={colors.white} size={20} />
|
<ActivityIndicator color={colors.white} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text style={styles.primaryButtonText}>Calcular Viagem</Text>
|
||||||
|
<ArrowRight color={colors.white} size={20} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{(distance || duration) ? (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.secondaryButton}
|
||||||
|
onPress={handleOpenGoogleMaps}
|
||||||
|
>
|
||||||
|
<Navigation color={colors.primary} size={20} />
|
||||||
|
<Text style={styles.secondaryButtonText}>Abrir no Google Maps</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Text style={styles.disclaimerText}>
|
<Text style={styles.disclaimerText}>
|
||||||
A IA vai analisar o trajeto, pontos de interesse, clima{'\n'}e duração para criar a banda sonora perfeita.
|
A IA vai analisar o trajeto, pontos de interesse, clima{'\n'}e duração para criar a banda sonora perfeita.
|
||||||
</Text>
|
</Text>
|
||||||
@@ -282,4 +484,46 @@ const styles = StyleSheet.create({
|
|||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
resultsContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
marginTop: 24,
|
||||||
|
paddingTop: 24,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.inputBorder,
|
||||||
|
},
|
||||||
|
resultItem: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
resultDivider: {
|
||||||
|
width: 1,
|
||||||
|
backgroundColor: colors.inputBorder,
|
||||||
|
marginHorizontal: 16,
|
||||||
|
},
|
||||||
|
resultLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginBottom: 4,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
resultValue: {
|
||||||
|
fontSize: 18,
|
||||||
|
color: colors.textMain,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
secondaryButton: {
|
||||||
|
backgroundColor: colors.inputBackground,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 18,
|
||||||
|
borderRadius: 16,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
secondaryButtonText: {
|
||||||
|
color: colors.primary,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'react-native-url-polyfill/auto';
|
import 'react-native-url-polyfill/auto';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
import { AppState } from 'react-native';
|
||||||
|
|
||||||
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL as string;
|
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL as string;
|
||||||
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string;
|
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY as string;
|
||||||
@@ -13,3 +14,8 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|||||||
detectSessionInUrl: false,
|
detectSessionInUrl: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AppState.addEventListener('change', (state) => {
|
||||||
|
if (state === 'active') { supabase.auth.startAutoRefresh(); }
|
||||||
|
else { supabase.auth.stopAutoRefresh(); }
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user