From d06ea93f1112928c943f6b3288280ab4671a71f8 Mon Sep 17 00:00:00 2001 From: Antigravity Date: Fri, 29 May 2026 17:09:06 +0000 Subject: [PATCH] mobile: login - bouton Google OAuth + show/hide password + message erreur Google MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - login.tsx: bouton 'Continuer avec Google' (expo-web-browser + deep link memento://auth) - login.tsx: bouton oeil pour afficher/masquer mot de passe - login.tsx: message d'erreur contextuel si compte Google (pas de mot de passe en DB) - store.ts: loginWithToken() pour recevoir le token après OAuth Google - google-start/route.ts: lance le flux NextAuth Google avec redirect callback - google-callback/route.ts: reçoit la session, génère token mobile, redirige vers memento://auth Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- memento-mobile/app/(auth)/login.tsx | 131 ++++++++++++++++-- memento-mobile/lib/store.ts | 7 +- .../api/mobile/auth/google-callback/route.ts | 38 +++++ .../app/api/mobile/auth/google-start/route.ts | 9 ++ 4 files changed, 171 insertions(+), 14 deletions(-) create mode 100644 memento-note/app/api/mobile/auth/google-callback/route.ts create mode 100644 memento-note/app/api/mobile/auth/google-start/route.ts 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) +}