fix login google & fb, fix icon
This commit is contained in:
@@ -64,7 +64,7 @@ export default function PlanetCore({ size = 90, glowIntensity = 0.5 }) {
|
|||||||
{/* Planet image */}
|
{/* Planet image */}
|
||||||
<Image
|
<Image
|
||||||
source={require('../../assets/icon.png')}
|
source={require('../../assets/icon.png')}
|
||||||
style={{ width: size, height: size }}
|
style={{ width: size, height: size, borderRadius: size / 2 }}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ function PageContent({ item, isActive }) {
|
|||||||
)}
|
)}
|
||||||
<Image
|
<Image
|
||||||
source={require('../../assets/icon.png')}
|
source={require('../../assets/icon.png')}
|
||||||
style={{ width: item.iconSize, height: item.iconSize }}
|
style={{ width: item.iconSize, height: item.iconSize, borderRadius: item.iconSize / 2 }}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ const styles = StyleSheet.create({
|
|||||||
width: 200, height: 200, borderRadius: 100,
|
width: 200, height: 200, borderRadius: 100,
|
||||||
backgroundColor: colors.planetGlow, opacity: 0.4, position: 'absolute',
|
backgroundColor: colors.planetGlow, opacity: 0.4, position: 'absolute',
|
||||||
},
|
},
|
||||||
logo: { width: 140, height: 140 },
|
logo: { width: 160, height: 160, borderRadius: 80, overflow: 'hidden' },
|
||||||
title: {
|
title: {
|
||||||
color: colors.text, fontSize: fonts.sizes.hero,
|
color: colors.text, fontSize: fonts.sizes.hero,
|
||||||
fontWeight: fonts.weights.bold, letterSpacing: 8, marginBottom: 16,
|
fontWeight: fonts.weights.bold, letterSpacing: 8, marginBottom: 16,
|
||||||
|
|||||||
+107
-55
@@ -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
|
// Google OAuth
|
||||||
// To setup:
|
// To setup:
|
||||||
@@ -217,50 +233,91 @@ export async function signInAsDemo() {
|
|||||||
// ============================================
|
// ============================================
|
||||||
const GOOGLE_CLIENT_ID = '112566583381-29vsg1bg7ifnq5csg5o8fisdgtllb2sn.apps.googleusercontent.com';
|
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() {
|
export async function signInWithGoogle() {
|
||||||
try {
|
try {
|
||||||
const redirectUri = AuthSession.makeRedirectUri({ scheme: 'nova40' });
|
const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: false });
|
||||||
|
|
||||||
const discovery = {
|
// Use authorization code flow with PKCE (required by Google for mobile)
|
||||||
authorizationEndpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
|
const state = generateRandom(16);
|
||||||
tokenEndpoint: 'https://oauth2.googleapis.com/token',
|
const codeVerifier = generateRandom(64);
|
||||||
};
|
|
||||||
|
|
||||||
const request = new AuthSession.AuthRequest({
|
// For S256 we need crypto, but plain is simpler and works
|
||||||
clientId: GOOGLE_CLIENT_ID,
|
const authUrl =
|
||||||
scopes: ['openid', 'profile', 'email'],
|
`https://accounts.google.com/o/oauth2/v2/auth` +
|
||||||
redirectUri,
|
`?client_id=${GOOGLE_CLIENT_ID}` +
|
||||||
responseType: AuthSession.ResponseType.Token,
|
`&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) {
|
if (result.type === 'success' && result.url) {
|
||||||
// Fetch user info from Google
|
const params = parseUrlParams(result.url);
|
||||||
const userInfo = await fetch('https://www.googleapis.com/userinfo/v2/me', {
|
|
||||||
headers: { Authorization: `Bearer ${result.authentication.accessToken}` },
|
|
||||||
}).then((r) => r.json());
|
|
||||||
|
|
||||||
// Create offline session with Google user data
|
if (params.code) {
|
||||||
const email = userInfo.email || 'google-user@nova40.app';
|
// Exchange code for tokens
|
||||||
const user = makeUser(email);
|
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 tokens = await tokenRes.json();
|
||||||
const users = await getStoredUsers();
|
|
||||||
const key = email.toLowerCase();
|
if (tokens.access_token) {
|
||||||
if (!users[key]) {
|
const userInfo = await fetch('https://www.googleapis.com/userinfo/v2/me', {
|
||||||
users[key] = { id: user.id, password: '__google_oauth__', provider: 'google', name: userInfo.name, picture: userInfo.picture };
|
headers: { Authorization: `Bearer ${tokens.access_token}` },
|
||||||
await saveUsers(users);
|
}).then((r) => r.json());
|
||||||
} else {
|
|
||||||
user.id = users[key].id;
|
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.');
|
throw new Error('Google sign-in failed.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message?.includes('cancel')) throw e;
|
if (e.message?.includes('cancel')) throw e;
|
||||||
@@ -280,35 +337,30 @@ const FACEBOOK_APP_ID = '1696508695097482';
|
|||||||
|
|
||||||
export async function signInWithFacebook() {
|
export async function signInWithFacebook() {
|
||||||
try {
|
try {
|
||||||
const redirectUri = AuthSession.makeRedirectUri({ scheme: 'nova40' });
|
const redirectUri = AuthSession.makeRedirectUri({ preferLocalhost: false });
|
||||||
|
|
||||||
const result = await AuthSession.startAsync({
|
const authUrl =
|
||||||
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`,
|
`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) {
|
const result = await WebBrowser.openAuthSessionAsync(authUrl, redirectUri);
|
||||||
// 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 email = userInfo.email || `fb-${userInfo.id}@nova40.app`;
|
if (result.type === 'success' && result.url) {
|
||||||
const user = makeUser(email);
|
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 email = userInfo.email || `fb-${userInfo.id}@nova40.app`;
|
||||||
const key = email.toLowerCase();
|
return await createOAuthSession(email, 'facebook', userInfo.name, userInfo.picture?.data?.url);
|
||||||
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 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.');
|
throw new Error('Facebook sign-in failed.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message?.includes('cancel')) throw e;
|
if (e.message?.includes('cancel')) throw e;
|
||||||
|
|||||||
Reference in New Issue
Block a user