Fix Android APK Issues Phase 2
This commit is contained in:
48
App.tsx
48
App.tsx
@@ -1,41 +1,53 @@
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { View, ActivityIndicator } from 'react-native';
|
||||||
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 WebBrowser from 'expo-web-browser';
|
||||||
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
import { handleInitialAuthUrl, subscribeToAuthRedirects } from './src/auth/authRedirect';
|
||||||
import { supabase } from './src/services/supabase';
|
import { supabase } from './src/services/supabase';
|
||||||
|
|
||||||
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const [isBootstrapped, setIsBootstrapped] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const catchTokenOnLoad = async () => {
|
const bootstrapAuth = async () => {
|
||||||
const url = await Linking.getInitialURL();
|
console.log('[App] Bootstrapping auth...');
|
||||||
if (!url) return;
|
await handleInitialAuthUrl();
|
||||||
|
await supabase.auth.getSession();
|
||||||
|
|
||||||
// Supabase sends tokens after a '#' which Expo might ignore, replace it with '?'
|
const sub = subscribeToAuthRedirects();
|
||||||
const cleanUrl = url.replace('#', '?');
|
|
||||||
const { params, errorCode } = QueryParams.getQueryParams(cleanUrl);
|
|
||||||
|
|
||||||
if (params?.access_token) {
|
setIsBootstrapped(true);
|
||||||
console.log('🔥 TOKEN APANHADO NO ARRANQUE!');
|
console.log('[App] Bootstrapped auth!');
|
||||||
await supabase.auth.setSession({
|
|
||||||
access_token: params.access_token,
|
return () => {
|
||||||
refresh_token: params.refresh_token || '',
|
sub.remove();
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
catchTokenOnLoad();
|
};
|
||||||
|
|
||||||
|
bootstrapAuth();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
if (!isBootstrapped) {
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#ffffff' }}>
|
||||||
|
<ActivityIndicator size="large" color="#0000ff" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<NavigationContainer>
|
<NavigationContainer>
|
||||||
<AppNavigator />
|
<AppNavigator />
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
<StatusBar style="auto" hidden={false} />
|
<StatusBar style="dark" translucent={false} backgroundColor="#ffffff" hidden={false} />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
20
app.json
20
app.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "roadtrip-dj",
|
"name": "roadtrip-dj",
|
||||||
"scheme": "roadtripdj",
|
|
||||||
"slug": "roadtrip-dj",
|
"slug": "roadtrip-dj",
|
||||||
|
"scheme": "roadtripdj",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
@@ -17,18 +17,30 @@
|
|||||||
"supportsTablet": true
|
"supportsTablet": true
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
|
"package": "com.eduardo12345122.roadtripdj",
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": false,
|
||||||
"predictiveBackGestureEnabled": false
|
"predictiveBackGestureEnabled": false
|
||||||
},
|
},
|
||||||
|
"androidStatusBar": {
|
||||||
|
"barStyle": "dark-content",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"translucent": false
|
||||||
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-web-browser"
|
"expo-web-browser",
|
||||||
]
|
"expo-secure-store"
|
||||||
|
],
|
||||||
|
"extra": {
|
||||||
|
"eas": {
|
||||||
|
"projectId": "df13cff2-46b7-4344-ba7e-c74c2a8b32f0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
21
eas.json
Normal file
21
eas.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"cli": {
|
||||||
|
"version": ">= 18.13.0",
|
||||||
|
"appVersionSource": "remote"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"development": {
|
||||||
|
"developmentClient": true,
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"distribution": "internal"
|
||||||
|
},
|
||||||
|
"production": {
|
||||||
|
"autoIncrement": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"submit": {
|
||||||
|
"production": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
128
package-lock.json
generated
128
package-lock.json
generated
@@ -17,6 +17,8 @@
|
|||||||
"expo": "~54.0.33",
|
"expo": "~54.0.33",
|
||||||
"expo-auth-session": "~7.0.11",
|
"expo-auth-session": "~7.0.11",
|
||||||
"expo-crypto": "~15.0.9",
|
"expo-crypto": "~15.0.9",
|
||||||
|
"expo-dev-client": "~6.0.21",
|
||||||
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-web-browser": "~15.0.11",
|
"expo-web-browser": "~15.0.11",
|
||||||
"lucide-react-native": "^1.14.0",
|
"lucide-react-native": "^1.14.0",
|
||||||
@@ -3450,6 +3452,22 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/anser": {
|
"node_modules/anser": {
|
||||||
"version": "1.4.10",
|
"version": "1.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz",
|
||||||
@@ -4831,6 +4849,63 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-dev-client": {
|
||||||
|
"version": "6.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.21.tgz",
|
||||||
|
"integrity": "sha512-SWI6HD0pa4eJujkYFkvvpezUE1zmJXGLu+34azpu7+QJgO+FLutDYDj8BSTdeH/NYDEClDFjCGqVMcWETvmsCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-dev-launcher": "6.0.21",
|
||||||
|
"expo-dev-menu": "7.0.19",
|
||||||
|
"expo-dev-menu-interface": "2.0.0",
|
||||||
|
"expo-manifests": "~1.0.11",
|
||||||
|
"expo-updates-interface": "~2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-launcher": {
|
||||||
|
"version": "6.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.21.tgz",
|
||||||
|
"integrity": "sha512-QZ9gcKMZbp6EsIhzS0QoGB8Cf4xeVJhjbNgWUwcoBIk8gshoFz8CkCQOnX+HNv2sSY3rdCaNpx3Xo0Rflyq7rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.11.0",
|
||||||
|
"expo-dev-menu": "7.0.19",
|
||||||
|
"expo-manifests": "~1.0.11"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-menu": {
|
||||||
|
"version": "7.0.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.19.tgz",
|
||||||
|
"integrity": "sha512-ju5MZiBCPhUKKvHy0ElZdnlhq01mkEEiR8jfrgQVvW26aWjzjLiOhppNAyXtvGbhk7WxJim3wYMiqFFrjGdfKA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-dev-menu-interface": "2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-dev-menu-interface": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-json-utils": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/expo-linking": {
|
"node_modules/expo-linking": {
|
||||||
"version": "8.0.12",
|
"version": "8.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-8.0.12.tgz",
|
||||||
@@ -4859,6 +4934,19 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-manifests": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-6zItytTewN37Cjhp3glUg0ozrgW2GwB8x9wtfzUNoJIMmxO38nnGdTLMaotYhRqdf5PP2Dzdmej1HDHXVNUpRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@expo/config": "~12.0.13",
|
||||||
|
"expo-json-utils": "~0.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "3.0.25",
|
"version": "3.0.25",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.25.tgz",
|
||||||
@@ -4958,6 +5046,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-secure-store": {
|
||||||
|
"version": "15.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz",
|
||||||
|
"integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-server": {
|
"node_modules/expo-server": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
|
||||||
@@ -4980,6 +5077,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-updates-interface": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-web-browser": {
|
"node_modules/expo-web-browser": {
|
||||||
"version": "15.0.11",
|
"version": "15.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.11.tgz",
|
||||||
@@ -5447,6 +5553,22 @@
|
|||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/fb-watchman": {
|
"node_modules/fb-watchman": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
||||||
@@ -6395,6 +6517,12 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
"expo": "~54.0.33",
|
"expo": "~54.0.33",
|
||||||
"expo-auth-session": "~7.0.11",
|
"expo-auth-session": "~7.0.11",
|
||||||
"expo-crypto": "~15.0.9",
|
"expo-crypto": "~15.0.9",
|
||||||
|
"expo-dev-client": "~6.0.21",
|
||||||
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-web-browser": "~15.0.11",
|
"expo-web-browser": "~15.0.11",
|
||||||
"lucide-react-native": "^1.14.0",
|
"lucide-react-native": "^1.14.0",
|
||||||
|
|||||||
204
src/auth/authRedirect.ts
Normal file
204
src/auth/authRedirect.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import * as Linking from 'expo-linking';
|
||||||
|
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
||||||
|
import { supabase } from '../services/supabase';
|
||||||
|
import { setSpotifyToken, setSpotifyRefreshToken } from './spotifyToken';
|
||||||
|
import { addAuthDebugEvent } from '../debug/authDebug';
|
||||||
|
|
||||||
|
import { Alert } from 'react-native';
|
||||||
|
|
||||||
|
let isHandlingUrl = false;
|
||||||
|
const processedSuccessUrls = new Set<string>();
|
||||||
|
|
||||||
|
export function parseOAuthParams(url: string) {
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const qsIndex = url.indexOf('?');
|
||||||
|
const hashIndex = url.indexOf('#');
|
||||||
|
|
||||||
|
let queryPart = '';
|
||||||
|
let hashPart = '';
|
||||||
|
|
||||||
|
if (qsIndex !== -1) {
|
||||||
|
queryPart = url.substring(qsIndex + 1, hashIndex !== -1 ? hashIndex : undefined);
|
||||||
|
}
|
||||||
|
if (hashIndex !== -1) {
|
||||||
|
hashPart = url.substring(hashIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsePart = (part: string) => {
|
||||||
|
if (!part) return;
|
||||||
|
part.split('&').forEach(pair => {
|
||||||
|
const [key, value] = pair.split('=');
|
||||||
|
if (key && value) {
|
||||||
|
params[decodeURIComponent(key)] = decodeURIComponent(value.replace(/\+/g, ' '));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
parsePart(queryPart);
|
||||||
|
parsePart(hashPart);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing OAuth params', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { params, errorCode: params.error_code || params.error || null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleAuthRedirectUrl(url: string | null) {
|
||||||
|
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_called', has_url: Boolean(url) });
|
||||||
|
|
||||||
|
if (!url) return null;
|
||||||
|
|
||||||
|
if (processedSuccessUrls.has(url)) {
|
||||||
|
console.log('[AuthRedirect] Ignoring already processed URL:', url);
|
||||||
|
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_skipped_duplicate' });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHandlingUrl) {
|
||||||
|
console.log('[AuthRedirect] Currently handling a URL, skipping:', url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[AuthRedirect] Processing URL:', url);
|
||||||
|
isHandlingUrl = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_processing' });
|
||||||
|
const { params, errorCode } = parseOAuthParams(url);
|
||||||
|
const paramKeys = Object.keys(params || {});
|
||||||
|
console.log('[AuthRedirect] Parsed param keys:', paramKeys);
|
||||||
|
|
||||||
|
await addAuthDebugEvent({
|
||||||
|
event: 'url_parsed',
|
||||||
|
paramKeys,
|
||||||
|
code_exists: !!params?.code,
|
||||||
|
access_token_exists: !!params?.access_token,
|
||||||
|
refresh_token_exists: !!params?.refresh_token,
|
||||||
|
error_exists: !!(params?.error || params?.error_code),
|
||||||
|
error_code: params?.error_code || params?.error,
|
||||||
|
error_description: params?.error_description
|
||||||
|
});
|
||||||
|
|
||||||
|
if (params?.error || params?.error_code || params?.error_description) {
|
||||||
|
await addAuthDebugEvent({
|
||||||
|
event: 'oauth_error_received',
|
||||||
|
error: params?.error,
|
||||||
|
error_code: params?.error_code,
|
||||||
|
error_description: params?.error_description
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorMsg = params.error_description || params.error || params.error_code;
|
||||||
|
Alert.alert("Spotify OAuth Error", errorMsg);
|
||||||
|
|
||||||
|
// Do not add to processedSuccessUrls if there's an error
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorCode) {
|
||||||
|
console.error('[AuthRedirect] OAuth Error:', errorCode);
|
||||||
|
throw new Error(`OAuth Error: ${errorCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!params) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Provider Tokens if present
|
||||||
|
if (params.provider_token) {
|
||||||
|
console.log('[AuthRedirect] Provider token found! Saving...');
|
||||||
|
await setSpotifyToken(params.provider_token);
|
||||||
|
await addAuthDebugEvent({ event: 'provider_token_saved_from_params', success: true });
|
||||||
|
}
|
||||||
|
if (params.provider_refresh_token) {
|
||||||
|
console.log('[AuthRedirect] Provider refresh token found! Saving...');
|
||||||
|
await setSpotifyRefreshToken(params.provider_refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PKCE flow usually returns 'code'
|
||||||
|
if (params.code) {
|
||||||
|
console.log('[AuthRedirect] Found auth code. Exchanging for session...');
|
||||||
|
await addAuthDebugEvent({ event: 'exchangeCodeForSession_started' });
|
||||||
|
|
||||||
|
const { data, error } = await supabase.auth.exchangeCodeForSession(params.code);
|
||||||
|
|
||||||
|
await addAuthDebugEvent({ event: 'exchangeCodeForSession_finished', success: !error, error: error?.message });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('[AuthRedirect] exchangeCodeForSession failed:', error);
|
||||||
|
} else {
|
||||||
|
console.log('[AuthRedirect] exchangeCodeForSession succeeded!');
|
||||||
|
|
||||||
|
// Force session refresh for app state
|
||||||
|
const sessionRes = await supabase.auth.getSession();
|
||||||
|
await addAuthDebugEvent({ event: 'getSession_after_exchange', user_exists: !!sessionRes.data.session?.user });
|
||||||
|
|
||||||
|
// Sometimes the provider token comes in the session object here instead of params
|
||||||
|
if (data?.session?.provider_token) {
|
||||||
|
console.log('[AuthRedirect] Found provider_token in session data! Saving...');
|
||||||
|
await setSpotifyToken(data.session.provider_token);
|
||||||
|
await addAuthDebugEvent({ event: 'provider_token_saved_from_session', success: true });
|
||||||
|
}
|
||||||
|
if (data?.session?.provider_refresh_token) {
|
||||||
|
await setSpotifyRefreshToken(data.session.provider_refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
processedSuccessUrls.add(url);
|
||||||
|
return data.session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback/Implicit flow returns access_token & refresh_token
|
||||||
|
else if (params.access_token) {
|
||||||
|
console.log('[AuthRedirect] Found access_token. Setting session directly...');
|
||||||
|
await addAuthDebugEvent({ event: 'setSession_started' });
|
||||||
|
|
||||||
|
const { data, error } = await supabase.auth.setSession({
|
||||||
|
access_token: params.access_token,
|
||||||
|
refresh_token: params.refresh_token || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
await addAuthDebugEvent({ event: 'setSession_finished', success: !error, error: error?.message });
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('[AuthRedirect] setSession failed:', error);
|
||||||
|
} else {
|
||||||
|
console.log('[AuthRedirect] setSession succeeded!');
|
||||||
|
|
||||||
|
const sessionRes = await supabase.auth.getSession();
|
||||||
|
await addAuthDebugEvent({ event: 'getSession_after_setSession', user_exists: !!sessionRes.data.session?.user });
|
||||||
|
|
||||||
|
if (data?.session?.provider_token) {
|
||||||
|
await setSpotifyToken(data.session.provider_token);
|
||||||
|
await addAuthDebugEvent({ event: 'provider_token_saved_from_session', success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
processedSuccessUrls.add(url);
|
||||||
|
return data.session;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[AuthRedirect] Error in handleAuthRedirectUrl:', error);
|
||||||
|
await addAuthDebugEvent({ event: 'handleAuthRedirectUrl_exception', error: error.message });
|
||||||
|
} finally {
|
||||||
|
isHandlingUrl = false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleInitialAuthUrl() {
|
||||||
|
const url = await Linking.getInitialURL();
|
||||||
|
console.log('[AuthRedirect] Linking.getInitialURL returned:', url);
|
||||||
|
await addAuthDebugEvent({ event: 'Linking.getInitialURL_received', exists: !!url });
|
||||||
|
return handleAuthRedirectUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeToAuthRedirects() {
|
||||||
|
const subscription = Linking.addEventListener('url', (e) => {
|
||||||
|
console.log('[AuthRedirect] Linking.addEventListener received URL:', e.url);
|
||||||
|
addAuthDebugEvent({ event: 'Linking.addEventListener_received', exists: !!e.url });
|
||||||
|
handleAuthRedirectUrl(e.url);
|
||||||
|
});
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
47
src/auth/spotifyToken.ts
Normal file
47
src/auth/spotifyToken.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import * as SecureStore from 'expo-secure-store';
|
||||||
|
|
||||||
|
const SPOTIFY_TOKEN_KEY = 'spotify_provider_token';
|
||||||
|
const SPOTIFY_REFRESH_TOKEN_KEY = 'spotify_provider_refresh_token';
|
||||||
|
|
||||||
|
export async function setSpotifyToken(token: string) {
|
||||||
|
try {
|
||||||
|
await SecureStore.setItemAsync(SPOTIFY_TOKEN_KEY, token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving Spotify token:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpotifyAccessToken() {
|
||||||
|
try {
|
||||||
|
return await SecureStore.getItemAsync(SPOTIFY_TOKEN_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting Spotify token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setSpotifyRefreshToken(token: string) {
|
||||||
|
try {
|
||||||
|
await SecureStore.setItemAsync(SPOTIFY_REFRESH_TOKEN_KEY, token);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving Spotify refresh token:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSpotifyRefreshToken() {
|
||||||
|
try {
|
||||||
|
return await SecureStore.getItemAsync(SPOTIFY_REFRESH_TOKEN_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting Spotify refresh token:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearSpotifyTokens() {
|
||||||
|
try {
|
||||||
|
await SecureStore.deleteItemAsync(SPOTIFY_TOKEN_KEY);
|
||||||
|
await SecureStore.deleteItemAsync(SPOTIFY_REFRESH_TOKEN_KEY);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing Spotify tokens:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/debug/authDebug.ts
Normal file
35
src/debug/authDebug.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
const AUTH_DEBUG_KEY = 'auth_debug_events';
|
||||||
|
|
||||||
|
export async function addAuthDebugEvent(event: Record<string, any>) {
|
||||||
|
try {
|
||||||
|
const existing = await AsyncStorage.getItem(AUTH_DEBUG_KEY);
|
||||||
|
const events = existing ? JSON.parse(existing) : [];
|
||||||
|
const newEvent = { timestamp: new Date().toISOString(), ...event };
|
||||||
|
events.unshift(newEvent);
|
||||||
|
// Keep only the last 30 events
|
||||||
|
const trimmed = events.slice(0, 30);
|
||||||
|
await AsyncStorage.setItem(AUTH_DEBUG_KEY, JSON.stringify(trimmed));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save auth debug event', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuthDebugEvents() {
|
||||||
|
try {
|
||||||
|
const existing = await AsyncStorage.getItem(AUTH_DEBUG_KEY);
|
||||||
|
return existing ? JSON.parse(existing) : [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get auth debug events', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearAuthDebugEvents() {
|
||||||
|
try {
|
||||||
|
await AsyncStorage.removeItem(AUTH_DEBUG_KEY);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to clear auth debug events', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ import { supabase } from '../../services/supabase';
|
|||||||
import { makeRedirectUri } from 'expo-auth-session';
|
import { makeRedirectUri } from 'expo-auth-session';
|
||||||
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
import * as QueryParams from 'expo-auth-session/build/QueryParams';
|
||||||
import * as Linking from 'expo-linking';
|
import * as Linking from 'expo-linking';
|
||||||
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
|
import { handleAuthRedirectUrl } from '../../auth/authRedirect';
|
||||||
|
import { addAuthDebugEvent, getAuthDebugEvents, clearAuthDebugEvents } from '../../debug/authDebug';
|
||||||
|
import { clearSpotifyTokens } from '../../auth/spotifyToken';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default function LoginScreen({ navigation }) {
|
export default function LoginScreen({ navigation }) {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@@ -33,14 +36,65 @@ export default function LoginScreen({ navigation }) {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAuthDebug = async () => {
|
||||||
|
const events = await getAuthDebugEvents();
|
||||||
|
Alert.alert('Auth Debug Events', JSON.stringify(events, null, 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetAuth = async () => {
|
||||||
|
await supabase.auth.signOut();
|
||||||
|
await clearSpotifyTokens();
|
||||||
|
await clearAuthDebugEvents();
|
||||||
|
Alert.alert('Reset', 'Auth state cleared.');
|
||||||
|
};
|
||||||
|
|
||||||
const handleSpotifyLogin = async () => {
|
const handleSpotifyLogin = async () => {
|
||||||
const redirectUrl = Linking.createURL('/');
|
try {
|
||||||
|
const redirectTo = "roadtripdj://auth/callback";
|
||||||
|
await addAuthDebugEvent({ event: 'login_button_pressed', redirectTo });
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||||
provider: 'spotify',
|
provider: 'spotify',
|
||||||
options: { redirectTo: redirectUrl }
|
options: {
|
||||||
|
redirectTo,
|
||||||
|
skipBrowserRedirect: true,
|
||||||
|
scopes: "user-read-email user-read-private playlist-modify-public playlist-modify-private",
|
||||||
|
queryParams: {
|
||||||
|
show_dialog: "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await addAuthDebugEvent({
|
||||||
|
event: 'oauth_url_created',
|
||||||
|
success: !!data?.url,
|
||||||
|
host: data?.url ? data.url.split('?')[0] : null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
if (data?.url) {
|
if (data?.url) {
|
||||||
await Linking.openURL(data.url);
|
const result = await WebBrowser.openAuthSessionAsync(data.url, redirectTo);
|
||||||
|
await addAuthDebugEvent({ event: 'web_browser_closed', type: result.type });
|
||||||
|
if (result.type === 'success' && result.url) {
|
||||||
|
await addAuthDebugEvent({ event: 'web_browser_success_url_received', success: true });
|
||||||
|
await addAuthDebugEvent({ event: 'processing_web_browser_success_url' });
|
||||||
|
|
||||||
|
await handleAuthRedirectUrl(result.url);
|
||||||
|
|
||||||
|
const { data: sessionData } = await supabase.auth.getSession();
|
||||||
|
await addAuthDebugEvent({
|
||||||
|
event: 'post_browser_getSession',
|
||||||
|
user_exists: Boolean(sessionData.session?.user),
|
||||||
|
provider_token_exists: Boolean((sessionData.session as any)?.provider_token)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('🚀 [LoginScreen] OAuth Error:', e);
|
||||||
|
Alert.alert('Erro de Autenticação', e.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,6 +157,14 @@ export default function LoginScreen({ navigation }) {
|
|||||||
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
<Text style={styles.spotifyButtonText}>Entrar com Spotify</Text>
|
||||||
</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}>
|
||||||
|
<Text style={{ color: 'red' }}>Reset Auth</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
<View style={styles.footerContainer}>
|
<View style={styles.footerContainer}>
|
||||||
<Text style={styles.footerText}>Não tens conta? </Text>
|
<Text style={styles.footerText}>Não tens conta? </Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
|
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, SafeAreaView, ImageBackground } from 'react-native';
|
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ImageBackground, Alert, ActivityIndicator } from 'react-native';
|
||||||
import { NavigationProp } from '@react-navigation/native';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Navigation } from 'lucide-react-native'; // Closest to MapPin with outline
|
import { NavigationProp, useFocusEffect } from '@react-navigation/native';
|
||||||
import { Clock } from 'lucide-react-native';
|
import { Navigation, Clock, MapPin } from 'lucide-react-native';
|
||||||
import { colors } from '../../utils/colors';
|
import { colors } from '../../utils/colors';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { supabase } from '../../services/supabase';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
navigation: NavigationProp<any>;
|
navigation: NavigationProp<any>;
|
||||||
@@ -15,6 +16,43 @@ export default function HomeScreen({ navigation }: Props) {
|
|||||||
const userName = user?.user_metadata?.name || 'Viajante';
|
const userName = user?.user_metadata?.name || 'Viajante';
|
||||||
const initial = userName.charAt(0).toUpperCase();
|
const initial = userName.charAt(0).toUpperCase();
|
||||||
|
|
||||||
|
const [trips, setTrips] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const fetchTrips = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
console.log('[HomeScreen] Fetching trips. User exists:', !!user);
|
||||||
|
let query = supabase.from('trips').select('*').order('created_at', { ascending: false });
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
query = query.or(`user_id.eq.${user.id},user_id.is.null`);
|
||||||
|
} else {
|
||||||
|
query = query.is('user_id', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await query;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error('[HomeScreen] Supabase error fetching trips:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[HomeScreen] Fetched ${data?.length || 0} trips`);
|
||||||
|
setTrips(data || []);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[HomeScreen] Error loading trips:', error.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fetchTrips();
|
||||||
|
}, [fetchTrips])
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
@@ -27,8 +65,42 @@ export default function HomeScreen({ navigation }: Props) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Main Trip Card (Removed hardcoded data - empty state below) */}
|
{loading ? (
|
||||||
{/* Prompt Card */}
|
<ActivityIndicator size="large" color={colors.primary} style={{ marginTop: 40 }} />
|
||||||
|
) : trips.length > 0 ? (
|
||||||
|
trips.map(trip => (
|
||||||
|
<View key={trip.id} style={styles.mainTripCard}>
|
||||||
|
<View style={[styles.tripImage, { backgroundColor: colors.inputBackground, borderTopLeftRadius: 24, borderTopRightRadius: 24 }]} />
|
||||||
|
<View style={styles.tripContent}>
|
||||||
|
<Text style={styles.tripTitle}>{trip.title}</Text>
|
||||||
|
|
||||||
|
<View style={styles.statsRow}>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Navigation color={colors.primary} size={16} />
|
||||||
|
<Text style={styles.statText}>{trip.distance}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statDot} />
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Clock color={colors.primary} size={16} />
|
||||||
|
<Text style={styles.statText}>{trip.duration}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.itinerarySnippet}>
|
||||||
|
<View style={styles.timeline}>
|
||||||
|
<View style={styles.dot} />
|
||||||
|
<View style={styles.line} />
|
||||||
|
<View style={[styles.dot, styles.dotEmpty]} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.locations}>
|
||||||
|
<Text style={styles.locationText}>{trip.origin}</Text>
|
||||||
|
<Text style={styles.locationText}>{trip.destination}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<View style={styles.promptCard}>
|
<View style={styles.promptCard}>
|
||||||
<Text style={styles.promptTitle}>Pronto para a próxima?</Text>
|
<Text style={styles.promptTitle}>Pronto para a próxima?</Text>
|
||||||
<Text style={styles.promptSubtitle}>
|
<Text style={styles.promptSubtitle}>
|
||||||
@@ -41,6 +113,7 @@ export default function HomeScreen({ navigation }: Props) {
|
|||||||
<Text style={styles.promptButtonText}>Criar Nova Viagem</Text>
|
<Text style={styles.promptButtonText}>Criar Nova Viagem</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, StyleSheet, SafeAreaView, TouchableOpacity, Image, ScrollView } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Settings, Music, Map as MapIcon, Heart, LogOut } from 'lucide-react-native';
|
import { Settings, Music, Map as MapIcon, Heart, LogOut } from 'lucide-react-native';
|
||||||
import { colors } from '../../utils/colors';
|
import { colors } from '../../utils/colors';
|
||||||
import { supabase } from '../../services/supabase';
|
import { supabase } from '../../services/supabase';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export default function NewTripScreen({ navigation }) {
|
export default function NewTripScreen({ navigation }) {
|
||||||
@@ -42,13 +43,16 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
setDistance(finalDistance);
|
setDistance(finalDistance);
|
||||||
setDuration(finalDuration);
|
setDuration(finalDuration);
|
||||||
|
|
||||||
|
let generatedPlaylistUrl = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// A. Get provider token
|
// A. Get provider token
|
||||||
const { data: sessionData, error: sessionError } = await supabase.auth.getSession();
|
const providerToken = await getSpotifyAccessToken();
|
||||||
if (sessionError || !sessionData?.session) throw new Error('Session not found.');
|
if (!providerToken) {
|
||||||
const providerToken = sessionData.session.provider_token;
|
throw new Error("Spotify token missing. Please log in with Spotify again.");
|
||||||
if (!providerToken) throw new Error('Spotify provider token missing.');
|
}
|
||||||
|
|
||||||
|
if (providerToken) {
|
||||||
// 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', {
|
||||||
headers: { 'Authorization': `Bearer ${providerToken}` }
|
headers: { 'Authorization': `Bearer ${providerToken}` }
|
||||||
@@ -86,7 +90,7 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
const playlistData = await createPlaylistRes.json();
|
const playlistData = await createPlaylistRes.json();
|
||||||
if (!playlistData.id) throw new Error('Could not create playlist');
|
if (!playlistData.id) throw new Error('Could not create playlist');
|
||||||
const playlistId = playlistData.id;
|
const playlistId = playlistData.id;
|
||||||
const generatedPlaylistUrl = playlistData.external_urls.spotify;
|
generatedPlaylistUrl = playlistData.external_urls.spotify;
|
||||||
|
|
||||||
// E. Fetch Spotify track recommendations
|
// E. Fetch Spotify track recommendations
|
||||||
let accumulatedDurationMs = 0;
|
let accumulatedDurationMs = 0;
|
||||||
@@ -125,14 +129,24 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log("No tracks found for genres:", seed_genres);
|
console.error("No tracks found for genres:", seed_genres);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Spotify token missing, skipping playlist generation.");
|
||||||
|
Alert.alert('Aviso', 'Sessão Spotify não encontrada. A viagem será guardada sem playlist.');
|
||||||
|
}
|
||||||
|
} catch (playlistError) {
|
||||||
|
console.error("Error generating playlist:", playlistError);
|
||||||
|
Alert.alert('Erro Playlist', 'A viagem foi calculada, mas ocorreu um erro a criar a playlist.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// G. Save to Supabase
|
// G. Save to Supabase unconditionally if route is valid
|
||||||
const { data: { user } } = await supabase.auth.getUser();
|
try {
|
||||||
if (user) {
|
const { data: { session } } = await supabase.auth.getSession();
|
||||||
|
const userId = session?.user?.id || null;
|
||||||
|
|
||||||
const { error: dbError } = await supabase.from('trips').insert({
|
const { error: dbError } = await supabase.from('trips').insert({
|
||||||
user_id: user.id,
|
user_id: userId,
|
||||||
title: tripName,
|
title: tripName,
|
||||||
origin,
|
origin,
|
||||||
destination,
|
destination,
|
||||||
@@ -140,15 +154,15 @@ export default function NewTripScreen({ navigation }) {
|
|||||||
duration: finalDuration,
|
duration: finalDuration,
|
||||||
playlist_url: generatedPlaylistUrl
|
playlist_url: generatedPlaylistUrl
|
||||||
});
|
});
|
||||||
if (dbError) {
|
|
||||||
console.log("DB Insert error:", dbError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Alert.alert('Sucesso!', 'Viagem e Playlist Criadas!');
|
if (dbError) {
|
||||||
} catch (playlistError) {
|
console.error("DB Insert error:", dbError);
|
||||||
console.log("Error generating playlist:", playlistError);
|
Alert.alert('Erro ao Guardar', 'Não foi possível guardar a viagem na base de dados: ' + dbError.message);
|
||||||
Alert.alert('Erro Playlist', 'A viagem foi calculada, mas ocorreu um erro a criar a playlist.');
|
} else {
|
||||||
|
Alert.alert('Sucesso!', 'Viagem calculada e guardada!');
|
||||||
|
}
|
||||||
|
} catch (dbEx) {
|
||||||
|
console.error("Exception during DB save:", dbEx);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
|
|||||||
autoRefreshToken: true,
|
autoRefreshToken: true,
|
||||||
persistSession: true,
|
persistSession: true,
|
||||||
detectSessionInUrl: false,
|
detectSessionInUrl: false,
|
||||||
|
flowType: 'pkce',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
21
supabase/create_trips_table.sql
Normal file
21
supabase/create_trips_table.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
-- Drop if exists (optional, but requested robust creation)
|
||||||
|
-- DROP TABLE IF EXISTS public.trips;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS public.trips (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE,
|
||||||
|
title text NOT NULL,
|
||||||
|
origin text NOT NULL,
|
||||||
|
destination text NOT NULL,
|
||||||
|
distance text,
|
||||||
|
duration text,
|
||||||
|
playlist_url text,
|
||||||
|
created_at timestamptz DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Note: Depending on your Supabase settings, you might need to enable RLS:
|
||||||
|
-- ALTER TABLE public.trips ENABLE ROW LEVEL SECURITY;
|
||||||
|
-- CREATE POLICY "Users can insert their own trips." ON public.trips FOR INSERT WITH CHECK (auth.uid() = user_id);
|
||||||
|
-- CREATE POLICY "Users can view their own trips." ON public.trips FOR SELECT USING (auth.uid() = user_id);
|
||||||
|
|
||||||
|
NOTIFY pgrst, 'reload schema';
|
||||||
Reference in New Issue
Block a user