diff --git a/memento-mobile/app/(auth)/login.tsx b/memento-mobile/app/(auth)/login.tsx index c669340..502020c 100644 --- a/memento-mobile/app/(auth)/login.tsx +++ b/memento-mobile/app/(auth)/login.tsx @@ -1,17 +1,53 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import { View, Text, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, ActivityIndicator, Alert, StyleSheet, } from 'react-native' +import * as WebBrowser from 'expo-web-browser' +import * as Linking from 'expo-linking' import { useAuthStore } from '@/lib/store' +import { API_URL } from '@/lib/config' import { C } from '@/lib/theme' +WebBrowser.maybeCompleteAuthSession() + export default function LoginScreen() { const [email, setEmail] = useState('') const [password, setPassword] = useState('') + const [showPwd, setShowPwd] = useState(false) const [loading, setLoading] = useState(false) - const login = useAuthStore((s) => s.login) + const [googleLoading, setGoogleLoading] = useState(false) + const { login, loginWithToken } = useAuthStore() + + // Écoute le deep link memento://auth?token=... + useEffect(() => { + const handleUrl = async (event: { url: string }) => { + const parsed = Linking.parse(event.url) + if (parsed.scheme === 'memento' && parsed.path === 'auth') { + const p = parsed.queryParams as Record | undefined + if (p?.error) { + Alert.alert('Connexion Google échouée', p.error === 'unauthorized' ? 'Non autorisé' : 'Erreur serveur') + setGoogleLoading(false) + return + } + if (p?.token && p?.id) { + await loginWithToken(p.token, { + id: p.id, + name: p.name || null, + email: p.email || '', + tier: p.tier || 'FREE', + }) + } + setGoogleLoading(false) + } + } + + const sub = Linking.addEventListener('url', handleUrl) + // Vérifier si l'app a été lancée via un deep link + Linking.getInitialURL().then((url) => { if (url) handleUrl({ url }) }) + return () => sub.remove() + }, []) const handleLogin = async () => { if (!email.trim() || !password.trim()) return @@ -19,20 +55,61 @@ export default function LoginScreen() { try { await login(email.trim().toLowerCase(), password) } catch (e: any) { - Alert.alert('Connexion échouée', e.message) + Alert.alert( + 'Connexion échouée', + e.message?.includes('Identifiants invalides') + ? 'Email ou mot de passe incorrect.\n\nSi vous vous êtes inscrit avec Google, utilisez le bouton Google ci-dessous.' + : e.message + ) } finally { setLoading(false) } } + const handleGoogle = async () => { + setGoogleLoading(true) + try { + const url = `${API_URL}/api/mobile/auth/google-start` + const result = await WebBrowser.openAuthSessionAsync(url, 'memento://auth') + // Si l'utilisateur ferme sans terminer + if (result.type !== 'success') { + setGoogleLoading(false) + } + // Si succès, le deep link listener prend le relais + } catch (e: any) { + Alert.alert('Erreur', e.message) + setGoogleLoading(false) + } + } + return ( + + {/* Logo */} Momento Votre espace de connaissance + {/* Bouton Google */} + + {googleLoading + ? + : <> + G + Continuer avec Google + } + + + {/* Séparateur */} + + + ou + + + + {/* Formulaire email/password */} Email + Mot de passe - + + + setShowPwd(!showPwd)} style={s.eyeBtn}> + {showPwd ? '🙈' : '👁️'} + + + ((set) => ({ body: JSON.stringify({ email, password }), }) if (!res.ok) { - const data = await res.json() + const data = await res.json().catch(() => ({})) throw new Error(data.error || 'Identifiants invalides') } const { token, user } = await res.json() @@ -37,6 +37,11 @@ export const useAuthStore = create((set) => ({ set({ user }) }, + loginWithToken: async (token, user) => { + await setToken(token) + set({ user }) + }, + logout: async () => { await clearToken() set({ user: null }) diff --git a/memento-note/app/api/mobile/auth/google-callback/route.ts b/memento-note/app/api/mobile/auth/google-callback/route.ts new file mode 100644 index 0000000..6b98bf3 --- /dev/null +++ b/memento-note/app/api/mobile/auth/google-callback/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server' +import { auth } from '@/auth' +import prisma from '@/lib/prisma' +import { createMobileToken } from '@/lib/mobile-auth' + +// Appelé après le flux Google OAuth — génère un token mobile et redirige vers l'app native +export async function GET(req: NextRequest) { + try { + const session = await auth() + + if (!session?.user?.email) { + return NextResponse.redirect(`memento://auth?error=unauthorized`) + } + + const user = await prisma.user.findUnique({ + where: { email: session.user.email }, + select: { id: true, name: true, email: true, subscription: { select: { tier: true } } }, + }) + + if (!user) { + return NextResponse.redirect(`memento://auth?error=not_found`) + } + + const token = createMobileToken(user.id) + const params = new URLSearchParams({ + token, + id: user.id, + name: user.name ?? '', + email: user.email ?? '', + tier: user.subscription?.tier ?? 'FREE', + }) + + return NextResponse.redirect(`memento://auth?${params.toString()}`) + } catch (e) { + console.error('[mobile/auth/google-callback]', e) + return NextResponse.redirect(`memento://auth?error=server_error`) + } +} diff --git a/memento-note/app/api/mobile/auth/google-start/route.ts b/memento-note/app/api/mobile/auth/google-start/route.ts new file mode 100644 index 0000000..5d5ed9e --- /dev/null +++ b/memento-note/app/api/mobile/auth/google-start/route.ts @@ -0,0 +1,9 @@ +import { NextRequest, NextResponse } from 'next/server' + +// Lance le flux Google OAuth et redirige vers le callback mobile +export async function GET(req: NextRequest) { + const baseUrl = process.env.NEXTAUTH_URL || 'https://memento-note.com' + const callbackUrl = `${baseUrl}/api/mobile/auth/google-callback` + const signinUrl = `${baseUrl}/api/auth/signin/google?callbackUrl=${encodeURIComponent(callbackUrl)}` + return NextResponse.redirect(signinUrl) +}