From 016f7c27ac61dcfd01fd4d67d72ef5e69cc4de4e Mon Sep 17 00:00:00 2001 From: Thomas Demirdjian Date: Mon, 17 Nov 2025 15:54:54 +0100 Subject: [PATCH] end of tickets --- functions/helpers/prompts.js | 30 +++- src/data/data.js | 3 + src/providers/StripeProvider.js | 140 +++++++++++++----- .../Library/components/ResearchHeader.js | 3 - .../Library/components/SearchResultsList.js | 16 +- src/screens/Studio/SongReady.js | 2 +- src/screens/Writing/CreatingLyrics.js | 16 +- src/screens/Writing/Lyrics.js | 20 ++- 8 files changed, 159 insertions(+), 71 deletions(-) diff --git a/functions/helpers/prompts.js b/functions/helpers/prompts.js index f4d518e..d4fd147 100644 --- a/functions/helpers/prompts.js +++ b/functions/helpers/prompts.js @@ -14,6 +14,13 @@ exports.generatePicturePrompt = (project = {}) => { .replace(/[<>]/g, "") .trim(); }; + const normalizeForMatching = (value = "") => { + if (typeof value !== "string") return ""; + return value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); + }; const titleForPrompt = sanitizeInline(title) || "Sans titre"; const artistName = @@ -63,11 +70,23 @@ exports.generatePicturePrompt = (project = {}) => { ? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)" : "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)"; - const coverStyle = String(coverStyleRaw || "").trim(); - const tagsLine = tags.join(" ; ") || "Non spécifié"; + const coverStyleInput = String(coverStyleRaw || "").trim(); + const coverStyle = sanitizeInline(coverStyleInput); + const normalizedStyle = normalizeForMatching(coverStyle); + let coverStyleGuidance = coverStyle; + if (normalizedStyle.includes("realist")) { + coverStyleGuidance = + "Style réaliste photographique hyper détaillé, textures fidèles, lumière naturelle, profondeur de champ crédible, aucun effet cartoon, fantastique ou peint"; + } + const hasTags = tags.length > 0; + const tagsLine = hasTags + ? `${tags.join( + " ; ", + )}. Utilise ces indications pour guider uniquement la palette, l'énergie et l'émotion, sans représenter littéralement les instruments, objets ou mots cités.` + : "Ambiance sonore non précisée : crée une atmosphère abstraite sans instrument ni objet musical apparent."; const lyricsLine = lyricsSample || "Pas d'extraits fournis"; - const styleLine = coverStyle - ? `${coverStyle}. ${paletteHint}` + const styleLine = coverStyleGuidance + ? `${coverStyleGuidance}. ${paletteHint}` : `${paletteHint}. Style libre, artistique et lumineux.`; const typographyLine = hasArtistName ? `Reproduis strictement le titre de la chanson ${titleForPrompt} sans modification, sans traduction et sans ajout ou suppression de caractères. Ajoute également le nom de l'artiste ${artistName} de manière artistique, parfaitement lisible et hiérarchisée, en harmonie avec le style abstrait et lumineux de la pochette.` @@ -83,7 +102,7 @@ ${artistLine} ${tagsLine} ${lyricsLine} - + Image carrée 1024x1024 pixels, résolution haute. La composition doit être dynamique et remplir toute la surface, sans laisser de bordures ni de zones vides. @@ -95,6 +114,7 @@ ${artistLine} ${tagsLine} Personnes, visages ou silhouettes reconnaissables. Fonds blancs ou bordures délimitant l'image. Texte illisible ou trop petit. + Représenter des instruments ou objets évoqués dans STYLE_MUSICAL : ces informations servent uniquement au contexte audio. `.trim(); diff --git a/src/data/data.js b/src/data/data.js index 073aa8e..4c64fb0 100644 --- a/src/data/data.js +++ b/src/data/data.js @@ -507,3 +507,6 @@ export const CHOOSE_DECOR = [ "Jardin", "Neige", ]; + +export const defaultAvatar = + "https://firebasestorage.googleapis.com/v0/b/musicland-d33f9.firebasestorage.app/o/default%2FdefaultAvatar.png?alt=media&token=99b6231a-e594-421e-b6cd-0e3aab148be0"; diff --git a/src/providers/StripeProvider.js b/src/providers/StripeProvider.js index 17e7a56..2f9274a 100644 --- a/src/providers/StripeProvider.js +++ b/src/providers/StripeProvider.js @@ -30,6 +30,49 @@ const stripePromise = loadStripe( isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE, ); +const isSafariBrowser = () => { + if ( + typeof navigator !== "object" || + typeof navigator.userAgent !== "string" + ) { + return false; + } + return ( + /safari/i.test(navigator.userAgent) && + !/(chrome|crios|android|fxios|edgios|opr|ucbrowser)/i.test( + navigator.userAgent, + ) + ); +}; + +const openSafariCheckoutWindow = () => { + if (!isWeb || typeof window === "undefined") { + return null; + } + if (!isSafariBrowser()) { + return null; + } + try { + const placeholder = window.open("", "_blank", "noopener,noreferrer"); + if (!placeholder) { + return null; + } + try { + placeholder.document.write( + "

Chargement du paiement Stripe...

", + ); + placeholder.document.title = "Stripe Checkout"; + } catch { + // Ignore failures when the window gets reused or is cross-domain. + } + placeholder.focus?.(); + return placeholder; + } catch (error) { + console.warn("[StripeProvider] Safari window pre-open failed", error); + return null; + } +}; + export const StripeContext = React.createContext(null); export const useStripe = () => { @@ -68,56 +111,70 @@ const StripeProvider = ({ children }) => { setClientSecret(null); }, []); - const redirectToCheckout = React.useCallback(async (checkoutUrl) => { - if (!checkoutUrl) { - throw new Error("Session Stripe introuvable."); - } + const redirectToCheckout = React.useCallback( + async (checkoutUrl, { safariWindow } = {}) => { + if (!checkoutUrl) { + throw new Error("Session Stripe introuvable."); + } - if (isWeb) { - if (typeof window !== "undefined") { - const openedTab = window.open( - checkoutUrl, - "_blank", - "noopener,noreferrer", - ); - // Fallback to same-tab navigation if the popup is blocked. - if (!openedTab) { - window.location.assign(checkoutUrl); + if (isWeb) { + if (safariWindow && !safariWindow.closed) { + try { + safariWindow.location.replace(checkoutUrl); + } catch (navigationError) { + safariWindow.location.href = checkoutUrl; + } + safariWindow.focus?.(); + return; } + + if (typeof window !== "undefined") { + const openedTab = window.open( + checkoutUrl, + "_blank", + "noopener,noreferrer", + ); + if (openedTab) { + return; + } + } + + await Linking.openURL(checkoutUrl); return; } - throw new Error("Navigation Stripe impossible dans cet environnement."); - } - const isMobileApp = - Platform.OS === "ios" || Platform.OS === "android"; + const isMobileApp = + Platform.OS === "ios" || Platform.OS === "android"; - if (isMobileApp) { - try { - await WebBrowser.openBrowserAsync(checkoutUrl, { - enableDefaultShareMenu: false, - dismissButtonStyle: "close", - presentationStyle: - WebBrowser?.WebBrowserPresentationStyle?.PAGE_SHEET, - }); - return; - } catch (webBrowserError) { - console.warn( - "[StripeProvider] WebBrowser checkout fallback", - webBrowserError, - ); + if (isMobileApp) { + try { + await WebBrowser.openBrowserAsync(checkoutUrl, { + enableDefaultShareMenu: false, + dismissButtonStyle: "close", + presentationStyle: + WebBrowser?.WebBrowserPresentationStyle?.PAGE_SHEET, + }); + return; + } catch (webBrowserError) { + console.warn( + "[StripeProvider] WebBrowser checkout fallback", + webBrowserError, + ); + } } - } - const canOpen = await Linking.canOpenURL(checkoutUrl); - if (!canOpen) { - throw new Error("Impossible d'ouvrir l'URL de paiement."); - } - await Linking.openURL(checkoutUrl); - }, []); + const canOpen = await Linking.canOpenURL(checkoutUrl); + if (!canOpen) { + throw new Error("Impossible d'ouvrir l'URL de paiement."); + } + await Linking.openURL(checkoutUrl); + }, + [], + ); const runCheckoutSession = React.useCallback( async ({ callableName, payload, logTag }) => { + const safariWindow = openSafariCheckoutWindow(); try { const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( callableName, @@ -127,8 +184,11 @@ const StripeProvider = ({ children }) => { if (!checkoutUrl) { throw new Error("Session Stripe introuvable."); } - await redirectToCheckout(checkoutUrl); + await redirectToCheckout(checkoutUrl, { safariWindow }); } catch (error) { + if (safariWindow && !safariWindow.closed) { + safariWindow.close(); + } console.error(`[StripeProvider] ${logTag}`, error); throw new Error( error?.message || diff --git a/src/screens/Library/components/ResearchHeader.js b/src/screens/Library/components/ResearchHeader.js index a8d2f59..2f2183a 100644 --- a/src/screens/Library/components/ResearchHeader.js +++ b/src/screens/Library/components/ResearchHeader.js @@ -49,9 +49,6 @@ const ResearchHeader = ({ {item} diff --git a/src/screens/Library/components/SearchResultsList.js b/src/screens/Library/components/SearchResultsList.js index c7b5c86..8d3a14a 100644 --- a/src/screens/Library/components/SearchResultsList.js +++ b/src/screens/Library/components/SearchResultsList.js @@ -10,6 +10,7 @@ import { FONT_FAMILY } from "../../../styles/Fonts"; import { getArtistDisplayName } from "../../../utils/artistName"; import CreateLyricsHeader from "../../Writing/components/CreateLyricsHeader"; import MusicCard from "./MusicCard"; +import { defaultAvatar } from "../../../data/data"; const EmptyText = ({ text }) => ( @@ -80,13 +81,13 @@ const SearchResultsList = ({ ...cover.options.flatMap((option) => [ option?.finalUrl, option?.generatedUrl, - ]) + ]), ); } return coverCandidates.find(isValid) || null; }, - [] + [], ); const handleMusicPress = (project) => { @@ -164,7 +165,7 @@ const SearchResultsList = ({ setShowMenu( (previous) => !previous || - positionTop?.top !== (menuPosition?.top ?? null) + positionTop?.top !== (menuPosition?.top ?? null), ); }} /> @@ -210,7 +211,7 @@ const SearchResultsList = ({ setShowMenu( (previous) => !previous || - positionTop?.top !== (menuPosition?.top ?? null) + positionTop?.top !== (menuPosition?.top ?? null), ); }} /> @@ -249,12 +250,7 @@ const SearchResultsList = ({ onPress={() => handleProfilePress(user.id)} > diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js index f314091..7437b39 100644 --- a/src/screens/Studio/SongReady.js +++ b/src/screens/Studio/SongReady.js @@ -197,7 +197,7 @@ const SongReady = () => { try { await pauseAllPlayers(); } catch {} - navigate(Routes.ComposeSong, { isRegeneration: true }); + navigate(Routes.Lyrics); }; const handleRegeneratePress = () => { diff --git a/src/screens/Writing/CreatingLyrics.js b/src/screens/Writing/CreatingLyrics.js index dd3789b..a63d767 100644 --- a/src/screens/Writing/CreatingLyrics.js +++ b/src/screens/Writing/CreatingLyrics.js @@ -73,7 +73,7 @@ const CreatingLyrics = ({ active, config, selections }) => { }); const sanitizedStructure = sanitizeStructureList( config?.structure, - CUSTOM_SANITIZE_OPTIONS + CUSTOM_SANITIZE_OPTIONS, ); const callable = firebase .functions() @@ -136,7 +136,7 @@ const CreatingLyrics = ({ active, config, selections }) => { const rawMessage = typeof e?.message === "string" ? e.message : ""; const cleanedMessage = rawMessage.replace( /^functions error: \w+-\w+:\s*/i, - "" + "", ); setError({ message: @@ -166,7 +166,7 @@ const CreatingLyrics = ({ active, config, selections }) => { console.log("💾 [CreatingLyrics] Sauvegarde automatique des paroles"); const sanitizedStructure = sanitizeStructureList( result?.structure || config?.structure, - CUSTOM_SANITIZE_OPTIONS + CUSTOM_SANITIZE_OPTIONS, ); const baseConfig = config && typeof config === "object" && !Array.isArray(config) @@ -192,12 +192,12 @@ const CreatingLyrics = ({ active, config, selections }) => { if ("customStructure" in persistedSelections) { persistedSelections.customStructure = sanitizeStructureList( persistedSelections.customStructure, - CUSTOM_SANITIZE_OPTIONS + CUSTOM_SANITIZE_OPTIONS, ); } if ("parsedStructure" in persistedSelections) { persistedSelections.parsedStructure = sanitizeStructureList( - persistedSelections.parsedStructure + persistedSelections.parsedStructure, ); } } @@ -250,7 +250,7 @@ const CreatingLyrics = ({ active, config, selections }) => { onPress: redirect, }, ], - { cancelable: false } + { cancelable: false }, ); if (Platform.OS === "web") { @@ -322,7 +322,7 @@ const CreatingLyrics = ({ active, config, selections }) => { setSaved(true); await setIsLoading(true); console.log( - "📄 [CreatingLyrics] Consultation manuelle du texte" + "📄 [CreatingLyrics] Consultation manuelle du texte", ); await updateProjectData({ title: result?.title || "", @@ -337,7 +337,7 @@ const CreatingLyrics = ({ active, config, selections }) => { } catch (e) { console.error( "⚠️ [CreatingLyrics] Erreur lors de l'ouverture manuelle", - e + e, ); } finally { await setIsLoading(false); diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js index ad792e7..6c2913b 100644 --- a/src/screens/Writing/Lyrics.js +++ b/src/screens/Writing/Lyrics.js @@ -37,6 +37,12 @@ const Lyrics = ({ navigation }) => { const [containerLayout, setContainerLayout] = useState(null); const { setIsLoading } = useMinuit(); const { selectedProjectId, selectedProject } = useUser(); + const hasExistingMusicDraft = useMemo(() => { + if (!Array.isArray(selectedProject?.musicUrls)) return false; + return selectedProject.musicUrls.some( + (url) => typeof url === "string" && url.trim() + ); + }, [selectedProject?.musicUrls]); const [isFocus, setIsFocus] = useState(null); const [itemsContainerLayout, setItemsContainerLayout] = useState([]); const [sensitiveContentModal, setSensitiveContentModal] = useState({ @@ -306,6 +312,14 @@ const Lyrics = ({ navigation }) => { } const beatmakerStage = getStageAction("beatmaker", projectForStage); await setIsLoading(false); + if (hasExistingMusicDraft) { + navigate(Routes.ComposeSong, { isRegeneration: true }); + return; + } + const handleNavigateToStudio = () => { + const targetRoute = beatmakerStage?.route || Routes.Compose; + navigate(targetRoute, beatmakerStage?.params); + }; alert( "Céline", "Bravo ! Vous venez de terminer de créer les paroles de votre musique ! Maintenant vous pouvez passer à la prochaine étape : le Studio pour donner vie à votre chanson !", @@ -323,10 +337,7 @@ const Lyrics = ({ navigation }) => { }, { text: "Continuer vers le Studio", - onPress: () => { - const targetRoute = beatmakerStage?.route || Routes.Compose; - navigate(targetRoute, beatmakerStage?.params); - }, + onPress: handleNavigateToStudio, }, ] ); @@ -350,6 +361,7 @@ const Lyrics = ({ navigation }) => { projectHasLyrics, projectLyrics, selectedProject, + hasExistingMusicDraft, ]); const typeOccurrences = {};