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 = {};