fix list and improve cloud functions
This commit is contained in:
@@ -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;
|
||||||
@@ -164,7 +164,7 @@ ${policy}
|
|||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.generateImageV2 = async (prompt, size = 1024) => {
|
exports.generateImageV2 = async (prompt, size = 1024, path = "") => {
|
||||||
if (typeof prompt !== "string" || prompt.trim().length < 1) {
|
if (typeof prompt !== "string" || prompt.trim().length < 1) {
|
||||||
throw new Error("Vous devez spécifier un prompt (string) non vide.");
|
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 buffer = Buffer.from(b64, "base64");
|
||||||
const bucket = admin.storage().bucket();
|
const bucket = admin.storage().bucket();
|
||||||
const ext =
|
|
||||||
mimeType === "image/jpeg"
|
|
||||||
? "jpg"
|
|
||||||
: mimeType === "image/webp"
|
|
||||||
? "webp"
|
|
||||||
: "png";
|
|
||||||
const token = require("crypto").randomUUID();
|
const token = require("crypto").randomUUID();
|
||||||
const path = `generated/images/${Date.now()}-${token}.${ext}`;
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"💾 [generateImageV2] Sauvegarde de l'image dans Firebase Storage",
|
"💾 [generateImageV2] Sauvegarde de l'image dans Firebase Storage",
|
||||||
@@ -327,6 +320,7 @@ exports.CombineCoverAndPicture = async (
|
|||||||
backgroundImage,
|
backgroundImage,
|
||||||
foregroundImage,
|
foregroundImage,
|
||||||
options = {},
|
options = {},
|
||||||
|
path = "",
|
||||||
) => {
|
) => {
|
||||||
const size = Number(options.size || 1024);
|
const size = Number(options.size || 1024);
|
||||||
console.log("🧩 [CombineCoverAndPicture] Start", { size });
|
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 buffer = Buffer.from(b64, "base64");
|
||||||
const bucket = admin.storage().bucket();
|
const bucket = admin.storage().bucket();
|
||||||
const ext =
|
|
||||||
mimeType === "image/jpeg"
|
|
||||||
? "jpg"
|
|
||||||
: mimeType === "image/webp"
|
|
||||||
? "webp"
|
|
||||||
: "png";
|
|
||||||
const token = require("crypto").randomUUID();
|
const token = require("crypto").randomUUID();
|
||||||
const path = `generated/combined/${Date.now()}-${token}.${ext}`;
|
|
||||||
|
|
||||||
await bucket.file(path).save(buffer, {
|
await bucket.file(path).save(buffer, {
|
||||||
resumable: false,
|
resumable: false,
|
||||||
|
|||||||
+3
-1
@@ -6,12 +6,14 @@ admin.initializeApp();
|
|||||||
|
|
||||||
exports.refList = {
|
exports.refList = {
|
||||||
projects: admin.firestore().collection("projects"),
|
projects: admin.firestore().collection("projects"),
|
||||||
|
playlists: admin.firestore().collection("playlists"),
|
||||||
|
tasks: admin.firestore().collection("tasks"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Exporter toutes les fonctions
|
// Exporter toutes les fonctions
|
||||||
|
exports.users = require("./src/users");
|
||||||
exports.music = require("./src/music");
|
exports.music = require("./src/music");
|
||||||
exports.lyrics = require("./src/lyrics");
|
exports.lyrics = require("./src/lyrics");
|
||||||
exports.cover = require("./src/cover");
|
exports.cover = require("./src/cover");
|
||||||
exports.projects = require("./src/project");
|
exports.projects = require("./src/project");
|
||||||
exports.hls = require("./src/hls");
|
|
||||||
exports.thumbnail = require("./src/thumbnail");
|
exports.thumbnail = require("./src/thumbnail");
|
||||||
|
|||||||
+10
-2
@@ -21,7 +21,11 @@ async function performCoverGeneration(project) {
|
|||||||
let coverUrl = "";
|
let coverUrl = "";
|
||||||
try {
|
try {
|
||||||
// Utiliser une taille fixe de 1024 pixels
|
// 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);
|
console.log("coverUrl", coverUrl);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("❌ [Cover] generateImageV2 failed", {
|
logger.error("❌ [Cover] generateImageV2 failed", {
|
||||||
@@ -73,7 +77,11 @@ async function performCombineGeneration(project) {
|
|||||||
fgPreview: String(foreground).slice(0, 80),
|
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é
|
// Sauvegarder le résultat et marquer comme généré
|
||||||
await db
|
await db
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
@@ -490,18 +490,16 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
|||||||
|
|
||||||
// 1) Récupérer le projectId associé au taskId
|
// 1) Récupérer le projectId associé au taskId
|
||||||
let projectId = null;
|
let projectId = null;
|
||||||
try {
|
|
||||||
const projSnap = await db
|
const projSnap = await db
|
||||||
.collection("projects")
|
.collection("projects")
|
||||||
.where("sunoTaskId", "==", taskId)
|
.where("sunoTaskId", "==", taskId)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.get();
|
.get();
|
||||||
if (!projSnap.empty) {
|
if (projSnap.empty) {
|
||||||
|
throw new Error("Aucun projet trouvé pour ce taskId");
|
||||||
|
}
|
||||||
projectId = projSnap.docs[0].id;
|
projectId = projSnap.docs[0].id;
|
||||||
}
|
const { userId } = projSnap.docs[0].data() || {};
|
||||||
} catch (e) {
|
|
||||||
console.error("❌ [SunoCallback] Erreur lookup project par taskId:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId });
|
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}`);
|
console.log(`⬇️ [SunoCallback] Téléchargement piste ${index + 1}`);
|
||||||
const resp = await axios.get(url, { responseType: "arraybuffer" });
|
const resp = await axios.get(url, { responseType: "arraybuffer" });
|
||||||
const buffer = Buffer.from(resp.data);
|
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 token = require("crypto").randomUUID();
|
||||||
const file = bucket.file(path);
|
const file = bucket.file(path);
|
||||||
await file.save(buffer, {
|
await file.save(buffer, {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
const admin = require("firebase-admin");
|
||||||
const { onDocumentWritten } = require("firebase-functions/firestore");
|
const { onDocumentWritten } = require("firebase-functions/firestore");
|
||||||
|
const { refList } = require("../index");
|
||||||
const { getSunoTimestamps } = require("./lyrics");
|
const { getSunoTimestamps } = require("./lyrics");
|
||||||
|
const { deleteFolder } = require("../helpers/firebase");
|
||||||
|
|
||||||
exports.onProjectUpdate = onDocumentWritten(
|
exports.onProjectUpdate = onDocumentWritten(
|
||||||
"projects/{projectId}",
|
"projects/{projectId}",
|
||||||
@@ -13,6 +16,24 @@ exports.onProjectUpdate = onDocumentWritten(
|
|||||||
if (!previousData?.songUrl && currentData?.songUrl) {
|
if (!previousData?.songUrl && currentData?.songUrl) {
|
||||||
await getSunoTimestamps(projectId);
|
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) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
|||||||
// Use unique folder under /tmp to avoid name collisions
|
// Use unique folder under /tmp to avoid name collisions
|
||||||
const tmpDir = path.join(
|
const tmpDir = path.join(
|
||||||
os.tmpdir(),
|
os.tmpdir(),
|
||||||
`thumb-${Date.now()}-${Math.round(Math.random() * 1000)}`
|
`thumb-${Date.now()}-${Math.round(Math.random() * 1000)}`,
|
||||||
);
|
);
|
||||||
const baseName = path.basename(objectName);
|
const baseName = path.basename(objectName);
|
||||||
const dirName = path.posix.dirname(objectName);
|
const dirName = path.posix.dirname(objectName);
|
||||||
@@ -96,7 +96,7 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
|||||||
thumbnailUrl,
|
thumbnailUrl,
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{ merge: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,5 +115,5 @@ exports.generateVideoThumbnail = onObjectFinalized(
|
|||||||
} finally {
|
} finally {
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true });
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.1 MiB |
@@ -223,6 +223,7 @@ export const background = {
|
|||||||
hitParadeBG,
|
hitParadeBG,
|
||||||
homeBG,
|
homeBG,
|
||||||
homeBGWeb: require("./UI/homeBGWeb.png"),
|
homeBGWeb: require("./UI/homeBGWeb.png"),
|
||||||
|
loginBgWeb: require("./UI/loginBgWeb.png"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ai = {
|
export const ai = {
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
import React, {
|
||||||
import { FlatList, StyleSheet, View, useWindowDimensions } from "react-native";
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
FlatList,
|
||||||
|
Platform,
|
||||||
|
StyleSheet,
|
||||||
|
View,
|
||||||
|
useWindowDimensions,
|
||||||
|
} from "react-native";
|
||||||
import PersonaCard from "../cards/PersonaCard/PersonaCard";
|
import PersonaCard from "../cards/PersonaCard/PersonaCard";
|
||||||
import { ai } from "../../assets";
|
import { ai } from "../../assets";
|
||||||
import { getCreationStageStates } from "../../utils/projectStages";
|
import { getCreationStageStates } from "../../utils/projectStages";
|
||||||
@@ -53,29 +65,207 @@ const FeatureCarousel = ({
|
|||||||
})),
|
})),
|
||||||
[stageStates],
|
[stageStates],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { height: windowHeight } = useWindowDimensions();
|
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 listRef = useRef(null);
|
||||||
const pendingScrollRef = useRef(false);
|
const pendingScrollRef = useRef(false);
|
||||||
const activeIndexRef = useRef(activeIndex);
|
const alignTimeoutRef = useRef(null);
|
||||||
|
const activeIndexRef = useRef(
|
||||||
|
typeof activeIndex === "number" ? activeIndex : 0,
|
||||||
|
);
|
||||||
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
|
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
activeIndexRef.current = activeIndex;
|
|
||||||
}, [activeIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onActiveIndexChangeRef.current = onActiveIndexChange;
|
onActiveIndexChangeRef.current = onActiveIndexChange;
|
||||||
}, [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 keyExtractor = useCallback((item) => item.key, []);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item, index }) => (
|
({ item, index }) => (
|
||||||
<PersonaCard item={item} index={index} isLock={item.isLocked} />
|
<PersonaCard
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
isLock={item.isLocked}
|
||||||
|
height={itemHeight}
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
[],
|
[itemHeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
const getItemLayout = useCallback(
|
const getItemLayout = useCallback(
|
||||||
@@ -108,26 +298,15 @@ const FeatureCarousel = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const snapOffsets = useMemo(() => {
|
||||||
if (
|
if (!itemHeight || !isWeb) {
|
||||||
listRef.current == null ||
|
return undefined;
|
||||||
typeof activeIndex !== "number" ||
|
|
||||||
activeIndex < 0 ||
|
|
||||||
activeIndex >= carouselItems.length
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
return carouselItems.map((_, index) => index * itemHeight);
|
||||||
pendingScrollRef.current = true;
|
}, [carouselItems.length, isWeb, itemHeight]);
|
||||||
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 (
|
return (
|
||||||
<View style={[styles.container, style]}>
|
<View style={[styles.container, style]} onLayout={handleLayout}>
|
||||||
<FlatList
|
<FlatList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
data={carouselItems}
|
data={carouselItems}
|
||||||
@@ -137,16 +316,22 @@ const FeatureCarousel = ({
|
|||||||
viewabilityConfig={viewabilityConfig.current}
|
viewabilityConfig={viewabilityConfig.current}
|
||||||
getItemLayout={getItemLayout}
|
getItemLayout={getItemLayout}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
pagingEnabled
|
pagingEnabled={!isWeb}
|
||||||
|
bounces={false}
|
||||||
|
overScrollMode="never"
|
||||||
initialNumToRender={1}
|
initialNumToRender={1}
|
||||||
maxToRenderPerBatch={2}
|
maxToRenderPerBatch={2}
|
||||||
windowSize={3}
|
windowSize={3}
|
||||||
scrollEventThrottle={16}
|
scrollEventThrottle={16}
|
||||||
snapToAlignment="start"
|
snapToAlignment={isWeb ? undefined : "start"}
|
||||||
snapToInterval={itemHeight}
|
snapToInterval={!isWeb && itemHeight ? itemHeight : undefined}
|
||||||
decelerationRate="fast"
|
snapToOffsets={snapOffsets}
|
||||||
|
disableIntervalMomentum={!isWeb}
|
||||||
|
decelerationRate={!isWeb ? "fast" : undefined}
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
contentContainerStyle={styles.listContent}
|
onScroll={isWeb ? handleScroll : undefined}
|
||||||
|
onMomentumScrollEnd={handleScrollEnd}
|
||||||
|
onScrollEndDrag={isWeb ? handleScrollEnd : undefined}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@@ -162,11 +347,4 @@ const styles = StyleSheet.create({
|
|||||||
list: {
|
list: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
listContent: {
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
cardRight: {
|
|
||||||
flexDirection: "row-reverse",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ import { FONT_FAMILY } from "../../styles/Fonts";
|
|||||||
import GradientButton from "../GradientButton";
|
import GradientButton from "../GradientButton";
|
||||||
|
|
||||||
const BUTTON_BLUR_INTENSITY = 20;
|
const BUTTON_BLUR_INTENSITY = 20;
|
||||||
const ROW_BLUR_INTENSITY = 5;
|
|
||||||
const ROW_BLUR_SELECTED_INTENSITY = 45;
|
|
||||||
|
|
||||||
const defaultFormatDate = (timestamp) => {
|
const defaultFormatDate = (timestamp) => {
|
||||||
try {
|
try {
|
||||||
@@ -191,7 +189,6 @@ export default ProjectDropDown;
|
|||||||
const ProjectRow = ({
|
const ProjectRow = ({
|
||||||
project,
|
project,
|
||||||
formatDate,
|
formatDate,
|
||||||
isSelected,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onModify,
|
onModify,
|
||||||
isTitle = false,
|
isTitle = false,
|
||||||
@@ -252,13 +249,7 @@ const ProjectRow = ({
|
|||||||
source={coverUri ? { uri: coverUri } : img.placeholder}
|
source={coverUri ? { uri: coverUri } : img.placeholder}
|
||||||
style={styles.projectImage}
|
style={styles.projectImage}
|
||||||
/>
|
/>
|
||||||
<BlurView
|
<BlurView tint="dark" style={styles.projectInfo}>
|
||||||
intensity={
|
|
||||||
isSelected ? ROW_BLUR_SELECTED_INTENSITY : ROW_BLUR_INTENSITY
|
|
||||||
}
|
|
||||||
tint="dark"
|
|
||||||
style={styles.projectInfo}
|
|
||||||
>
|
|
||||||
<View style={styles.projectTexts}>
|
<View style={styles.projectTexts}>
|
||||||
<Text style={styles.projectTitle} numberOfLines={1}>
|
<Text style={styles.projectTitle} numberOfLines={1}>
|
||||||
{title}
|
{title}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
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 { Image } from "expo-image";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { gutters, Palette } from "../../../styles";
|
import { gutters, Palette } from "../../../styles";
|
||||||
@@ -7,16 +7,17 @@ import { FONT_FAMILY } from "../../../styles/Fonts";
|
|||||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||||
import palette from "../../../styles/Palette";
|
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 { height: windowHeight } = useWindowDimensions();
|
||||||
|
const baseHeight = Math.max(height ?? windowHeight, 1);
|
||||||
|
const cardHeight = Platform.OS === "web" ? Math.round(baseHeight) : baseHeight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
height: windowHeight,
|
height: cardHeight,
|
||||||
marginTop: -20,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export default function PersonaCard({ item, index, isLock = true }) {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
height: windowHeight,
|
height: windowHeight,
|
||||||
marginTop: 50,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
|
|||||||
+49
-48
@@ -1,32 +1,32 @@
|
|||||||
import { GoogleSigninButton } from '@react-native-google-signin/google-signin';
|
import { GoogleSigninButton } from "@react-native-google-signin/google-signin";
|
||||||
import * as AuthSession from 'expo-auth-session';
|
import * as AuthSession from "expo-auth-session";
|
||||||
import * as GoogleAuth from 'expo-auth-session/providers/google';
|
import * as GoogleAuth from "expo-auth-session/providers/google";
|
||||||
import * as WebBrowser from 'expo-web-browser';
|
import * as WebBrowser from "expo-web-browser";
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { Pressable, Text, View } from 'react-native';
|
import { Pressable, Text, View } from "react-native";
|
||||||
import { useGlobal } from 'reactn';
|
import { useGlobal } from "reactn";
|
||||||
import { background } from '../assets';
|
import { background } from "../assets";
|
||||||
import GradientButton from '../components/GradientButton.js';
|
import GradientButton from "../components/GradientButton.js";
|
||||||
import { Input } from '../components/Input.js';
|
import { Input } from "../components/Input.js";
|
||||||
import ItemContainer from '../components/ItemContainer/ItemContainer.js';
|
import firebase, { usersRef } from "../config/firebase";
|
||||||
import firebase, { usersRef } from '../config/firebase';
|
|
||||||
import {
|
import {
|
||||||
GOOGLE_ANDROID_CLIENT_ID,
|
GOOGLE_ANDROID_CLIENT_ID,
|
||||||
GOOGLE_IOS_CLIENT_ID,
|
GOOGLE_IOS_CLIENT_ID,
|
||||||
GOOGLE_WEB_CLIENT_ID,
|
GOOGLE_WEB_CLIENT_ID,
|
||||||
} from '../data/keys';
|
} from "../data/keys";
|
||||||
import { isWeb } from '../hooks/useLayoutType';
|
import { isWeb } from "../hooks/useLayoutType";
|
||||||
import Page from '../layouts/Page.js';
|
import Page from "../layouts/Page.js";
|
||||||
import { Routes } from '../navigation';
|
import { Routes } from "../navigation";
|
||||||
import { navigate } from '../navigation/NavigationService.js';
|
import { navigate } from "../navigation/NavigationService.js";
|
||||||
import { FONT_FAMILY } from '../styles/Fonts.js';
|
import { FONT_FAMILY } from "../styles/Fonts.js";
|
||||||
import Palette from '../styles/Palette.js';
|
import Palette from "../styles/Palette.js";
|
||||||
|
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||||
|
|
||||||
export default ({ navigation }) => {
|
export default ({ navigation }) => {
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
WebBrowser.maybeCompleteAuthSession();
|
||||||
const [, setTooltip] = useGlobal('_tooltip');
|
const [, setTooltip] = useGlobal("_tooltip");
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
// Let the provider compute a compliant redirect URI for native (com.googleusercontent.apps.<client-id>:/oauth2redirect)
|
// Let the provider compute a compliant redirect URI for native (com.googleusercontent.apps.<client-id>:/oauth2redirect)
|
||||||
// Avoid forcing a custom scheme like musicland:// which Google can reject for native apps.
|
// 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
|
// Use Authorization Code + PKCE to comply with Google OAuth for native apps
|
||||||
responseType: AuthSession.ResponseType.Code,
|
responseType: AuthSession.ResponseType.Code,
|
||||||
usePKCE: true,
|
usePKCE: true,
|
||||||
scopes: ['openid', 'profile', 'email'],
|
scopes: ["openid", "profile", "email"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const afterLoginNavigate = async () => {
|
const afterLoginNavigate = async () => {
|
||||||
const uid = firebase.auth().currentUser?.uid;
|
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 snap = await usersRef.doc(uid).get();
|
||||||
const hasUserName = !!snap.data()?.userName;
|
const hasUserName = !!snap.data()?.userName;
|
||||||
if (hasUserName) {
|
if (hasUserName) {
|
||||||
@@ -60,8 +60,8 @@ export default ({ navigation }) => {
|
|||||||
await firebase.auth().signInWithEmailAndPassword(email.trim(), password);
|
await firebase.auth().signInWithEmailAndPassword(email.trim(), password);
|
||||||
await afterLoginNavigate();
|
await afterLoginNavigate();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Login error', e?.message);
|
console.log("Login error", e?.message);
|
||||||
setTooltip({ text: e?.message || 'Connexion impossible', type: 'error' });
|
setTooltip({ text: e?.message || "Connexion impossible", type: "error" });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -72,24 +72,24 @@ export default ({ navigation }) => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
const provider = new firebase.auth.GoogleAuthProvider();
|
const provider = new firebase.auth.GoogleAuthProvider();
|
||||||
provider.addScope('profile');
|
provider.addScope("profile");
|
||||||
provider.addScope('email');
|
provider.addScope("email");
|
||||||
await firebase.auth().signInWithPopup(provider);
|
await firebase.auth().signInWithPopup(provider);
|
||||||
await afterLoginNavigate();
|
await afterLoginNavigate();
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} else {
|
} else {
|
||||||
// Use defaults from the request; don't override with proxy here
|
// Use defaults from the request; don't override with proxy here
|
||||||
const result = await promptAsync();
|
const result = await promptAsync();
|
||||||
if (result?.type !== 'success') {
|
if (result?.type !== "success") {
|
||||||
// Cancelled or errored during the browser flow
|
// Cancelled or errored during the browser flow
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Google Login error', e?.message);
|
console.log("Google Login error", e?.message);
|
||||||
setTooltip({
|
setTooltip({
|
||||||
text: e?.message || 'Connexion Google impossible. Merci de réessayer.',
|
text: e?.message || "Connexion Google impossible. Merci de réessayer.",
|
||||||
type: 'error',
|
type: "error",
|
||||||
});
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export default ({ navigation }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleNativeGoogleResponse = async () => {
|
const handleNativeGoogleResponse = async () => {
|
||||||
try {
|
try {
|
||||||
if (response?.type === 'success') {
|
if (response?.type === "success") {
|
||||||
// If using Expo proxy, tokens can be present already.
|
// If using Expo proxy, tokens can be present already.
|
||||||
let idToken =
|
let idToken =
|
||||||
response?.authentication?.idToken || response?.params?.id_token;
|
response?.authentication?.idToken || response?.params?.id_token;
|
||||||
@@ -114,13 +114,13 @@ export default ({ navigation }) => {
|
|||||||
const clientId = request?.clientId;
|
const clientId = request?.clientId;
|
||||||
const discovery = {
|
const discovery = {
|
||||||
authorizationEndpoint:
|
authorizationEndpoint:
|
||||||
'https://accounts.google.com/o/oauth2/v2/auth',
|
"https://accounts.google.com/o/oauth2/v2/auth",
|
||||||
tokenEndpoint: 'https://oauth2.googleapis.com/token',
|
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
||||||
revocationEndpoint: 'https://oauth2.googleapis.com/revoke',
|
revocationEndpoint: "https://oauth2.googleapis.com/revoke",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Light debug: helps identify redirect/client mismatches in dev.
|
// Light debug: helps identify redirect/client mismatches in dev.
|
||||||
console.log('Google token exchange', {
|
console.log("Google token exchange", {
|
||||||
clientId,
|
clientId,
|
||||||
redirectUri: request?.redirectUri,
|
redirectUri: request?.redirectUri,
|
||||||
hasCodeVerifier: !!request?.codeVerifier,
|
hasCodeVerifier: !!request?.codeVerifier,
|
||||||
@@ -139,7 +139,7 @@ export default ({ navigation }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!idToken)
|
if (!idToken)
|
||||||
throw new Error('Jeton Google manquant (échange/retour)');
|
throw new Error("Jeton Google manquant (échange/retour)");
|
||||||
|
|
||||||
const credential =
|
const credential =
|
||||||
firebase.auth.GoogleAuthProvider.credential(idToken);
|
firebase.auth.GoogleAuthProvider.credential(idToken);
|
||||||
@@ -147,10 +147,10 @@ export default ({ navigation }) => {
|
|||||||
await afterLoginNavigate();
|
await afterLoginNavigate();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Google native sign-in error', e?.message);
|
console.log("Google native sign-in error", e?.message);
|
||||||
setTooltip({
|
setTooltip({
|
||||||
text: e?.message || 'Connexion Google impossible',
|
text: e?.message || "Connexion Google impossible",
|
||||||
type: 'error',
|
type: "error",
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -165,13 +165,14 @@ export default ({ navigation }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
backgroundImg={background.homeBG}
|
width={isWeb ? 600 : null}
|
||||||
|
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
|
||||||
headerType="NAVIGATION"
|
headerType="NAVIGATION"
|
||||||
title="Connexion"
|
title="Connexion"
|
||||||
hideBackButton
|
hideBackButton
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, paddingTop: 20 }}>
|
<View style={{ flex: 1, paddingTop: 20 }}>
|
||||||
<ItemContainer height={600} disableKeyboardHeight>
|
<ItemContainer height={600} width={800} disableKeyboardHeight>
|
||||||
<View style={{ gap: 30, paddingTop: 5, paddingHorizontal: 5 }}>
|
<View style={{ gap: 30, paddingTop: 5, paddingHorizontal: 5 }}>
|
||||||
<View style={{ gap: 2 }}>
|
<View style={{ gap: 2 }}>
|
||||||
<Text
|
<Text
|
||||||
@@ -181,7 +182,7 @@ export default ({ navigation }) => {
|
|||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Bonjour !
|
{"Bonjour !"}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -228,8 +229,8 @@ export default ({ navigation }) => {
|
|||||||
<GradientButton
|
<GradientButton
|
||||||
title="Se connecter"
|
title="Se connecter"
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
width: '80%',
|
width: "80%",
|
||||||
alignSelf: 'center',
|
alignSelf: "center",
|
||||||
}}
|
}}
|
||||||
onPress={onLogin}
|
onPress={onLogin}
|
||||||
disabled={loading || !email || !password}
|
disabled={loading || !email || !password}
|
||||||
@@ -252,7 +253,7 @@ export default ({ navigation }) => {
|
|||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -264,7 +265,7 @@ export default ({ navigation }) => {
|
|||||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Pas encore de compte?{' '}
|
Pas encore de compte?{" "}
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const Profile = () => {
|
|||||||
setFollowers(
|
setFollowers(
|
||||||
Array.isArray(currentUserData?.followedBy)
|
Array.isArray(currentUserData?.followedBy)
|
||||||
? currentUserData.followedBy.length
|
? currentUserData.followedBy.length
|
||||||
: 0
|
: 0,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -263,7 +263,12 @@ const Profile = () => {
|
|||||||
// }
|
// }
|
||||||
>
|
>
|
||||||
{isWeb && (
|
{isWeb && (
|
||||||
<Pressable onPress={() => SheetManager.show("ProfileSettings")}>
|
<Pressable
|
||||||
|
style={{
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
onPress={() => SheetManager.show("ProfileSettings")}
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
source={icons.more}
|
source={icons.more}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import BorderGradientButton from "../components/BorderGradientButton";
|
|||||||
import { navigate } from "../navigation/NavigationService";
|
import { navigate } from "../navigation/NavigationService";
|
||||||
import { Routes } from "../navigation";
|
import { Routes } from "../navigation";
|
||||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||||
|
import { isWeb } from "../hooks/useLayoutType";
|
||||||
|
|
||||||
const Register = () => {
|
const Register = () => {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
backgroundImg={background.homeBG}
|
width={isWeb ? 600 : null}
|
||||||
|
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
|
||||||
headerType="NAVIGATION"
|
headerType="NAVIGATION"
|
||||||
title="Inscription"
|
title="Inscription"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user