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;