diff --git a/src/components/PlanetCore.js b/src/components/PlanetCore.js index 57d6f2b..b91c81c 100644 --- a/src/components/PlanetCore.js +++ b/src/components/PlanetCore.js @@ -64,7 +64,7 @@ export default function PlanetCore({ size = 90, glowIntensity = 0.5 }) { {/* Planet image */} diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js index d272bc9..d12fe0f 100644 --- a/src/screens/OnboardingScreen.js +++ b/src/screens/OnboardingScreen.js @@ -107,7 +107,7 @@ function PageContent({ item, isActive }) { )} diff --git a/src/screens/SplashScreen.js b/src/screens/SplashScreen.js index 4a7f6d9..b6d65ad 100644 --- a/src/screens/SplashScreen.js +++ b/src/screens/SplashScreen.js @@ -119,7 +119,7 @@ const styles = StyleSheet.create({ width: 200, height: 200, borderRadius: 100, backgroundColor: colors.planetGlow, opacity: 0.4, position: 'absolute', }, - logo: { width: 140, height: 140 }, + logo: { width: 160, height: 160, borderRadius: 80, overflow: 'hidden' }, title: { color: colors.text, fontSize: fonts.sizes.hero, fontWeight: fonts.weights.bold, letterSpacing: 8, marginBottom: 16, diff --git a/src/services/authService.js b/src/services/authService.js index 798c497..9d1e2e7 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -207,6 +207,22 @@ export async function signInAsDemo() { }); } +// Shared helper for OAuth login (Google/Facebook) +async function createOAuthSession(email, provider, name, picture) { + const user = makeUser(email); + const users = await getStoredUsers(); + const key = email.toLowerCase(); + if (!users[key]) { + users[key] = { id: user.id, password: `__${provider}_oauth__`, provider, name, picture }; + await saveUsers(users); + } else { + user.id = users[key].id; + } + const session = makeSession(user); + await saveSession(session); + return { session, user }; +} + // ============================================ // Google OAuth // To setup: @@ -217,50 +233,91 @@ export async function signInAsDemo() { // ============================================ const GOOGLE_CLIENT_ID = '112566583381-29vsg1bg7ifnq5csg5o8fisdgtllb2sn.apps.googleusercontent.com'; +// Parse params from redirect URL (both ? and # fragments) +function parseUrlParams(url) { + const params = {}; + // Try query string first (?code=xxx&...) + const query = url.split('?')[1]?.split('#')[0]; + if (query) { + query.split('&').forEach((part) => { + const [k, v] = part.split('='); + if (k) params[decodeURIComponent(k)] = decodeURIComponent(v || ''); + }); + } + // Also try hash fragment (#access_token=xxx&...) + const hash = url.split('#')[1]; + if (hash) { + hash.split('&').forEach((part) => { + const [k, v] = part.split('='); + if (k) params[decodeURIComponent(k)] = decodeURIComponent(v || ''); + }); + } + return params; +} + +// Generate random string for PKCE +function generateRandom(length) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + let result = ''; + for (let i = 0; i < length; i++) result += chars.charAt(Math.floor(Math.random() * chars.length)); + return result; +} + export async function signInWithGoogle() { try { - const redirectUri = AuthSession.makeRedirectUri({ scheme: 'nova40' }); + const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: false }); - const discovery = { - authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth', - tokenEndpoint: 'https://oauth2.googleapis.com/token', - }; + // Use authorization code flow with PKCE (required by Google for mobile) + const state = generateRandom(16); + const codeVerifier = generateRandom(64); - const request = new AuthSession.AuthRequest({ - clientId: GOOGLE_CLIENT_ID, - scopes: ['openid', 'profile', 'email'], - redirectUri, - responseType: AuthSession.ResponseType.Token, - }); + // For S256 we need crypto, but plain is simpler and works + const authUrl = + `https://accounts.google.com/o/oauth2/v2/auth` + + `?client_id=${GOOGLE_CLIENT_ID}` + + `&redirect_uri=${encodeURIComponent(redirectUri)}` + + `&response_type=code` + + `&scope=${encodeURIComponent('openid profile email')}` + + `&state=${state}` + + `&code_challenge=${codeVerifier}` + + `&code_challenge_method=plain` + + `&access_type=offline`; - const result = await request.promptAsync(discovery); + const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri); - if (result.type === 'success' && result.authentication?.accessToken) { - // Fetch user info from Google - const userInfo = await fetch('https://www.googleapis.com/userinfo/v2/me', { - headers: { Authorization: `Bearer ${result.authentication.accessToken}` }, - }).then((r) => r.json()); + if (result.type === 'success' && result.url) { + const params = parseUrlParams(result.url); - // Create offline session with Google user data - const email = userInfo.email || 'google-user@nova40.app'; - const user = makeUser(email); + if (params.code) { + // Exchange code for tokens + const tokenRes = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: [ + `code=${encodeURIComponent(params.code)}`, + `client_id=${encodeURIComponent(GOOGLE_CLIENT_ID)}`, + `redirect_uri=${encodeURIComponent(redirectUri)}`, + `grant_type=authorization_code`, + `code_verifier=${encodeURIComponent(codeVerifier)}`, + ].join('&'), + }); - // Store user if not exists - const users = await getStoredUsers(); - const key = email.toLowerCase(); - if (!users[key]) { - users[key] = { id: user.id, password: '__google_oauth__', provider: 'google', name: userInfo.name, picture: userInfo.picture }; - await saveUsers(users); - } else { - user.id = users[key].id; + const tokens = await tokenRes.json(); + + if (tokens.access_token) { + const userInfo = await fetch('https://www.googleapis.com/userinfo/v2/me', { + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }).then((r) => r.json()); + + const email = userInfo.email || 'google-user@nova40.app'; + return await createOAuthSession(email, 'google', userInfo.name, userInfo.picture); + } + + throw new Error(tokens.error_description || 'Failed to exchange code for token.'); } - - const session = makeSession(user); - await saveSession(session); - return { session, user }; } - if (result.type === 'cancel') throw new Error('Google sign-in was cancelled.'); + if (result.type === 'cancel' || result.type === 'dismiss') throw new Error('Google sign-in was cancelled.'); throw new Error('Google sign-in failed.'); } catch (e) { if (e.message?.includes('cancel')) throw e; @@ -280,35 +337,30 @@ const FACEBOOK_APP_ID = '1696508695097482'; export async function signInWithFacebook() { try { - const redirectUri = AuthSession.makeRedirectUri({ scheme: 'nova40' }); + const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: false }); - const result = await AuthSession.startAsync({ - authUrl: `https://www.facebook.com/v18.0/dialog/oauth?client_id=${FACEBOOK_APP_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=token&scope=email,public_profile`, - }); + const authUrl = + `https://www.facebook.com/v18.0/dialog/oauth` + + `?client_id=${FACEBOOK_APP_ID}` + + `&redirect_uri=${encodeURIComponent(redirectUri)}` + + `&response_type=token` + + `&scope=email,public_profile`; - if (result.type === 'success' && result.params?.access_token) { - // Fetch user info from Facebook - const userInfo = await fetch(`https://graph.facebook.com/me?fields=id,name,email,picture&access_token=${result.params.access_token}`) - .then((r) => r.json()); + const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri); - const email = userInfo.email || `fb-${userInfo.id}@nova40.app`; - const user = makeUser(email); + if (result.type === 'success' && result.url) { + const params = parseUrlParams(result.url); + if (params.access_token) { + const userInfo = await fetch( + `https://graph.facebook.com/me?fields=id,name,email,picture&access_token=${params.access_token}` + ).then((r) => r.json()); - const users = await getStoredUsers(); - const key = email.toLowerCase(); - if (!users[key]) { - users[key] = { id: user.id, password: '__facebook_oauth__', provider: 'facebook', name: userInfo.name, picture: userInfo.picture?.data?.url }; - await saveUsers(users); - } else { - user.id = users[key].id; + const email = userInfo.email || `fb-${userInfo.id}@nova40.app`; + return await createOAuthSession(email, 'facebook', userInfo.name, userInfo.picture?.data?.url); } - - const session = makeSession(user); - await saveSession(session); - return { session, user }; } - if (result.type === 'cancel') throw new Error('Facebook sign-in was cancelled.'); + if (result.type === 'cancel' || result.type === 'dismiss') throw new Error('Facebook sign-in was cancelled.'); throw new Error('Facebook sign-in failed.'); } catch (e) { if (e.message?.includes('cancel')) throw e;