diff --git a/memento-mobile/app.json b/memento-mobile/app.json
new file mode 100644
index 0000000..68f3910
--- /dev/null
+++ b/memento-mobile/app.json
@@ -0,0 +1,34 @@
+{
+ "expo": {
+ "name": "Momento",
+ "slug": "memento-mobile",
+ "version": "1.0.0",
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "automatic",
+ "scheme": "memento",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#FAFAF8"
+ },
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "com.momentonote.app"
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#FAFAF8"
+ },
+ "package": "com.momentonote.app"
+ },
+ "plugins": [
+ "expo-router",
+ "expo-secure-store"
+ ],
+ "experiments": {
+ "typedRoutes": true
+ }
+ }
+}
diff --git a/memento-mobile/app/(auth)/_layout.tsx b/memento-mobile/app/(auth)/_layout.tsx
new file mode 100644
index 0000000..981966b
--- /dev/null
+++ b/memento-mobile/app/(auth)/_layout.tsx
@@ -0,0 +1,5 @@
+import { Stack } from 'expo-router'
+
+export default function AuthLayout() {
+ return
+}
diff --git a/memento-mobile/app/(auth)/login.tsx b/memento-mobile/app/(auth)/login.tsx
new file mode 100644
index 0000000..070719c
--- /dev/null
+++ b/memento-mobile/app/(auth)/login.tsx
@@ -0,0 +1,98 @@
+import { useState } from 'react'
+import {
+ View,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ KeyboardAvoidingView,
+ Platform,
+ ActivityIndicator,
+ Alert,
+} from 'react-native'
+import { useAuthStore } from '@/lib/store'
+
+export default function LoginScreen() {
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [loading, setLoading] = useState(false)
+ const login = useAuthStore((s) => s.login)
+
+ const handleLogin = async () => {
+ if (!email.trim() || !password.trim()) return
+ setLoading(true)
+ try {
+ await login(email.trim().toLowerCase(), password)
+ } catch (e: any) {
+ Alert.alert('Connexion échouée', e.message)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+ {/* Logo */}
+
+ Momento
+ Votre espace de connaissance
+
+
+ {/* Form */}
+
+
+
+ Email
+
+
+
+
+
+
+ Mot de passe
+
+
+
+
+
+ {loading ? (
+
+ ) : (
+ Se connecter
+ )}
+
+
+
+
+ memento-note.com
+
+
+
+ )
+}
diff --git a/memento-mobile/app/(tabs)/_layout.tsx b/memento-mobile/app/(tabs)/_layout.tsx
new file mode 100644
index 0000000..fdec56c
--- /dev/null
+++ b/memento-mobile/app/(tabs)/_layout.tsx
@@ -0,0 +1,55 @@
+import { Tabs } from 'expo-router'
+import { BookOpen, Search, Home, User } from 'lucide-react-native'
+
+export default function TabsLayout() {
+ return (
+
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+
+ )
+}
diff --git a/memento-mobile/app/(tabs)/home.tsx b/memento-mobile/app/(tabs)/home.tsx
new file mode 100644
index 0000000..4240ede
--- /dev/null
+++ b/memento-mobile/app/(tabs)/home.tsx
@@ -0,0 +1,132 @@
+import { useEffect, useState } from 'react'
+import {
+ View, Text, ScrollView, TouchableOpacity,
+ ActivityIndicator, RefreshControl,
+} from 'react-native'
+import { SafeAreaView } from 'react-native-safe-area-context'
+import { useRouter } from 'expo-router'
+import { CalendarDays, Sparkles, BookOpen } from 'lucide-react-native'
+import { apiFetch } from '@/lib/api'
+import { ENDPOINTS } from '@/lib/config'
+import { useAuthStore } from '@/lib/store'
+
+interface Note {
+ id: string
+ title: string
+ updatedAt: string
+ notebookName?: string
+ color?: string
+}
+
+export default function HomeScreen() {
+ const user = useAuthStore((s) => s.user)
+ const [recentNotes, setRecentNotes] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const router = useRouter()
+
+ const load = async () => {
+ try {
+ const res = await apiFetch(ENDPOINTS.notes())
+ if (res.ok) {
+ const data = await res.json()
+ setRecentNotes((data.notes ?? []).slice(0, 10))
+ }
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => { load() }, [])
+
+ const handleDailyNote = async () => {
+ const res = await apiFetch(ENDPOINTS.dailyNote)
+ if (res.ok) {
+ const data = await res.json()
+ router.push(`/note/${data.id}`)
+ }
+ }
+
+ const formatDate = (iso: string) => {
+ const d = new Date(iso)
+ return d.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })
+ }
+
+ return (
+
+ { setRefreshing(true); load() }} tintColor="#A47148" />}
+ >
+ {/* Header */}
+
+
+ Bonjour{user?.name ? `, ${user.name.split(' ')[0]}` : ''} 👋
+
+
+ {new Date().toLocaleDateString('fr-FR', { weekday: 'long', day: 'numeric', month: 'long' })}
+
+
+
+ {/* Quick actions */}
+
+
+
+ Note du jour
+
+ router.push('/(tabs)/notebooks')}
+ className="flex-1 bg-white border border-border rounded-2xl p-4 items-center gap-2"
+ >
+
+ Carnets
+
+ router.push('/(tabs)/search')}
+ className="flex-1 bg-white border border-border rounded-2xl p-4 items-center gap-2"
+ >
+
+ Recherche IA
+
+
+
+ {/* Recent notes */}
+
+
+ Récentes
+
+
+ {loading ? (
+
+ ) : recentNotes.length === 0 ? (
+ Aucune note pour l'instant.
+ ) : (
+ recentNotes.map((note) => (
+ router.push(`/note/${note.id}`)}
+ className="bg-white border border-border rounded-2xl p-4 mb-2.5 active:opacity-70"
+ >
+
+ {note.title || 'Sans titre'}
+
+
+ {note.notebookName && (
+ {note.notebookName}
+ )}
+ {formatDate(note.updatedAt)}
+
+
+ ))
+ )}
+
+
+
+
+
+ )
+}
diff --git a/memento-mobile/app/(tabs)/notebooks.tsx b/memento-mobile/app/(tabs)/notebooks.tsx
new file mode 100644
index 0000000..46a22b9
--- /dev/null
+++ b/memento-mobile/app/(tabs)/notebooks.tsx
@@ -0,0 +1,78 @@
+import { useEffect, useState } from 'react'
+import {
+ View, Text, FlatList, TouchableOpacity,
+ ActivityIndicator, RefreshControl,
+} from 'react-native'
+import { SafeAreaView } from 'react-native-safe-area-context'
+import { useRouter } from 'expo-router'
+import { ChevronRight } from 'lucide-react-native'
+import { apiFetch } from '@/lib/api'
+import { ENDPOINTS } from '@/lib/config'
+
+interface Notebook {
+ id: string
+ name: string
+ icon: string | null
+ _count: { notes: number }
+}
+
+export default function NotebooksScreen() {
+ const [notebooks, setNotebooks] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const router = useRouter()
+
+ const load = async () => {
+ try {
+ const res = await apiFetch(ENDPOINTS.notebooks)
+ if (res.ok) {
+ const data = await res.json()
+ setNotebooks(data.notebooks ?? [])
+ }
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => { load() }, [])
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+ Carnets
+
+
+ item.id}
+ contentContainerClassName="px-5 pb-8"
+ refreshControl={ { setRefreshing(true); load() }} tintColor="#A47148" />}
+ renderItem={({ item }) => (
+ router.push(`/notebook/${item.id}`)}
+ className="flex-row items-center bg-white border border-border rounded-2xl p-4 mb-2.5 active:opacity-70"
+ >
+ {item.icon ?? '📓'}
+
+ {item.name}
+ {item._count?.notes ?? 0} notes
+
+
+
+ )}
+ ListEmptyComponent={
+ Aucun carnet.
+ }
+ />
+
+ )
+}
diff --git a/memento-mobile/app/(tabs)/profile.tsx b/memento-mobile/app/(tabs)/profile.tsx
new file mode 100644
index 0000000..b456bd5
--- /dev/null
+++ b/memento-mobile/app/(tabs)/profile.tsx
@@ -0,0 +1,73 @@
+import { View, Text, TouchableOpacity, ScrollView, Alert } from 'react-native'
+import { SafeAreaView } from 'react-native-safe-area-context'
+import { LogOut, CreditCard, Globe } from 'lucide-react-native'
+import { useAuthStore } from '@/lib/store'
+
+const TIER_LABELS: Record = {
+ FREE: 'Gratuit',
+ BASIC: 'Basic',
+ PRO: 'Pro',
+ BUSINESS: 'Business',
+ ENTERPRISE: 'Enterprise',
+}
+
+export default function ProfileScreen() {
+ const { user, logout } = useAuthStore()
+
+ const handleLogout = () => {
+ Alert.alert(
+ 'Se déconnecter',
+ 'Voulez-vous vraiment vous déconnecter ?',
+ [
+ { text: 'Annuler', style: 'cancel' },
+ { text: 'Déconnecter', style: 'destructive', onPress: logout },
+ ]
+ )
+ }
+
+ return (
+
+
+ Profil
+
+ {/* User info */}
+
+
+
+ {user?.name?.[0]?.toUpperCase() ?? user?.email?.[0]?.toUpperCase() ?? '?'}
+
+
+ {user?.name ?? 'Utilisateur'}
+ {user?.email}
+ {user?.tier && (
+
+
+ {TIER_LABELS[user.tier] ?? user.tier}
+
+
+ )}
+
+
+ {/* Actions */}
+
+
+
+ Abonnement
+
+
+
+ Ouvrir memento-note.com
+
+
+
+
+
+ Se déconnecter
+
+
+
+ )
+}
diff --git a/memento-mobile/app/(tabs)/search.tsx b/memento-mobile/app/(tabs)/search.tsx
new file mode 100644
index 0000000..4473612
--- /dev/null
+++ b/memento-mobile/app/(tabs)/search.tsx
@@ -0,0 +1,106 @@
+import { useState } from 'react'
+import {
+ View, Text, TextInput, FlatList,
+ TouchableOpacity, ActivityIndicator,
+} from 'react-native'
+import { SafeAreaView } from 'react-native-safe-area-context'
+import { Search as SearchIcon, X } from 'lucide-react-native'
+import { useRouter } from 'expo-router'
+import { apiFetch } from '@/lib/api'
+import { ENDPOINTS } from '@/lib/config'
+
+interface SearchResult {
+ id: string
+ title: string
+ snippet: string
+ notebookName?: string
+ score?: number
+}
+
+export default function SearchScreen() {
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [searched, setSearched] = useState(false)
+ const router = useRouter()
+
+ const handleSearch = async () => {
+ if (!query.trim()) return
+ setLoading(true)
+ setSearched(true)
+ try {
+ const res = await apiFetch(`${ENDPOINTS.search}?q=${encodeURIComponent(query)}`)
+ if (res.ok) {
+ const data = await res.json()
+ setResults(data.results ?? [])
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+ Recherche
+
+
+
+
+ {query.length > 0 && (
+ { setQuery(''); setResults([]); setSearched(false) }}>
+
+
+ )}
+
+
+
+ {loading ? (
+
+
+
+ ) : (
+ item.id}
+ contentContainerClassName="px-5 pt-3 pb-8"
+ renderItem={({ item }) => (
+ router.push(`/note/${item.id}`)}
+ className="bg-white border border-border rounded-2xl p-4 mb-2.5 active:opacity-70"
+ >
+
+ {item.title || 'Sans titre'}
+
+ {item.snippet && (
+
+ {item.snippet}
+
+ )}
+ {item.notebookName && (
+ {item.notebookName}
+ )}
+
+ )}
+ ListEmptyComponent={
+ searched ? (
+ Aucun résultat pour "{query}"
+ ) : (
+
+ Tapez votre recherche puis appuyez sur Entrée
+
+ )
+ }
+ />
+ )}
+
+ )
+}
diff --git a/memento-mobile/app/_layout.tsx b/memento-mobile/app/_layout.tsx
new file mode 100644
index 0000000..57c2507
--- /dev/null
+++ b/memento-mobile/app/_layout.tsx
@@ -0,0 +1,41 @@
+import { useEffect } from 'react'
+import { Slot, useRouter, useSegments } from 'expo-router'
+import { SafeAreaProvider } from 'react-native-safe-area-context'
+import { StatusBar } from 'expo-status-bar'
+import { View, ActivityIndicator } from 'react-native'
+import { useAuthStore } from '@/lib/store'
+
+export default function RootLayout() {
+ const { user, loading, restore } = useAuthStore()
+ const router = useRouter()
+ const segments = useSegments()
+
+ useEffect(() => {
+ restore()
+ }, [])
+
+ useEffect(() => {
+ if (loading) return
+ const inAuth = segments[0] === '(auth)'
+ if (!user && !inAuth) {
+ router.replace('/(auth)/login')
+ } else if (user && inAuth) {
+ router.replace('/(tabs)/home')
+ }
+ }, [user, loading, segments])
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+
+ )
+}
diff --git a/memento-mobile/app/note/[id].tsx b/memento-mobile/app/note/[id].tsx
new file mode 100644
index 0000000..653f6fe
--- /dev/null
+++ b/memento-mobile/app/note/[id].tsx
@@ -0,0 +1,104 @@
+import { useEffect, useState } from 'react'
+import {
+ View, Text, ScrollView, ActivityIndicator,
+ TouchableOpacity, Share,
+} from 'react-native'
+import { SafeAreaView } from 'react-native-safe-area-context'
+import { useLocalSearchParams, useRouter } from 'expo-router'
+import { ArrowLeft, Share2 } from 'lucide-react-native'
+import { WebView } from 'react-native-webview'
+import { apiFetch } from '@/lib/api'
+import { ENDPOINTS } from '@/lib/config'
+
+interface Note {
+ id: string
+ title: string
+ content: string
+ updatedAt: string
+ notebookName?: string
+}
+
+// Wrap HTML content in a minimal styled page for WebView rendering
+function buildHtml(content: string, title: string) {
+ return `
+
+
+
+
+
+${content}
+`
+}
+
+export default function NoteScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>()
+ const [note, setNote] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const router = useRouter()
+
+ useEffect(() => {
+ apiFetch(ENDPOINTS.note(id))
+ .then((r) => r.json())
+ .then((data) => setNote(data.note ?? null))
+ .finally(() => setLoading(false))
+ }, [id])
+
+ const handleShare = async () => {
+ if (!note) return
+ await Share.share({
+ title: note.title,
+ message: `${note.title}\nhttps://memento-note.com`,
+ })
+ }
+
+ return (
+
+ {/* Header */}
+
+ router.back()} className="p-1">
+
+
+
+ {note?.title ?? '…'}
+
+
+
+
+
+
+ {loading ? (
+
+
+
+ ) : note ? (
+
+ ) : (
+
+ Note introuvable.
+
+ )}
+
+ )
+}
diff --git a/memento-mobile/app/notebook/[id].tsx b/memento-mobile/app/notebook/[id].tsx
new file mode 100644
index 0000000..1576613
--- /dev/null
+++ b/memento-mobile/app/notebook/[id].tsx
@@ -0,0 +1,78 @@
+import { useEffect, useState } from 'react'
+import {
+ View, Text, FlatList, TouchableOpacity, ActivityIndicator, RefreshControl,
+} from 'react-native'
+import { SafeAreaView } from 'react-native-safe-area-context'
+import { useLocalSearchParams, useRouter } from 'expo-router'
+import { ArrowLeft } from 'lucide-react-native'
+import { apiFetch } from '@/lib/api'
+import { ENDPOINTS } from '@/lib/config'
+
+interface Note {
+ id: string
+ title: string
+ updatedAt: string
+}
+
+export default function NotebookScreen() {
+ const { id } = useLocalSearchParams<{ id: string }>()
+ const [notes, setNotes] = useState([])
+ const [notebookName, setNotebookName] = useState('')
+ const [loading, setLoading] = useState(true)
+ const [refreshing, setRefreshing] = useState(false)
+ const router = useRouter()
+
+ const load = async () => {
+ try {
+ const res = await apiFetch(ENDPOINTS.notes(id))
+ if (res.ok) {
+ const data = await res.json()
+ setNotes(data.notes ?? [])
+ setNotebookName(data.notebookName ?? '')
+ }
+ } finally {
+ setLoading(false)
+ setRefreshing(false)
+ }
+ }
+
+ useEffect(() => { load() }, [id])
+
+ return (
+
+
+ router.back()} className="p-1">
+
+
+ {notebookName || 'Carnet'}
+
+
+ {loading ? (
+
+
+
+ ) : (
+ item.id}
+ contentContainerClassName="px-5 pt-4 pb-8"
+ refreshControl={ { setRefreshing(true); load() }} tintColor="#A47148" />}
+ renderItem={({ item }) => (
+ router.push(`/note/${item.id}`)}
+ className="bg-white border border-border rounded-2xl p-4 mb-2.5 active:opacity-70"
+ >
+
+ {item.title || 'Sans titre'}
+
+
+ {new Date(item.updatedAt).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
+
+
+ )}
+ ListEmptyComponent={Carnet vide.}
+ />
+ )}
+
+ )
+}
diff --git a/memento-mobile/babel.config.js b/memento-mobile/babel.config.js
new file mode 100644
index 0000000..1d1ac9c
--- /dev/null
+++ b/memento-mobile/babel.config.js
@@ -0,0 +1,9 @@
+module.exports = function (api) {
+ api.cache(true);
+ return {
+ presets: [
+ ['babel-preset-expo', { jsxImportSource: 'nativewind' }],
+ 'nativewind/babel',
+ ],
+ };
+};
diff --git a/memento-mobile/index.ts b/memento-mobile/index.ts
new file mode 100644
index 0000000..5b83418
--- /dev/null
+++ b/memento-mobile/index.ts
@@ -0,0 +1 @@
+import 'expo-router/entry';
diff --git a/memento-mobile/lib/api.ts b/memento-mobile/lib/api.ts
new file mode 100644
index 0000000..5b6bd7e
--- /dev/null
+++ b/memento-mobile/lib/api.ts
@@ -0,0 +1,27 @@
+import * as SecureStore from 'expo-secure-store'
+
+const TOKEN_KEY = 'memento_token'
+
+export async function getToken(): Promise {
+ return SecureStore.getItemAsync(TOKEN_KEY)
+}
+
+export async function setToken(token: string): Promise {
+ await SecureStore.setItemAsync(TOKEN_KEY, token)
+}
+
+export async function clearToken(): Promise {
+ await SecureStore.deleteItemAsync(TOKEN_KEY)
+}
+
+export async function apiFetch(url: string, options: RequestInit = {}): Promise {
+ const token = await getToken()
+ return fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
+ ...options.headers,
+ },
+ })
+}
diff --git a/memento-mobile/lib/config.ts b/memento-mobile/lib/config.ts
new file mode 100644
index 0000000..253dc90
--- /dev/null
+++ b/memento-mobile/lib/config.ts
@@ -0,0 +1,18 @@
+// API base URL — change for dev/prod
+export const API_URL = __DEV__
+ ? 'http://192.168.1.190:3000' // local network dev server
+ : 'https://memento-note.com'
+
+export const ENDPOINTS = {
+ login: `${API_URL}/api/mobile/auth/login`,
+ logout: `${API_URL}/api/mobile/auth/logout`,
+ me: `${API_URL}/api/mobile/auth/me`,
+ notebooks: `${API_URL}/api/mobile/notebooks`,
+ notes: (notebookId?: string) =>
+ notebookId
+ ? `${API_URL}/api/mobile/notes?notebookId=${notebookId}`
+ : `${API_URL}/api/mobile/notes`,
+ note: (id: string) => `${API_URL}/api/mobile/notes/${id}`,
+ search: `${API_URL}/api/mobile/search`,
+ dailyNote: `${API_URL}/api/notes/daily`,
+}
diff --git a/memento-mobile/lib/store.ts b/memento-mobile/lib/store.ts
new file mode 100644
index 0000000..9c58ea7
--- /dev/null
+++ b/memento-mobile/lib/store.ts
@@ -0,0 +1,56 @@
+import { create } from 'zustand'
+import { apiFetch, setToken, clearToken, getToken } from '@/lib/api'
+import { ENDPOINTS } from '@/lib/config'
+
+interface User {
+ id: string
+ name: string | null
+ email: string
+ tier: string
+}
+
+interface AuthState {
+ user: User | null
+ loading: boolean
+ login: (email: string, password: string) => Promise
+ logout: () => Promise
+ restore: () => Promise
+}
+
+export const useAuthStore = create((set) => ({
+ user: null,
+ loading: true,
+
+ login: async (email, password) => {
+ const res = await fetch(ENDPOINTS.login, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email, password }),
+ })
+ if (!res.ok) {
+ const data = await res.json()
+ throw new Error(data.error || 'Identifiants invalides')
+ }
+ const { token, user } = await res.json()
+ await setToken(token)
+ set({ user })
+ },
+
+ logout: async () => {
+ await clearToken()
+ set({ user: null })
+ },
+
+ restore: async () => {
+ try {
+ const token = await getToken()
+ if (!token) { set({ loading: false }); return }
+ const res = await apiFetch(ENDPOINTS.me)
+ if (!res.ok) { await clearToken(); set({ loading: false, user: null }); return }
+ const user = await res.json()
+ set({ user, loading: false })
+ } catch {
+ set({ loading: false, user: null })
+ }
+ },
+}))
diff --git a/memento-mobile/package.json b/memento-mobile/package.json
new file mode 100644
index 0000000..a6274d9
--- /dev/null
+++ b/memento-mobile/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "memento-mobile",
+ "version": "1.0.0",
+ "main": "expo-router/entry",
+ "scripts": {
+ "start": "expo start",
+ "android": "expo start --android",
+ "ios": "expo start --ios",
+ "build:android": "eas build --platform android",
+ "build:ios": "eas build --platform ios"
+ },
+ "dependencies": {
+ "expo": "~52.0.0",
+ "expo-router": "~4.0.0",
+ "expo-status-bar": "~2.0.1",
+ "expo-secure-store": "~14.0.0",
+ "expo-font": "~13.0.2",
+ "expo-splash-screen": "~0.29.18",
+ "react": "18.3.1",
+ "react-native": "0.76.7",
+ "react-native-safe-area-context": "4.12.0",
+ "react-native-screens": "~4.4.0",
+ "react-native-webview": "13.12.5",
+ "@react-native-async-storage/async-storage": "1.23.1",
+ "nativewind": "^4.1.23",
+ "tailwindcss": "^3.4.0",
+ "zustand": "^5.0.2"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@types/react": "~18.3.12",
+ "typescript": "^5.3.3"
+ }
+}
diff --git a/memento-mobile/tailwind.config.js b/memento-mobile/tailwind.config.js
new file mode 100644
index 0000000..0596dd2
--- /dev/null
+++ b/memento-mobile/tailwind.config.js
@@ -0,0 +1,21 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ['./app/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'],
+ presets: [require('nativewind/preset')],
+ theme: {
+ extend: {
+ colors: {
+ brand: '#A47148',
+ ink: '#1A1A18',
+ paper: '#FAFAF8',
+ concrete: '#8A8A82',
+ border: '#E8E6E0',
+ },
+ fontFamily: {
+ sans: ['System'],
+ serif: ['Georgia'],
+ },
+ },
+ },
+ plugins: [],
+}
diff --git a/memento-mobile/tsconfig.json b/memento-mobile/tsconfig.json
new file mode 100644
index 0000000..ed3ad5c
--- /dev/null
+++ b/memento-mobile/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.d.ts", "expo-env.d.ts"]
+}
diff --git a/memento-note/app/api/mobile/auth/login/route.ts b/memento-note/app/api/mobile/auth/login/route.ts
new file mode 100644
index 0000000..f87ac6e
--- /dev/null
+++ b/memento-note/app/api/mobile/auth/login/route.ts
@@ -0,0 +1,45 @@
+import { NextRequest, NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+import bcrypt from 'bcryptjs'
+import { createMobileToken } from '@/lib/mobile-auth'
+
+export async function POST(req: NextRequest) {
+ try {
+ const { email, password } = await req.json()
+ if (!email || !password) {
+ return NextResponse.json({ error: 'Email et mot de passe requis' }, { status: 400 })
+ }
+
+ const user = await prisma.user.findUnique({
+ where: { email: email.toLowerCase().trim() },
+ select: {
+ id: true, name: true, email: true, password: true,
+ subscription: { select: { tier: true } },
+ },
+ })
+
+ if (!user?.password) {
+ return NextResponse.json({ error: 'Identifiants invalides' }, { status: 401 })
+ }
+
+ const valid = await bcrypt.compare(password, user.password)
+ if (!valid) {
+ return NextResponse.json({ error: 'Identifiants invalides' }, { status: 401 })
+ }
+
+ const token = createMobileToken(user.id)
+ return NextResponse.json({
+ token,
+ user: {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ tier: user.subscription?.tier ?? 'FREE',
+ },
+ })
+ } catch (e) {
+ console.error('[mobile/auth/login]', e)
+ return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
+ }
+}
+
diff --git a/memento-note/app/api/mobile/auth/me/route.ts b/memento-note/app/api/mobile/auth/me/route.ts
new file mode 100644
index 0000000..f7df5a1
--- /dev/null
+++ b/memento-note/app/api/mobile/auth/me/route.ts
@@ -0,0 +1,25 @@
+import { NextRequest, NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+import { getMobileUserId } from '@/lib/mobile-auth'
+
+export async function GET(req: NextRequest) {
+ const userId = getMobileUserId(req)
+ if (!userId) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
+
+ const user = await prisma.user.findUnique({
+ where: { id: userId },
+ select: {
+ id: true, name: true, email: true,
+ subscription: { select: { tier: true } },
+ },
+ })
+ if (!user) return NextResponse.json({ error: 'Introuvable' }, { status: 404 })
+
+ return NextResponse.json({
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ tier: user.subscription?.tier ?? 'FREE',
+ })
+}
+
diff --git a/memento-note/app/api/mobile/notebooks/route.ts b/memento-note/app/api/mobile/notebooks/route.ts
new file mode 100644
index 0000000..1458f86
--- /dev/null
+++ b/memento-note/app/api/mobile/notebooks/route.ts
@@ -0,0 +1,16 @@
+import { NextRequest, NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+import { getMobileUserId } from '@/lib/mobile-auth'
+
+export async function GET(req: NextRequest) {
+ const userId = getMobileUserId(req)
+ if (!userId) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
+
+ const notebooks = await prisma.notebook.findMany({
+ where: { userId, trashedAt: null },
+ include: { _count: { select: { notes: { where: { trashedAt: null } } } } },
+ orderBy: { order: 'asc' },
+ })
+
+ return NextResponse.json({ notebooks })
+}
diff --git a/memento-note/app/api/mobile/notes/[id]/route.ts b/memento-note/app/api/mobile/notes/[id]/route.ts
new file mode 100644
index 0000000..a0febe2
--- /dev/null
+++ b/memento-note/app/api/mobile/notes/[id]/route.ts
@@ -0,0 +1,30 @@
+import { NextRequest, NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+import { getMobileUserId } from '@/lib/mobile-auth'
+
+export async function GET(
+ req: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ const userId = getMobileUserId(req)
+ if (!userId) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
+
+ const { id } = await params
+
+ const note = await prisma.note.findFirst({
+ where: { id, userId, trashedAt: null },
+ select: {
+ id: true,
+ title: true,
+ content: true,
+ updatedAt: true,
+ createdAt: true,
+ color: true,
+ notebook: { select: { id: true, name: true } },
+ },
+ })
+
+ if (!note) return NextResponse.json({ error: 'Note introuvable' }, { status: 404 })
+
+ return NextResponse.json({ note })
+}
diff --git a/memento-note/app/api/mobile/notes/route.ts b/memento-note/app/api/mobile/notes/route.ts
new file mode 100644
index 0000000..ea8d739
--- /dev/null
+++ b/memento-note/app/api/mobile/notes/route.ts
@@ -0,0 +1,43 @@
+import { NextRequest, NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+import { getMobileUserId } from '@/lib/mobile-auth'
+
+export async function GET(req: NextRequest) {
+ const userId = getMobileUserId(req)
+ if (!userId) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
+
+ const { searchParams } = new URL(req.url)
+ const notebookId = searchParams.get('notebookId')
+
+ const notes = await prisma.note.findMany({
+ where: {
+ userId,
+ trashedAt: null,
+ ...(notebookId ? { notebookId } : {}),
+ },
+ select: {
+ id: true,
+ title: true,
+ updatedAt: true,
+ color: true,
+ notebook: { select: { name: true } },
+ },
+ orderBy: { updatedAt: 'desc' },
+ take: 50,
+ })
+
+ const notebookName = notebookId
+ ? (await prisma.notebook.findUnique({ where: { id: notebookId }, select: { name: true } }))?.name
+ : undefined
+
+ return NextResponse.json({
+ notes: notes.map((n) => ({
+ id: n.id,
+ title: n.title,
+ updatedAt: n.updatedAt,
+ color: n.color,
+ notebookName: n.notebook?.name,
+ })),
+ ...(notebookName ? { notebookName } : {}),
+ })
+}
diff --git a/memento-note/app/api/mobile/search/route.ts b/memento-note/app/api/mobile/search/route.ts
new file mode 100644
index 0000000..cef3d0f
--- /dev/null
+++ b/memento-note/app/api/mobile/search/route.ts
@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from 'next/server'
+import prisma from '@/lib/prisma'
+import { getMobileUserId } from '@/lib/mobile-auth'
+
+export async function GET(req: NextRequest) {
+ const userId = getMobileUserId(req)
+ if (!userId) return NextResponse.json({ error: 'Non autorisé' }, { status: 401 })
+
+ const { searchParams } = new URL(req.url)
+ const q = searchParams.get('q')?.trim()
+ if (!q) return NextResponse.json({ results: [] })
+
+ const notes = await prisma.note.findMany({
+ where: {
+ userId,
+ trashedAt: null,
+ OR: [
+ { title: { contains: q, mode: 'insensitive' } },
+ { content: { contains: q, mode: 'insensitive' } },
+ ],
+ },
+ select: {
+ id: true,
+ title: true,
+ content: true,
+ notebook: { select: { name: true } },
+ },
+ take: 20,
+ orderBy: { updatedAt: 'desc' },
+ })
+
+ const results = notes.map((n) => ({
+ id: n.id,
+ title: n.title,
+ notebookName: n.notebook?.name,
+ snippet: n.content
+ ? n.content.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 160)
+ : '',
+ }))
+
+ return NextResponse.json({ results })
+}
diff --git a/memento-note/components/settings/settings-help-box.tsx b/memento-note/components/settings/settings-help-box.tsx
index 5a0deed..7c4c04c 100644
--- a/memento-note/components/settings/settings-help-box.tsx
+++ b/memento-note/components/settings/settings-help-box.tsx
@@ -21,29 +21,29 @@ export function SettingsHelpBox({ title, steps, defaultOpen = false, className }
const [open, setOpen] = useState(defaultOpen)
return (
-
+
{open && (
-
+
{steps.map((step, i) => (
-
-
+
{step.icon ?? i + 1}
-
+
{step.text}
{step.link && (
<>
@@ -52,7 +52,7 @@ export function SettingsHelpBox({ title, steps, defaultOpen = false, className }
href={step.link.href}
target="_blank"
rel="noopener noreferrer"
- className="inline-flex items-center gap-0.5 underline underline-offset-2 font-medium hover:text-blue-600"
+ className="inline-flex items-center gap-0.5 text-brand-accent underline underline-offset-2 font-medium hover:opacity-80"
>
{step.link.label}
diff --git a/memento-note/lib/mobile-auth.ts b/memento-note/lib/mobile-auth.ts
new file mode 100644
index 0000000..6a68867
--- /dev/null
+++ b/memento-note/lib/mobile-auth.ts
@@ -0,0 +1,37 @@
+/**
+ * Mobile auth helper — validates Bearer token and returns userId
+ * Token format: base64(userId:timestamp:hmac)
+ */
+import { createHmac } from 'crypto'
+
+const SECRET = process.env.NEXTAUTH_SECRET || 'fallback-secret'
+
+export function createMobileToken(userId: string): string {
+ const ts = Date.now()
+ const payload = `${userId}:${ts}`
+ const sig = createHmac('sha256', SECRET).update(payload).digest('hex').slice(0, 16)
+ return Buffer.from(`${payload}:${sig}`).toString('base64url')
+}
+
+export function verifyMobileToken(token: string): string | null {
+ try {
+ const decoded = Buffer.from(token, 'base64url').toString('utf-8')
+ const parts = decoded.split(':')
+ if (parts.length !== 3) return null
+ const [userId, ts, sig] = parts
+ const payload = `${userId}:${ts}`
+ const expected = createHmac('sha256', SECRET).update(payload).digest('hex').slice(0, 16)
+ if (sig !== expected) return null
+ // Token valid for 90 days
+ if (Date.now() - Number(ts) > 90 * 24 * 60 * 60 * 1000) return null
+ return userId
+ } catch {
+ return null
+ }
+}
+
+export function getMobileUserId(request: Request): string | null {
+ const auth = request.headers.get('Authorization')
+ if (!auth?.startsWith('Bearer ')) return null
+ return verifyMobileToken(auth.slice(7))
+}