diff --git a/app.json b/app.json index 79b02b1..95309c1 100644 --- a/app.json +++ b/app.json @@ -16,7 +16,14 @@ "fallbackToCacheTimeout": 0 }, "packagerOpts": { - "sourceExts": ["js", "json", "ts", "tsx", "jsx", "vue"] + "sourceExts": [ + "js", + "json", + "ts", + "tsx", + "jsx", + "vue" + ] }, "splash": { "image": "./assets/splash.png", @@ -29,7 +36,9 @@ "supportsTablet": true, "requireFullScreen": true, "userInterfaceStyle": "dark", - "associatedDomains": ["applinks:minuit.starter"], + "associatedDomains": [ + "applinks:minuit.starter" + ], "bundleIdentifier": "com.minuit.starter", "infoPlist": { "UISupportedInterfaceOrientations": [ @@ -40,7 +49,10 @@ "UIInterfaceOrientationLandscapeLeft", "UIInterfaceOrientationLandscapeRight" ], - "LSApplicationQueriesSchemes": ["itms-apps", "minuit"] + "LSApplicationQueriesSchemes": [ + "itms-apps", + "minuit" + ] }, "config": { "usesNonExemptEncryption": false @@ -57,7 +69,9 @@ "backgroundColor": "#FFFFFF" }, "package": "com.minuit.starter", - "permissions": ["android.permission.RECORD_AUDIO"] + "permissions": [ + "android.permission.RECORD_AUDIO" + ] }, "web": { "bundler": "metro" @@ -105,7 +119,8 @@ "microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone", "recordAudioAndroid": true } - ] + ], + "expo-audio" ], "extra": { "eas": { diff --git a/functions/package-lock.json b/functions/package-lock.json index a6666f9..f95a0fd 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -10,7 +10,8 @@ "axios": "^1.6.0", "firebase-admin": "^12.1.0", "firebase-functions": "^5.0.0", - "genkit": "^1.16.0" + "genkit": "^1.16.0", + "zod": "3.23.8" }, "devDependencies": { "eslint": "^8.15.0", @@ -4749,9 +4750,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/functions/package.json b/functions/package.json index 72a5562..978a237 100644 --- a/functions/package.json +++ b/functions/package.json @@ -18,11 +18,15 @@ "axios": "^1.6.0", "firebase-admin": "^12.1.0", "firebase-functions": "^5.0.0", - "genkit": "^1.16.0" + "genkit": "^1.16.0", + "zod": "3.23.8" }, "devDependencies": { "eslint": "^8.15.0", "eslint-config-google": "^0.14.0" }, + "overrides": { + "zod": "3.23.8" + }, "private": true } diff --git a/functions/src/cover.js b/functions/src/cover.js new file mode 100644 index 0000000..e69de29 diff --git a/functions/src/music.js b/functions/src/music.js index 601dd16..723af71 100644 --- a/functions/src/music.js +++ b/functions/src/music.js @@ -329,174 +329,168 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => { * Cette fonction est appelée par l'API Suno lorsque la génération * de musique est terminée */ -exports.sunoCallback = onRequest( - { - methods: ["POST"], - }, - async (req, res) => { - try { - console.log("🎵 Received music generation callback body:", req.body); - if (req.method !== "POST") { - console.warn("⚠️ Méthode non autorisée:", req.method); - return res.status(405).json({ error: "Méthode non autorisée" }); - } +exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => { + try { + console.log("🎵 [SunoCallback] Reçu:", JSON.stringify(req.body)); - const { code, msg, data } = req.body || {}; - console.log("🎵 Callback details:", { code, msg, data }); - - // Extraire les données du callback - peut être dans data ou directement dans req.body - const callbackData = data || req.body || {}; - const { - id, - taskId, - status, - audio_url: audioUrl, - video_url: videoUrl, - image_url: imageUrl, - lyric, - title, - tags, - duration, - error_message: errorMessage, - } = callbackData; - - // Normaliser le statut global et le taskId - const overallStatus = status || code || callbackData?.status || null; - const overallTaskId = callbackData?.taskId || callbackData?.task_id || null; - - // Déterminer les éléments piste(s) à traiter - const items = Array.isArray(callbackData?.data) - ? callbackData.data - : Array.isArray(callbackData?.response?.data) - ? callbackData.response.data - : Array.isArray(callbackData?.sunoData) - ? callbackData.sunoData - : id || audioUrl || imageUrl || title || tags || duration - ? [callbackData] - : []; - - if (!items.length) { - // Aucun contenu piste à mettre à jour: acquitter le callback sans erreur - return res.status(200).json({ - success: true, - message: "Callback reçu (aucune piste à mettre à jour)", - taskId: overallTaskId, - status: overallStatus, - }); - } - - // Retrouver l'association taskId -> projectId via le champ sunoTaskId du projet - let mappedProjectId = null; - try { - if (overallTaskId) { - const projSnap = await db - .collection("projects") - .where("sunoTaskId", "==", overallTaskId) - .limit(1) - .get(); - if (!projSnap.empty) { - mappedProjectId = projSnap.docs[0].id; - } - } - } catch (e) { - // ignore mapping error, we'll proceed without projectId - } - - const updates = []; - - for (const item of items) { - const trackId = item.id || item.musicId || item.audioId || item.audio_id; - - if (!trackId) { - continue; - } - - // Chercher le document correspondant dans Firestore - const musicRef = db.collection("music").doc(trackId); - const musicDoc = await musicRef.get(); - - const updateData = { - status: item.status || overallStatus, - updatedAt: admin.firestore.FieldValue.serverTimestamp(), - }; - - const audioU = item.audio_url || item.audioUrl || item.streamAudioUrl; - const videoU = item.video_url || item.videoUrl; - const imageU = item.image_url || item.imageUrl; - const lyricText = item.lyric || item.prompt; - const titleText = item.title; - const tagsText = item.tags; - const durationVal = item.duration; - const errMsg = item.error_message || item.errorMessage; - - if (audioU) updateData.audioUrl = audioU; - if (videoU) updateData.videoUrl = videoU; - if (imageU) updateData.imageUrl = imageU; - if (lyricText) updateData.lyrics = lyricText; - if (titleText) updateData.title = titleText; - if (tagsText) updateData.style = tagsText; - if (durationVal) updateData.duration = durationVal; - if (errMsg) updateData.errorMessage = errMsg; - if (overallTaskId) updateData.taskId = overallTaskId; - if (mappedProjectId) updateData.projectId = mappedProjectId; - - if (!musicDoc.exists) { - updates.push( - musicRef - .set( - { - ...updateData, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }, - { merge: true }, - ) - .then(() => ({ id: trackId, ok: true })) - .catch(() => ({ id: trackId, ok: false })), - ); - } else { - updates.push( - musicRef - .update(updateData) - .then(() => ({ id: trackId, ok: true })) - .catch(() => ({ id: trackId, ok: false })), - ); - } - } - - const results = await Promise.all(updates); - const updated = results.filter((r) => r.ok).map((r) => r.id); - - // Mettre à jour le statut du projet si on connaît le projectId - if ( - mappedProjectId && - (overallStatus === 200 || - overallStatus === "SUCCESS" || - callbackData?.callbackType === "complete") - ) { - try { - await db.collection("projects").doc(mappedProjectId).set( - { - musicStatus: "GENERATED", - updatedAt: admin.firestore.FieldValue.serverTimestamp(), - }, - { merge: true }, - ); - } catch (_) {} - } - - return res.status(200).json({ - success: true, - message: "Callback traité avec succès", - updated, - taskId: overallTaskId, - status: overallStatus, - }); - } catch (error) { - logger.error("❌ Erreur lors du traitement du callback:", error); - res.status(500).json({ - error: "Erreur interne du serveur", - message: error.message, - }); + if (req.method !== "POST") { + console.warn("⚠️ [SunoCallback] Méthode non autorisée:", req.method); + return res.status(405).json({ error: "Méthode non autorisée" }); } - }, -); + + const body = req.body || {}; + const code = body.code ?? body.statusCode ?? null; + const callbackType = (body?.data?.callbackType || "").toString().toLowerCase(); + const status = (body.status || body.state || callbackType) + .toString() + .toLowerCase(); + const taskId = + body.taskId || + body.task_id || + body?.data?.taskId || + body?.data?.task_id || + null; + const tracks = Array.isArray(body?.data?.data) + ? body.data.data + : Array.isArray(body.data) + ? body.data + : []; + + console.log("🎯 [SunoCallback] Détails:", { + code, + status, + callbackType, + taskId, + count: tracks.length, + }); + + // On n'agit que si code === 200 et status === "complete" + if (code !== 200 || status !== "complete") { + console.log("ℹ️ [SunoCallback] Callback ignoré (code/status)", { + code, + status, + }); + return res.status(200).json({ success: true, ignored: true }); + } + + if (!taskId) { + console.warn("⚠️ [SunoCallback] taskId manquant dans le callback"); + return res.status(200).json({ success: true, ignored: true }); + } + + // 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); + } + + if (!projectId) { + console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId }); + return res.status(200).json({ success: true, ignored: true }); + } + + // 2) Extraire jusqu'à 2 URLs audio + const audioUrls = tracks + .map( + (t) => + t.audio_url || + t.audioUrl || + t.stream_audio_url || + t.streamAudioUrl, + ) + .filter(Boolean) + .slice(0, 2); + + if (audioUrls.length < 2) { + console.warn( + "⚠️ [SunoCallback] Moins de 2 pistes audio dans le callback", + { + found: audioUrls.length, + tracksSample: tracks.map((t) => ({ + id: t.id, + has_audio_url: !!t.audio_url, + has_stream_audio_url: !!t.stream_audio_url, + })), + }, + ); + } + + // 3) Télécharger et sauvegarder dans Cloud Storage + récupérer download URLs + const bucket = admin.storage().bucket(); + console.log("🪣 [SunoCallback] Bucket:", bucket.name); + const saveOne = async (url, index) => { + if (!url) return null; + try { + 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 token = require("crypto").randomUUID(); + const file = bucket.file(path); + await file.save(buffer, { + resumable: false, + metadata: { + contentType: "audio/mpeg", + cacheControl: "public, max-age=31536000", + metadata: { firebaseStorageDownloadTokens: token }, + }, + }); + const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent( + path, + )}?alt=media&token=${token}`; + console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl); + return { path, url: downloadUrl }; + } catch (e) { + console.error( + `❌ [SunoCallback] Échec save piste ${index + 1}:`, + e.message, + ); + return null; + } + }; + + const [p1, p2] = await Promise.all([ + saveOne(audioUrls[0], 0), + saveOne(audioUrls[1], 1), + ]); + + // 4) Mettre à jour le statut du projet + try { + const musicUrls = [p1?.url, p2?.url].filter(Boolean); + await db.collection("projects").doc(projectId).set( + { + musicStatus: "GENERATED", + musicUrls, + updatedAt: admin.firestore.FieldValue.serverTimestamp(), + }, + { merge: true }, + ); + console.log("🏷️ [SunoCallback] Projet marqué GENERATED", { + projectId, + musicUrlsCount: musicUrls.length, + }); + } catch (e) { + console.error("❌ [SunoCallback] Erreur maj projet:", e.message); + } + + return res.status(200).json({ + success: true, + projectId, + saved: [p1, p2].filter(Boolean), + }); + } catch (error) { + logger.error("❌ [SunoCallback] Erreur interne:", error); + res + .status(500) + .json({ error: "Erreur interne du serveur", message: error.message }); + } +}); diff --git a/ios/minuitstarter.xcodeproj/project.pbxproj b/ios/minuitstarter.xcodeproj/project.pbxproj index c271f88..9812393 100644 --- a/ios/minuitstarter.xcodeproj/project.pbxproj +++ b/ios/minuitstarter.xcodeproj/project.pbxproj @@ -10,32 +10,32 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 3CDCB2A6ACBF4A54A405140D /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2191D5FE133E45A2926B778E /* noop-file.swift */; }; 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; }; - 72F2555557D8F6EABE771399 /* Pods_minuitstarter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ACEAA07A4F71B659921B6E72 /* Pods_minuitstarter.framework */; }; + 61B5849DE3F147C89DEBD80E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 99B9EB1762DB4257B434DD94 /* GoogleService-Info.plist */; }; + 6DC0493EF7756C0D4156F20A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = FA3D2DD208AE96E5AB396716 /* PrivacyInfo.xcprivacy */; }; B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; }; BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; }; - C070A70B00BE496E8D630E02 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AD06C0EB7A242568E193EDE /* GoogleService-Info.plist */; }; - E84D7D494E7834360286DD64 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 892541484AE2CEDEC9F1A4B9 /* PrivacyInfo.xcprivacy */; }; - F06D769B37DF472DB8CE58F0 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4CDC778FD324A9581F2BF7C /* noop-file.swift */; }; + E3E12EBAF2A77E3F19EF428A /* Pods_minuitstarter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 932FDA70480C4888E77C4431 /* Pods_minuitstarter.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 10511ABF9CF3490F9E8ECABB /* minuitstarter-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "minuitstarter-Bridging-Header.h"; path = "minuitstarter/minuitstarter-Bridging-Header.h"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* minuitstarter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = minuitstarter.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = minuitstarter/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = minuitstarter/AppDelegate.mm; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = minuitstarter/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = minuitstarter/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = minuitstarter/main.m; sourceTree = ""; }; + 2191D5FE133E45A2926B778E /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "minuitstarter/noop-file.swift"; sourceTree = ""; }; 6C2E3173556A471DD304B334 /* Pods-minuitstarter.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minuitstarter.debug.xcconfig"; path = "Target Support Files/Pods-minuitstarter/Pods-minuitstarter.debug.xcconfig"; sourceTree = ""; }; 7A4D352CD337FB3A3BF06240 /* Pods-minuitstarter.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-minuitstarter.release.xcconfig"; path = "Target Support Files/Pods-minuitstarter/Pods-minuitstarter.release.xcconfig"; sourceTree = ""; }; - 892541484AE2CEDEC9F1A4B9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = minuitstarter/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 9AD06C0EB7A242568E193EDE /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "minuitstarter/GoogleService-Info.plist"; sourceTree = ""; }; + 932FDA70480C4888E77C4431 /* Pods_minuitstarter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_minuitstarter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 99B9EB1762DB4257B434DD94 /* GoogleService-Info.plist */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "minuitstarter/GoogleService-Info.plist"; sourceTree = ""; }; AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = minuitstarter/SplashScreen.storyboard; sourceTree = ""; }; - ACEAA07A4F71B659921B6E72 /* Pods_minuitstarter.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_minuitstarter.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = ""; }; - BBBE1881BB4E4B18BF26323C /* minuitstarter-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "minuitstarter-Bridging-Header.h"; path = "minuitstarter/minuitstarter-Bridging-Header.h"; sourceTree = ""; }; - E4CDC778FD324A9581F2BF7C /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "minuitstarter/noop-file.swift"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + FA3D2DD208AE96E5AB396716 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = minuitstarter/PrivacyInfo.xcprivacy; sourceTree = ""; }; FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-minuitstarter/ExpoModulesProvider.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -44,7 +44,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 72F2555557D8F6EABE771399 /* Pods_minuitstarter.framework in Frameworks */, + E3E12EBAF2A77E3F19EF428A /* Pods_minuitstarter.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -61,10 +61,10 @@ 13B07FB61A68108700A75B9A /* Info.plist */, 13B07FB71A68108700A75B9A /* main.m */, AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */, - 9AD06C0EB7A242568E193EDE /* GoogleService-Info.plist */, - E4CDC778FD324A9581F2BF7C /* noop-file.swift */, - BBBE1881BB4E4B18BF26323C /* minuitstarter-Bridging-Header.h */, - 892541484AE2CEDEC9F1A4B9 /* PrivacyInfo.xcprivacy */, + 99B9EB1762DB4257B434DD94 /* GoogleService-Info.plist */, + 2191D5FE133E45A2926B778E /* noop-file.swift */, + 10511ABF9CF3490F9E8ECABB /* minuitstarter-Bridging-Header.h */, + FA3D2DD208AE96E5AB396716 /* PrivacyInfo.xcprivacy */, ); name = minuitstarter; sourceTree = ""; @@ -73,7 +73,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - ACEAA07A4F71B659921B6E72 /* Pods_minuitstarter.framework */, + 932FDA70480C4888E77C4431 /* Pods_minuitstarter.framework */, ); name = Frameworks; sourceTree = ""; @@ -150,15 +150,15 @@ buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "minuitstarter" */; buildPhases = ( 08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */, - 338F946E107E3BBCB6461F5F /* [Expo] Configure project */, + 77DAFE3801F000CF4606DB8A /* [Expo] Configure project */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */, - 5045AF86FCE3306BA9F3EB85 /* [CP] Embed Pods Frameworks */, - A22C818FC9748665685082DE /* [CP-User] [RNFB] Core Configuration */, - CE226A716498761932DD6269 /* [CP-User] [RNFB] Crashlytics Configuration */, + 0D866A6FE1A1C17FF0541823 /* [CP] Embed Pods Frameworks */, + FD056DD880B96E4C7427F5EF /* [CP-User] [RNFB] Core Configuration */, + 09382956D95F1957020F2E13 /* [CP-User] [RNFB] Crashlytics Configuration */, ); buildRules = ( ); @@ -208,8 +208,8 @@ BB2F792D24A3F905000567C9 /* Expo.plist in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */, - C070A70B00BE496E8D630E02 /* GoogleService-Info.plist in Resources */, - E84D7D494E7834360286DD64 /* PrivacyInfo.xcprivacy in Resources */, + 61B5849DE3F147C89DEBD80E /* GoogleService-Info.plist in Resources */, + 6DC0493EF7756C0D4156F20A /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -253,7 +253,39 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 338F946E107E3BBCB6461F5F /* [Expo] Configure project */ = { + 09382956D95F1957020F2E13 /* [CP-User] [RNFB] Crashlytics Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Crashlytics Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; + }; + 0D866A6FE1A1C17FF0541823 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-minuitstarter/Pods-minuitstarter-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minuitstarter/Pods-minuitstarter-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 77DAFE3801F000CF4606DB8A /* [Expo] Configure project */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -272,24 +304,6 @@ shellPath = /bin/sh; shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-minuitstarter/expo-configure-project.sh\"\n"; }; - 5045AF86FCE3306BA9F3EB85 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-minuitstarter/Pods-minuitstarter-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minuitstarter/Pods-minuitstarter-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -372,7 +386,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-minuitstarter/Pods-minuitstarter-resources.sh\"\n"; showEnvVarsInLog = 0; }; - A22C818FC9748665685082DE /* [CP-User] [RNFB] Core Configuration */ = { + FD056DD880B96E4C7427F5EF /* [CP-User] [RNFB] Core Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -385,20 +399,6 @@ shellPath = /bin/sh; shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; }; - CE226A716498761932DD6269 /* [CP-User] [RNFB] Crashlytics Configuration */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}", - "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", - ); - name = "[CP-User] [RNFB] Crashlytics Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -409,7 +409,7 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */, - F06D769B37DF472DB8CE58F0 /* noop-file.swift in Sources */, + 3CDCB2A6ACBF4A54A405140D /* noop-file.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/package.json b/package.json index 6c40c22..bf3baba 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "add": "^2.0.6", "deprecated-react-native-prop-types": "^3.0.1", "expo": "~52.0.47", + "expo-audio": "~0.3.5", "expo-av": "~15.0.2", "expo-blur": "~14.0.3", "expo-build-properties": "~0.13.3", diff --git a/src/components/Slider.js b/src/components/Slider.js index 69a7abb..e799895 100644 --- a/src/components/Slider.js +++ b/src/components/Slider.js @@ -1,9 +1,10 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { useAnimatedStyle, useSharedValue, + runOnJS, } from "react-native-reanimated"; import { LinearGradient } from "./LinearGradient/LinearGradient"; import { Palette, Style } from "../styles"; @@ -11,7 +12,7 @@ import { FONT_FAMILY } from "../styles/Fonts"; const INITIAL_BOX_SIZE = 6; -export default ({ value, maxValue }) => { +export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => { const offset = useSharedValue(0); const boxWidth = useSharedValue(INITIAL_BOX_SIZE); const [layout, setLayout] = useState(null); @@ -19,19 +20,39 @@ export default ({ value, maxValue }) => { const SLIDER_WIDTH = layout?.width; const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE; - const pan = Gesture.Pan().onChange((event) => { - offset.value = - Math.abs(offset.value) <= MAX_VALUE - ? offset.value + event.changeX <= 0 - ? 0 - : offset.value + event.changeX >= MAX_VALUE - ? MAX_VALUE - : offset.value + event.changeX - : offset.value; + const pan = Gesture.Pan() + .enabled(seekEnabled) + .onChange((event) => { + offset.value = + Math.abs(offset.value) <= MAX_VALUE + ? offset.value + event.changeX <= 0 + ? 0 + : offset.value + event.changeX >= MAX_VALUE + ? MAX_VALUE + : offset.value + event.changeX + : offset.value; - const newWidth = INITIAL_BOX_SIZE + offset.value; - boxWidth.value = newWidth; - }); + const newWidth = INITIAL_BOX_SIZE + offset.value; + boxWidth.value = newWidth; + }) + .onEnd(() => { + if (!seekEnabled || !onSeek || !MAX_VALUE) return; + const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0; + // Reanimated -> JS thread bridge + runOnJS(onSeek)(ratio); + }); + + // Reflect external progress into the slider UI + useEffect(() => { + if (typeof progress === "number" && layout?.width) { + const max = layout.width - INITIAL_BOX_SIZE; + const clamped = Math.max(0, Math.min(1, progress)); + const newOffset = clamped * max; + offset.value = newOffset; + boxWidth.value = INITIAL_BOX_SIZE + newOffset; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [progress, layout?.width]); const boxStyle = useAnimatedStyle(() => { return { diff --git a/src/screens/Studio/ComposeSong.js b/src/screens/Studio/ComposeSong.js index 19cdb1e..8a52c20 100644 --- a/src/screens/Studio/ComposeSong.js +++ b/src/screens/Studio/ComposeSong.js @@ -62,11 +62,18 @@ const ComposeSong = () => { }, [selectedIndex, genres, voice, instruments, rhythm]); const musicConfig = useMemo(() => { - const lyricsArr = []; - const c = project?.lyrics?.couplet; - const r = project?.lyrics?.refrain; - if (c) lyricsArr.push({ type: "couplet", lyrics: c }); - if (r) lyricsArr.push({ type: "refrain", lyrics: r }); + let lyricsArr = []; + if (Array.isArray(project?.lyrics)) { + lyricsArr = project.lyrics.map((s) => ({ + type: (s?.type || "").toLowerCase(), + lyrics: s?.lyrics || "", + })); + } else { + const c = project?.lyrics?.couplet; + const r = project?.lyrics?.refrain; + if (c) lyricsArr.push({ type: "couplet", lyrics: c }); + if (r) lyricsArr.push({ type: "refrain", lyrics: r }); + } return { title: project?.title || "", lyrics: lyricsArr, diff --git a/src/screens/Studio/CreatingSong.js b/src/screens/Studio/CreatingSong.js index cf149a0..59149ad 100644 --- a/src/screens/Studio/CreatingSong.js +++ b/src/screens/Studio/CreatingSong.js @@ -119,8 +119,15 @@ const CreatingSong = ({ active, config }) => { { sunoTaskId: taskId, musicStatus: "GENERATING", - generationStartAt: - firebase.firestore.FieldValue.serverTimestamp(), + musicConfig: { + title: config?.title || "", + lyrics: config?.lyrics || [], + genres: config?.genres || [], + voice: config?.voice || "", + instruments: config?.instruments || [], + tempo: config?.tempo || "", + }, + generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }, { merge: true } @@ -221,12 +228,19 @@ const CreatingSong = ({ active, config }) => { navigate(Routes.SongReady, { result })} + onPress={() => + navigate(Routes.SongReady, { projectId: config?.projectId }) + } /> diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js index 2943ba7..a2a5ffd 100644 --- a/src/screens/Studio/SongReady.js +++ b/src/screens/Studio/SongReady.js @@ -1,22 +1,178 @@ -import React, { useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Page from "../../layouts/Page"; import { background, icons } from "../../assets"; import MusicLandHeader from "../../components/MusicLandHeader"; import { goBack, navigate } from "../../navigation/NavigationService"; import Slider from "../../components/Slider"; -import { Image, Platform, Pressable, View } from "react-native"; +import { Image, Platform, Pressable, View, Text } from "react-native"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import { BlurView } from "expo-blur"; import { Style } from "../../styles"; -import { responsiveWidth } from "react-native-responsive-dimensions"; import { gutters, size } from "../../styles/Style"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import { Routes } from "../../navigation"; import ValidateModal from "../../components/modal/ValidateModal"; +import { useRoute } from "@react-navigation/core"; +import firebase from "../../config/firebase"; +import { useAudioPlayer } from "expo-audio"; +import { responsiveHeight } from "react-native-responsive-dimensions"; const SongReady = () => { + const params = useRoute().params || {}; + const projectId = params?.projectId; const [showValidateModal, setShowValidateModal] = useState(false); + const [musicUrls, setMusicUrls] = useState([]); + const [selectedIndex, setSelectedIndex] = useState(0); + const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false }); + const [progressInfo, setProgressInfo] = useState({ + 0: { pos: 0, dur: 0 }, + 1: { pos: 0, dur: 0 }, + }); + const player0 = useAudioPlayer( + musicUrls[0] ? { uri: musicUrls[0] } : undefined, + ); + const player1 = useAudioPlayer( + musicUrls[1] ? { uri: musicUrls[1] } : undefined, + ); + + // Charger les URLs depuis le document projet + useEffect(() => { + if (!projectId) return; + const unsub = firebase + .firestore() + .collection("projects") + .doc(projectId) + .onSnapshot((doc) => { + const data = doc.data() || {}; + const urls = Array.isArray(data?.musicUrls) + ? data.musicUrls.slice(0, 2) + : []; + setMusicUrls(urls); + }); + return () => unsub?.(); + }, [projectId]); + + // Sync progression depuis les players + useEffect(() => { + const id = setInterval(() => { + const d0 = (player0?.duration || 0) * 1000; + const p0 = (player0?.currentTime || 0) * 1000; + const d1 = (player1?.duration || 0) * 1000; + const p1 = (player1?.currentTime || 0) * 1000; + setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } }); + setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing }); + }, 300); + return () => clearInterval(id); + }, [player0, player1]); + + const togglePlay = async (idx) => { + const url = musicUrls[idx]; + if (!url) return; + try { + // Pause l'autre piste si elle joue + const other = idx === 0 ? 1 : 0; + if (isPlaying[other]) { + if (other === 0) await player0?.pause?.(); + else await player1?.pause?.(); + setIsPlaying((p) => ({ ...p, [other]: false })); + } + const player = idx === 0 ? player0 : player1; + if (!player) return; + if (player.playing) { + await player.pause?.(); + setIsPlaying((p) => ({ ...p, [idx]: false })); + } else { + await player.play?.(); + setIsPlaying((p) => ({ ...p, [idx]: true })); + } + } catch (e) { + console.log("Audio error", e?.message); + } + }; + + const fmt = (ms) => { + const total = Math.max(0, Math.floor((ms || 0) / 1000)); + const m = Math.floor(total / 60) + .toString() + .padStart(1, "0"); + const s = (total % 60).toString().padStart(2, "0"); + return `${m}:${s}`; + }; + + const onSeek = async (idx, ratio) => { + try { + const info = progressInfo[idx] || {}; + const dur = info.dur || 0; + const pos = Math.floor(dur * ratio); + const player = idx === 0 ? player0 : player1; + if (player && dur > 0) { + await player.seekTo?.(Math.floor((pos || 0) / 1000)); + } + } catch (e) { + console.log("Seek error", e?.message); + } + }; + + const validateSelection = async () => { + try { + const url = musicUrls[selectedIndex]; + if (!projectId || !url) return; + await firebase + .firestore() + .collection("projects") + .doc(projectId) + .set( + { + song: { index: selectedIndex, url }, + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }, + { merge: true }, + ); + navigate(Routes.PouchReady); + } catch (e) { + console.log("Validate error", e?.message); + } + }; + + const onPressRegenerate = async () => { + try { + if (!projectId) return goBack(); + const doc = await firebase + .firestore() + .collection("projects") + .doc(projectId) + .get(); + const project = doc.data() || {}; + const musicConfig = project?.musicConfig || {}; + + const callable = firebase + .functions() + .httpsCallable("music-generateMusic"); + const { data } = await callable({ + ...musicConfig, + projectId, + }); + + const taskId = + data?.response?.data?.taskId || data?.response?.data?.task_id; + if (taskId) { + await firebase.firestore().collection("projects").doc(projectId).set( + { + sunoTaskId: taskId, + musicStatus: "GENERATING", + generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }, + { merge: true }, + ); + } + } catch (e) { + console.log("Regenerate error", e?.message); + } finally { + goBack(); + } + }; return ( @@ -25,38 +181,84 @@ const SongReady = () => { - - - - - - - - - - + /> + + {[0, 1].map((idx) => ( + + + togglePlay(idx)} + style={{ ...Style.containerCenter, ...size({ size: 48 }) }} + > + + + + {`Morceau ${idx + 1}`} + onSeek(idx, ratio)} + /> + + setSelectedIndex(idx)} + style={{ ...Style.containerCenter, ...size({ size: 24 }) }} + > + + {selectedIndex === idx && ( + + )} + + + + + ))} { - navigate(Routes.Regenerate, { - progress: 63, - }) - } + onPress={onPressRegenerate} /> setShowValidateModal(true)} + disabled={!musicUrls?.length} /> setShowValidateModal(false)} - onPressValidate={() => navigate(Routes.PouchReady)} + onPressValidate={validateSelection} /> ); diff --git a/src/screens/Studio/Studio.js b/src/screens/Studio/Studio.js index 016869c..211f1e6 100644 --- a/src/screens/Studio/Studio.js +++ b/src/screens/Studio/Studio.js @@ -1,5 +1,5 @@ -import { View, Text, ScrollView, Pressable } from "react-native"; -import React, { useEffect, useState, useMemo } from "react"; +import { View, Text, ScrollView, Pressable, Alert } from "react-native"; +import React, { useState } from "react"; import Page from "../../layouts/Page"; import { background } from "../../assets"; import GradientButton from "../../components/GradientButton"; @@ -9,11 +9,11 @@ import { gutters } from "../../styles"; import firebase from "../../config/firebase"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useDataFromRef from "../../hooks/useDataFromRef"; +import { responsiveHeight } from "react-native-responsive-dimensions"; const Studio = () => { const { setIsLoading } = useMinuit(); - const [selectedId, setSelectedId] = useState(null); - const isDisabled = useMemo(() => !selectedId, [selectedId]); + const [selected, setSelected] = useState(null); const user = firebase.auth().currentUser; const { data: projects } = useDataFromRef({ @@ -131,14 +131,11 @@ const Studio = () => { > Derniers projets générés - + {projects.map((p) => { const couplet = p?.lyrics?.couplet || ""; const preview = couplet.split("\n").slice(0, 2).join(" "); - const selected = selectedId === p.id; + const isSelected = selected === p; const createdAt = p?.createdAt?.toDate ? p.createdAt.toDate() : p?.createdAt @@ -151,13 +148,13 @@ const Studio = () => { return ( setSelectedId(p.id)} + onPress={() => setSelected(p)} style={{ backgroundColor: "#0F0C1933", borderRadius: 12, padding: 12, - borderWidth: selected ? 2 : 0, - borderColor: selected ? "#F94697" : "transparent", + borderWidth: isSelected ? 2 : 0, + borderColor: isSelected ? "#F94697" : "transparent", }} > { navigate(Routes.Compose, { projectId: selectedId })} + disabled={!selected} + onPress={() => { + if (selected.musicStatus === "GENERATING") { + Alert.alert( + "Attention", + "La chanson est en cours de génération. Veuillez patienter.", + ); + } else { + navigate(Routes.Compose, { projectId: selected.id }); + } + }} /> + {selected?.musicUrls?.length > 0 && ( + + navigate(Routes.SongReady, { projectId: selected.id }) + } + /> + )} ); diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js index 8e2947e..82e59f6 100644 --- a/src/screens/Writing/Lyrics.js +++ b/src/screens/Writing/Lyrics.js @@ -23,25 +23,35 @@ const Lyrics = ({ navigation }) => { const { setIsLoading } = useMinuit(); const initial = useMemo(() => { - if (!lyricsData || !lyricsData?.success) return {}; const title = lyricsData?.title || ""; - const sections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : []; - const couplets = sections - .filter((s) => (s?.type || "").toLowerCase().includes("couplet")) - .map((s) => s?.lyrics || ""); - const refrains = sections - .filter((s) => (s?.type || "").toLowerCase().includes("refrain")) - .map((s) => s?.lyrics || ""); - return { - title, - couplet: couplets.join("\n\n"), - refrain: refrains.join("\n\n"), - }; - }, [lyricsData]); + const aiSections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : []; + // Respecter l'ordre de la structure choisie si disponible + const targetStructure = Array.isArray(config?.structure) + ? config.structure.map((t) => (t || "").toLowerCase()) + : null; + if (aiSections.length && targetStructure && aiSections.length === targetStructure.length) { + return { title, sections: aiSections.map((s) => ({ type: (s?.type || "").toLowerCase(), lyrics: s?.lyrics || "" })) }; + } + // Sinon, créer à partir de la structure + if (targetStructure && targetStructure.length) { + return { + title, + sections: targetStructure.map((t) => ({ type: t, lyrics: "" })), + }; + } + // Fallback vide + return { title, sections: [] }; + }, [lyricsData, config]); const [titleValue, setTitleValue] = useState(initial.title || ""); - const [coupletValue, setCoupletValue] = useState(initial.couplet || ""); - const [refrainValue, setRefrainValue] = useState(initial.refrain || ""); + const [sections, setSections] = useState(initial.sections || []); + const setSectionAt = (index, value) => { + setSections((prev) => { + const next = [...prev]; + if (next[index]) next[index] = { ...next[index], lyrics: value }; + return next; + }); + }; // Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente const regenerate = useCallback(() => { @@ -70,10 +80,10 @@ const Lyrics = ({ navigation }) => { const user = firebase.auth().currentUser; const payload = { title: titleValue?.trim() || "", - lyrics: { - couplet: coupletValue || "", - refrain: refrainValue || "", - }, + lyrics: (sections || []).map((s) => ({ + type: (s?.type || "").toLowerCase(), + lyrics: s?.lyrics || "", + })), config: sanitize(config), selections: sanitize(selections), userId: user ? user.uid : null, @@ -88,14 +98,7 @@ const Lyrics = ({ navigation }) => { } finally { await setIsLoading(false); } - }, [ - titleValue, - coupletValue, - refrainValue, - config, - selections, - setIsLoading, - ]); + }, [titleValue, sections, config, selections, setIsLoading]); return ( @@ -124,40 +127,25 @@ const Lyrics = ({ navigation }) => { value={titleValue} setValue={setTitleValue} /> - - - Introduction instrumentale longue - - - - + {sections.map((s, idx) => { + // Calculer l'index humain par type + const type = (s?.type || "").toLowerCase(); + const countBefore = sections + .slice(0, idx) + .filter((x) => (x?.type || "").toLowerCase() === type).length; + const labelBase = type === "refrain" ? "Refrain" : "Couplet"; + const label = `${labelBase} ${countBefore + 1}`; + return ( + setSectionAt(idx, val)} + /> + ); + })} diff --git a/yarn.lock b/yarn.lock index dfd4c98..757edd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5031,6 +5031,11 @@ expo-asset@~11.0.5: invariant "^2.2.4" md5-file "^3.2.3" +expo-audio@~0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/expo-audio/-/expo-audio-0.3.5.tgz#1f151ec9919e163019f1aa76a117657e0ea6b613" + integrity sha512-gzpDH3vZI1FDL1Q8pXryACtNIW+idZ/zIZ8WqdTRzJuzxucazrG2gLXUS2ngcXQBn09Jyz4RUnU10Tu2N7/Hgg== + expo-av@~15.0.2: version "15.0.2" resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-15.0.2.tgz#65bb08658a7fe3a67aa47da614abbfae9adb684e"