diff --git a/functions/helpers/firebase.js b/functions/helpers/firebase.js new file mode 100644 index 0000000..ef922c8 --- /dev/null +++ b/functions/helpers/firebase.js @@ -0,0 +1,14 @@ +const admin = require("firebase-admin"); + +async function deleteFolder(path) { + try { + console.log(`Deleting folder: ${path}`); + const bucket = admin.storage().bucket(); + await bucket.deleteFiles({ prefix: path, force: true }); + console.log(`Folder ${path} deleted successfully`); + } catch (e) { + console.log(e); + } +} + +exports.deleteFolder = deleteFolder; diff --git a/functions/helpers/gemini.js b/functions/helpers/gemini.js index 237900c..4e596a9 100644 --- a/functions/helpers/gemini.js +++ b/functions/helpers/gemini.js @@ -164,7 +164,7 @@ ${policy} return output; }; -exports.generateImageV2 = async (prompt, size = 1024) => { +exports.generateImageV2 = async (prompt, size = 1024, path = "") => { if (typeof prompt !== "string" || prompt.trim().length < 1) { throw new Error("Vous devez spécifier un prompt (string) non vide."); } @@ -280,14 +280,7 @@ exports.generateImageV2 = async (prompt, size = 1024) => { const buffer = Buffer.from(b64, "base64"); const bucket = admin.storage().bucket(); - const ext = - mimeType === "image/jpeg" - ? "jpg" - : mimeType === "image/webp" - ? "webp" - : "png"; const token = require("crypto").randomUUID(); - const path = `generated/images/${Date.now()}-${token}.${ext}`; console.log( "💾 [generateImageV2] Sauvegarde de l'image dans Firebase Storage", @@ -327,6 +320,7 @@ exports.CombineCoverAndPicture = async ( backgroundImage, foregroundImage, options = {}, + path = "", ) => { const size = Number(options.size || 1024); console.log("🧩 [CombineCoverAndPicture] Start", { size }); @@ -441,14 +435,7 @@ Tu reçois 2 images: la première est l'arrière-plan, la seconde est une photo const buffer = Buffer.from(b64, "base64"); const bucket = admin.storage().bucket(); - const ext = - mimeType === "image/jpeg" - ? "jpg" - : mimeType === "image/webp" - ? "webp" - : "png"; const token = require("crypto").randomUUID(); - const path = `generated/combined/${Date.now()}-${token}.${ext}`; await bucket.file(path).save(buffer, { resumable: false, diff --git a/functions/index.js b/functions/index.js index 60c7eba..26dd9b9 100644 --- a/functions/index.js +++ b/functions/index.js @@ -6,12 +6,14 @@ admin.initializeApp(); exports.refList = { projects: admin.firestore().collection("projects"), + playlists: admin.firestore().collection("playlists"), + tasks: admin.firestore().collection("tasks"), }; // Exporter toutes les fonctions +exports.users = require("./src/users"); exports.music = require("./src/music"); exports.lyrics = require("./src/lyrics"); exports.cover = require("./src/cover"); exports.projects = require("./src/project"); -exports.hls = require("./src/hls"); exports.thumbnail = require("./src/thumbnail"); diff --git a/functions/src/cover.js b/functions/src/cover.js index 19ad034..516691a 100644 --- a/functions/src/cover.js +++ b/functions/src/cover.js @@ -21,7 +21,11 @@ async function performCoverGeneration(project) { let coverUrl = ""; try { // Utiliser une taille fixe de 1024 pixels - coverUrl = await generateImageV2(prompt, 1024); + coverUrl = await generateImageV2( + prompt, + 1024, + `users/${project.userId}/projects/${project.id}/generated-${Date.now()}.png`, + ); console.log("coverUrl", coverUrl); } catch (e) { logger.error("❌ [Cover] generateImageV2 failed", { @@ -73,7 +77,11 @@ async function performCombineGeneration(project) { fgPreview: String(foreground).slice(0, 80), }); - const combinedUrl = await CombineCoverAndPicture(background, foreground); + const combinedUrl = await CombineCoverAndPicture( + background, + foreground, + `users/${project.userId}/projects/${project.id}/combine-${Date.now()}.png`, + ); // Sauvegarder le résultat et marquer comme généré await db diff --git a/functions/src/hls.js b/functions/src/hls.js deleted file mode 100644 index 172bafc..0000000 --- a/functions/src/hls.js +++ /dev/null @@ -1,161 +0,0 @@ -const { onObjectFinalized } = require("firebase-functions/v2/storage"); -const functions = require("firebase-functions"); -const admin = require("firebase-admin"); -const path = require("path"); -const os = require("os"); -const fs = require("fs"); - -// These deps must be installed in functions/: ffmpeg-static, fluent-ffmpeg -let ffmpeg; -let ffmpegPath; -try { - ffmpeg = require("fluent-ffmpeg"); - ffmpegPath = require("ffmpeg-static"); - ffmpeg.setFfmpegPath(ffmpegPath); -} catch (e) { - console.log( - "ffmpeg modules not installed yet — deploy after npm i", - e?.message - ); -} - -const bucket = admin.storage().bucket(); - -exports.transcodePlaybackToHLS = onObjectFinalized( - { - region: "europe-west1", - memory: "2GiB", - timeoutSeconds: 540, - cpu: 2, - }, - async (event) => { - const object = event.data; - const filePath = object?.name || ""; // e.g., musics/{projectId}/source.mp4 - const contentType = object?.contentType || ""; - - try { - // Only process our target path + video files - const match = filePath.match(/^musics\/(.+?)\/source\.mp4$/i); - if (!match || !contentType.startsWith("video/")) { - return; - } - const projectId = match[1]; - const tempDir = fs.mkdtempSync( - path.join(os.tmpdir(), `hls_${projectId}_`) - ); - const localSource = path.join(tempDir, "source.mp4"); - const hlsDir = path.join(tempDir, "hls"); - fs.mkdirSync(hlsDir); - - // Download source - await bucket.file(filePath).download({ destination: localSource }); - - if (!ffmpeg) { - console.warn("ffmpeg not available; skip transcode"); - return; - } - - // Transcode to HLS (single rendition for simplicity) - const masterPath = path.join(hlsDir, "master.m3u8"); - const segmentPattern = path.join(hlsDir, "segment_%03d.ts"); - - await new Promise((resolve, reject) => { - ffmpeg(localSource) - .outputOptions([ - "-profile:v baseline", - "-level 3.0", - "-start_number 0", - "-hls_time 4", - "-hls_list_size 0", - "-f hls", - `-hls_segment_filename ${segmentPattern}`, - ]) - .output(masterPath) - .on("end", resolve) - .on("error", reject) - .run(); - }); - - // Upload all files in hlsDir to Storage under musics/{projectId}/hls/ - const fileNames = fs.readdirSync(hlsDir); - const destPrefix = `musics/${projectId}/hls/`; - - // Upload all segments first - const uploads = fileNames - .filter((n) => n.endsWith(".ts")) - .map((name) => { - const local = path.join(hlsDir, name); - const dest = `${destPrefix}${name}`; - return bucket.upload(local, { - destination: dest, - metadata: { - contentType: "video/MP2T", - cacheControl: "public, max-age=3600", - }, - }); - }); - await Promise.all(uploads); - - // Generate signed URLs for segments and rewrite playlist with absolute URLs - const segmentNames = fileNames.filter((n) => n.endsWith(".ts")); - const signedSegments = {}; - await Promise.all( - segmentNames.map(async (name) => { - const file = bucket.file(`${destPrefix}${name}`); - const [url] = await file.getSignedUrl({ - action: "read", - expires: Date.now() + 1000 * 60 * 60 * 24 * 365, - }); - signedSegments[name] = url; - }) - ); - - // Rewrite master playlist to use absolute signed segment URLs - let masterContent = fs.readFileSync(masterPath, "utf8"); - masterContent = masterContent - .split(/\r?\n/) - .map((line) => - line.endsWith(".ts") && signedSegments[line] - ? signedSegments[line] - : line - ) - .join("\n"); - const rewrittenMasterLocal = path.join(hlsDir, "master.abs.m3u8"); - fs.writeFileSync(rewrittenMasterLocal, masterContent, "utf8"); - - // Upload rewritten playlist - await bucket.upload(rewrittenMasterLocal, { - destination: `${destPrefix}master.m3u8`, - metadata: { - contentType: "application/vnd.apple.mpegurl", - cacheControl: "public, max-age=3600", - }, - }); - - // Compute signed URL for playlist - const [signedUrl] = await bucket - .file(`${destPrefix}master.m3u8`) - .getSignedUrl({ - action: "read", - expires: Date.now() + 1000 * 60 * 60 * 24 * 365, - }); - - // Update Firestore project doc with playbackUrl - await admin.firestore().collection("projects").doc(projectId).set( - { - playbackUrl: signedUrl, - playbackStatus: "ready", - updatedAt: admin.firestore.FieldValue.serverTimestamp(), - }, - { merge: true } - ); - - // Cleanup tmp dir - try { - fs.rmSync(tempDir, { recursive: true, force: true }); - } catch (e) {} - } catch (e) { - console.error("HLS transcode error", e); - } - } -); diff --git a/functions/src/music.js b/functions/src/music.js index 5b3112a..54356c9 100644 --- a/functions/src/music.js +++ b/functions/src/music.js @@ -490,18 +490,16 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { // 1) Récupérer le projectId associé au taskId let projectId = null; - try { - const projSnap = await db - .collection("projects") - .where("sunoTaskId", "==", taskId) - .limit(1) - .get(); - if (!projSnap.empty) { - projectId = projSnap.docs[0].id; - } - } catch (e) { - console.error("❌ [SunoCallback] Erreur lookup project par taskId:", e); + const projSnap = await db + .collection("projects") + .where("sunoTaskId", "==", taskId) + .limit(1) + .get(); + if (projSnap.empty) { + throw new Error("Aucun projet trouvé pour ce taskId"); } + projectId = projSnap.docs[0].id; + const { userId } = projSnap.docs[0].data() || {}; if (!projectId) { console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId }); @@ -540,7 +538,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { console.log(`⬇️ [SunoCallback] Téléchargement piste ${index + 1}`); const resp = await axios.get(url, { responseType: "arraybuffer" }); const buffer = Buffer.from(resp.data); - const path = `musics/${projectId}/sound${index + 1}.mp3`; + const path = `users/${userId}/projects/${projectId}/task-${taskId}-${index + 1}.mp3`; const token = require("crypto").randomUUID(); const file = bucket.file(path); await file.save(buffer, { diff --git a/functions/src/project.js b/functions/src/project.js index a451dda..4e740e3 100644 --- a/functions/src/project.js +++ b/functions/src/project.js @@ -1,5 +1,8 @@ +const admin = require("firebase-admin"); const { onDocumentWritten } = require("firebase-functions/firestore"); +const { refList } = require("../index"); const { getSunoTimestamps } = require("./lyrics"); +const { deleteFolder } = require("../helpers/firebase"); exports.onProjectUpdate = onDocumentWritten( "projects/{projectId}", @@ -13,6 +16,24 @@ exports.onProjectUpdate = onDocumentWritten( if (!previousData?.songUrl && currentData?.songUrl) { await getSunoTimestamps(projectId); } + + if (!currentData) { + const { userId } = previousData || {}; + + //delete folder + await deleteFolder(`users/${userId}/projects/${projectId}/`); + + //delete tasks + const snapshot = await refList.tasks + .where("projectId", "==", projectId) + .get(); + if (snapshot.empty) return null; + const batch = admin.firestore().batch(); + snapshot.forEach((doc) => { + batch.delete(doc.ref); + }); + await batch.commit(); + } } catch (e) { console.log(e); } diff --git a/functions/src/thumbnail.js b/functions/src/thumbnail.js index 6d3db99..f55d812 100644 --- a/functions/src/thumbnail.js +++ b/functions/src/thumbnail.js @@ -35,7 +35,7 @@ exports.generateVideoThumbnail = onObjectFinalized( // Use unique folder under /tmp to avoid name collisions const tmpDir = path.join( os.tmpdir(), - `thumb-${Date.now()}-${Math.round(Math.random() * 1000)}` + `thumb-${Date.now()}-${Math.round(Math.random() * 1000)}`, ); const baseName = path.basename(objectName); const dirName = path.posix.dirname(objectName); @@ -96,7 +96,7 @@ exports.generateVideoThumbnail = onObjectFinalized( thumbnailUrl, updatedAt: admin.firestore.FieldValue.serverTimestamp(), }, - { merge: true } + { merge: true }, ); } @@ -115,5 +115,5 @@ exports.generateVideoThumbnail = onObjectFinalized( } finally { await fs.rm(tmpDir, { recursive: true, force: true }); } - } + }, ); diff --git a/functions/src/users.js b/functions/src/users.js new file mode 100644 index 0000000..2156fb3 --- /dev/null +++ b/functions/src/users.js @@ -0,0 +1,36 @@ +const admin = require("firebase-admin"); +const { onDocumentDeleted } = require("firebase-functions/firestore"); +const { refList } = require("../index"); +const { deleteFolder } = require("../helpers/firebase"); + +exports.onUserDelete = onDocumentDeleted("users/{userID}", async (event) => { + try { + const userID = event?.params?.userID; + await clearAllUserData(userID); + + await deleteFolder(`users/${userID}/`); + + await admin.auth().deleteUser(userID); + console.log(`User ${userID} deleted successfully`); + } catch (e) { + console.log(e); + } +}); + +async function clearAllUserData(userID) { + const deleteAll = async (ref, key, operator = "==") => { + const snapshot = await ref.where(key, operator, userID).get(); + snapshot.forEach((item) => item.ref.delete()); + }; + const removeFromArray = async (ref, arrayName) => { + const snapshot = await ref.where(arrayName, "array-contains", userID).get(); + snapshot.forEach((item) => + item.ref.update({ + [arrayName]: admin.firestore.FieldValue.arrayRemove(userID), + }), + ); + }; + + await deleteAll(refList.projects, "userId"); + await deleteAll(refList.playlists, "createdBy"); +} diff --git a/src/assets/UI/loginBgWeb.png b/src/assets/UI/loginBgWeb.png new file mode 100644 index 0000000..bbfe083 Binary files /dev/null and b/src/assets/UI/loginBgWeb.png differ diff --git a/src/assets/index.js b/src/assets/index.js index 7611fc5..eba13fb 100644 --- a/src/assets/index.js +++ b/src/assets/index.js @@ -223,6 +223,7 @@ export const background = { hitParadeBG, homeBG, homeBGWeb: require("./UI/homeBGWeb.png"), + loginBgWeb: require("./UI/loginBgWeb.png"), }; export const ai = { diff --git a/src/components/FeatureCarousel/FeatureCarousel.js b/src/components/FeatureCarousel/FeatureCarousel.js index 081c5ef..1c9de9a 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.js +++ b/src/components/FeatureCarousel/FeatureCarousel.js @@ -1,5 +1,17 @@ -import React, { useCallback, useEffect, useMemo, useRef } from "react"; -import { FlatList, StyleSheet, View, useWindowDimensions } from "react-native"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { + FlatList, + Platform, + StyleSheet, + View, + useWindowDimensions, +} from "react-native"; import PersonaCard from "../cards/PersonaCard/PersonaCard"; import { ai } from "../../assets"; import { getCreationStageStates } from "../../utils/projectStages"; @@ -53,29 +65,207 @@ const FeatureCarousel = ({ })), [stageStates], ); + const { height: windowHeight } = useWindowDimensions(); - const itemHeight = Math.max(windowHeight, 1); + const isWeb = Platform.OS === "web"; + + const [viewportHeight, setViewportHeight] = useState(() => + Math.max(windowHeight, 1), + ); + + const updateSnapHeight = useCallback((height) => { + if (!height || Number.isNaN(height)) { + return; + } + setViewportHeight((prev) => { + if (prev == null || Math.abs(prev - height) > 0.5) { + return height; + } + return prev; + }); + }, []); + + useEffect(() => { + updateSnapHeight(Math.max(windowHeight, 1)); + }, [updateSnapHeight, windowHeight]); + + const itemHeight = Math.max(viewportHeight, 1); const listRef = useRef(null); const pendingScrollRef = useRef(false); - const activeIndexRef = useRef(activeIndex); + const alignTimeoutRef = useRef(null); + const activeIndexRef = useRef( + typeof activeIndex === "number" ? activeIndex : 0, + ); const onActiveIndexChangeRef = useRef(onActiveIndexChange); - useEffect(() => { - activeIndexRef.current = activeIndex; - }, [activeIndex]); - useEffect(() => { onActiveIndexChangeRef.current = onActiveIndexChange; }, [onActiveIndexChange]); + useEffect(() => { + if (typeof activeIndex === "number") { + activeIndexRef.current = activeIndex; + } + }, [activeIndex]); + + const clampIndex = useCallback( + (index) => { + if (!carouselItems.length) { + return 0; + } + if (index < 0) { + return 0; + } + if (index >= carouselItems.length) { + return carouselItems.length - 1; + } + return index; + }, + [carouselItems.length], + ); + + const clearPendingAlignment = useCallback(() => { + if (alignTimeoutRef.current != null) { + clearTimeout(alignTimeoutRef.current); + alignTimeoutRef.current = null; + } + }, []); + + useEffect(() => () => clearPendingAlignment(), [clearPendingAlignment]); + + const scrollToIndex = useCallback( + (index, animated = true, heightOverride) => { + const ref = listRef.current; + if (!ref) { + return; + } + + const clamped = clampIndex(index); + const height = + heightOverride && heightOverride > 0 ? heightOverride : itemHeight; + + if (isWeb) { + if (!height) { + return; + } + updateSnapHeight(height); + try { + ref.scrollToOffset({ offset: clamped * height, animated: false }); + } catch (_error) { + // Ignore scroll errors when list is not ready yet. + } + activeIndexRef.current = clamped; + return; + } + + try { + pendingScrollRef.current = !!animated; + ref.scrollToIndex({ index: clamped, animated }); + activeIndexRef.current = clamped; + } catch (_error) { + pendingScrollRef.current = false; + } + }, + [clampIndex, isWeb, itemHeight, updateSnapHeight], + ); + + useEffect(() => { + if ( + listRef.current == null || + typeof activeIndex !== "number" || + activeIndex < 0 || + activeIndex >= carouselItems.length || + (isWeb && !itemHeight) + ) { + return; + } + scrollToIndex(activeIndex); + }, [activeIndex, carouselItems.length, isWeb, itemHeight, scrollToIndex]); + + useEffect(() => { + if (listRef.current == null || (isWeb && !itemHeight)) { + return; + } + scrollToIndex(activeIndexRef.current, false); + }, [carouselItems.length, isWeb, itemHeight, scrollToIndex]); + + const alignToOffset = useCallback( + (offset, layoutHeight) => { + const height = + layoutHeight && layoutHeight > 0 ? layoutHeight : itemHeight; + if (!height) { + return; + } + + updateSnapHeight(height); + + const nextIndex = clampIndex(Math.round(offset / height)); + const hasChanged = nextIndex !== activeIndexRef.current; + + if (hasChanged) { + activeIndexRef.current = nextIndex; + const callback = onActiveIndexChangeRef.current; + if (callback) { + callback(nextIndex); + } + } + + if (isWeb || hasChanged) { + scrollToIndex(nextIndex, !isWeb, height); + } + }, + [clampIndex, isWeb, itemHeight, scrollToIndex, updateSnapHeight], + ); + + const handleScrollEnd = useCallback( + (event) => { + clearPendingAlignment(); + const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0; + const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height; + alignToOffset(offsetY, layoutHeight); + }, + [alignToOffset, clearPendingAlignment], + ); + + const handleScroll = useCallback( + (event) => { + if (!isWeb) { + return; + } + const offsetY = event?.nativeEvent?.contentOffset?.y ?? 0; + const layoutHeight = event?.nativeEvent?.layoutMeasurement?.height; + clearPendingAlignment(); + alignTimeoutRef.current = setTimeout(() => { + alignToOffset(offsetY, layoutHeight); + alignTimeoutRef.current = null; + }, 80); + }, + [alignToOffset, clearPendingAlignment, isWeb], + ); + + const handleLayout = useCallback( + (event) => { + const layoutHeight = event?.nativeEvent?.layout?.height ?? 0; + if (layoutHeight > 0) { + updateSnapHeight(layoutHeight); + } + }, + [updateSnapHeight], + ); + const keyExtractor = useCallback((item) => item.key, []); const renderItem = useCallback( ({ item, index }) => ( - + ), - [], + [itemHeight], ); const getItemLayout = useCallback( @@ -108,26 +298,15 @@ const FeatureCarousel = ({ }; } - useEffect(() => { - if ( - listRef.current == null || - typeof activeIndex !== "number" || - activeIndex < 0 || - activeIndex >= carouselItems.length - ) { - return; + const snapOffsets = useMemo(() => { + if (!itemHeight || !isWeb) { + return undefined; } - try { - pendingScrollRef.current = true; - listRef.current.scrollToIndex({ index: activeIndex, animated: true }); - } catch (_error) { - pendingScrollRef.current = false; - // Ignore scroll errors when list is not ready yet. - } - }, [activeIndex, carouselItems.length]); + return carouselItems.map((_, index) => index * itemHeight); + }, [carouselItems.length, isWeb, itemHeight]); return ( - + ); @@ -162,11 +347,4 @@ const styles = StyleSheet.create({ list: { flex: 1, }, - listContent: { - flexGrow: 1, - }, - cardRight: { - flexDirection: "row-reverse", - alignItems: "center", - }, }); diff --git a/src/components/ProjectDropDown/ProjectDropDown.js b/src/components/ProjectDropDown/ProjectDropDown.js index d3b2008..b6049e8 100644 --- a/src/components/ProjectDropDown/ProjectDropDown.js +++ b/src/components/ProjectDropDown/ProjectDropDown.js @@ -14,8 +14,6 @@ import { FONT_FAMILY } from "../../styles/Fonts"; import GradientButton from "../GradientButton"; const BUTTON_BLUR_INTENSITY = 20; -const ROW_BLUR_INTENSITY = 5; -const ROW_BLUR_SELECTED_INTENSITY = 45; const defaultFormatDate = (timestamp) => { try { @@ -191,7 +189,6 @@ export default ProjectDropDown; const ProjectRow = ({ project, formatDate, - isSelected, onSelect, onModify, isTitle = false, @@ -252,13 +249,7 @@ const ProjectRow = ({ source={coverUri ? { uri: coverUri } : img.placeholder} style={styles.projectImage} /> - + {title} diff --git a/src/components/cards/PersonaCard/PersonaCard.js b/src/components/cards/PersonaCard/PersonaCard.js index 6efcc70..436fa64 100644 --- a/src/components/cards/PersonaCard/PersonaCard.js +++ b/src/components/cards/PersonaCard/PersonaCard.js @@ -1,5 +1,5 @@ import React from "react"; -import { Text, useWindowDimensions, View } from "react-native"; +import { Platform, Text, useWindowDimensions, View } from "react-native"; import { Image } from "expo-image"; import { BlurView } from "expo-blur"; import { gutters, Palette } from "../../../styles"; @@ -7,16 +7,17 @@ import { FONT_FAMILY } from "../../../styles/Fonts"; import FontAwesome from "@expo/vector-icons/FontAwesome"; import palette from "../../../styles/Palette"; -export default function PersonaCard({ item, isLock = false }) { +export default function PersonaCard({ item, isLock = false, height }) { const { height: windowHeight } = useWindowDimensions(); + const baseHeight = Math.max(height ?? windowHeight, 1); + const cardHeight = Platform.OS === "web" ? Math.round(baseHeight) : baseHeight; return ( { WebBrowser.maybeCompleteAuthSession(); - const [, setTooltip] = useGlobal('_tooltip'); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); + const [, setTooltip] = useGlobal("_tooltip"); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); // Let the provider compute a compliant redirect URI for native (com.googleusercontent.apps.:/oauth2redirect) // Avoid forcing a custom scheme like musicland:// which Google can reject for native apps. @@ -39,12 +39,12 @@ export default ({ navigation }) => { // Use Authorization Code + PKCE to comply with Google OAuth for native apps responseType: AuthSession.ResponseType.Code, usePKCE: true, - scopes: ['openid', 'profile', 'email'], + scopes: ["openid", "profile", "email"], }); const afterLoginNavigate = async () => { const uid = firebase.auth().currentUser?.uid; - if (!uid) throw new Error('Aucun utilisateur après connexion'); + if (!uid) throw new Error("Aucun utilisateur après connexion"); const snap = await usersRef.doc(uid).get(); const hasUserName = !!snap.data()?.userName; if (hasUserName) { @@ -60,8 +60,8 @@ export default ({ navigation }) => { await firebase.auth().signInWithEmailAndPassword(email.trim(), password); await afterLoginNavigate(); } catch (e) { - console.log('Login error', e?.message); - setTooltip({ text: e?.message || 'Connexion impossible', type: 'error' }); + console.log("Login error", e?.message); + setTooltip({ text: e?.message || "Connexion impossible", type: "error" }); } finally { setLoading(false); } @@ -72,24 +72,24 @@ export default ({ navigation }) => { setLoading(true); if (isWeb) { const provider = new firebase.auth.GoogleAuthProvider(); - provider.addScope('profile'); - provider.addScope('email'); + provider.addScope("profile"); + provider.addScope("email"); await firebase.auth().signInWithPopup(provider); await afterLoginNavigate(); setLoading(false); } else { // Use defaults from the request; don't override with proxy here const result = await promptAsync(); - if (result?.type !== 'success') { + if (result?.type !== "success") { // Cancelled or errored during the browser flow setLoading(false); } } } catch (e) { - console.log('Google Login error', e?.message); + console.log("Google Login error", e?.message); setTooltip({ - text: e?.message || 'Connexion Google impossible. Merci de réessayer.', - type: 'error', + text: e?.message || "Connexion Google impossible. Merci de réessayer.", + type: "error", }); setLoading(false); } @@ -99,7 +99,7 @@ export default ({ navigation }) => { useEffect(() => { const handleNativeGoogleResponse = async () => { try { - if (response?.type === 'success') { + if (response?.type === "success") { // If using Expo proxy, tokens can be present already. let idToken = response?.authentication?.idToken || response?.params?.id_token; @@ -114,13 +114,13 @@ export default ({ navigation }) => { const clientId = request?.clientId; const discovery = { authorizationEndpoint: - 'https://accounts.google.com/o/oauth2/v2/auth', - tokenEndpoint: 'https://oauth2.googleapis.com/token', - revocationEndpoint: 'https://oauth2.googleapis.com/revoke', + "https://accounts.google.com/o/oauth2/v2/auth", + tokenEndpoint: "https://oauth2.googleapis.com/token", + revocationEndpoint: "https://oauth2.googleapis.com/revoke", }; // Light debug: helps identify redirect/client mismatches in dev. - console.log('Google token exchange', { + console.log("Google token exchange", { clientId, redirectUri: request?.redirectUri, hasCodeVerifier: !!request?.codeVerifier, @@ -139,7 +139,7 @@ export default ({ navigation }) => { } if (!idToken) - throw new Error('Jeton Google manquant (échange/retour)'); + throw new Error("Jeton Google manquant (échange/retour)"); const credential = firebase.auth.GoogleAuthProvider.credential(idToken); @@ -147,10 +147,10 @@ export default ({ navigation }) => { await afterLoginNavigate(); } } catch (e) { - console.log('Google native sign-in error', e?.message); + console.log("Google native sign-in error", e?.message); setTooltip({ - text: e?.message || 'Connexion Google impossible', - type: 'error', + text: e?.message || "Connexion Google impossible", + type: "error", }); } finally { setLoading(false); @@ -165,13 +165,14 @@ export default ({ navigation }) => { return ( - + { fontFamily: FONT_FAMILY.InterSemiBold, }} > - Bonjour ! + {"Bonjour !"} { { @@ -264,7 +265,7 @@ export default ({ navigation }) => { fontFamily: FONT_FAMILY.HelveticaNeueRegular, }} > - Pas encore de compte?{' '} + Pas encore de compte?{" "} { setFollowers( Array.isArray(currentUserData?.followedBy) ? currentUserData.followedBy.length - : 0 + : 0, ); return; } @@ -263,7 +263,12 @@ const Profile = () => { // } > {isWeb && ( - SheetManager.show("ProfileSettings")}> + SheetManager.show("ProfileSettings")} + > { const [email, setEmail] = useState(""); return (