last tickets

This commit is contained in:
Thomas Demirdjian
2025-11-24 15:35:14 +01:00
parent 471a3177e8
commit 72852ad92b
40 changed files with 1749 additions and 667 deletions
@@ -1,17 +1,18 @@
import { BlurView } from "expo-blur";
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Image,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background } from "../../assets";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import BorderGradientButton from "../../components/BorderGradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import firebase, {
projectsRef,
@@ -23,12 +24,16 @@ import useDataFromRef from "../../hooks/useDataFromRef";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { useUserData, useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { getBlobForUrl, releaseBlobUrl } from "../../utils/blobUrlCache";
import ClubAdvantagesCard from "../Profile/components/ClubAdvantagesCard";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { Image as ExpoImage } from "expo-image";
import SubscriptionConfirmModal from "../../components/SubscriptionConfirmModal";
import { isWeb } from "../../hooks/useLayoutType";
const triggerWebDownload = (url, title) => {
if (Platform.OS !== "web") return;
@@ -46,7 +51,7 @@ const triggerWebDownload = (url, title) => {
anchor.click();
document.body.removeChild(anchor);
} catch (error) {
console.log("[DownloadSongs] web download fallback", {
console.log("[PlaybackDownload] web download fallback", {
message: error?.message,
});
try {
@@ -64,7 +69,7 @@ const guessExtension = (inputUri = "") => {
};
const uploadSourceRecording = async ({ uri, uid, projectId }) => {
console.log("[DownloadSongs] uploadSourceRecording params", {
console.log("[PlaybackDownload] uploadSourceRecording params", {
hasUri: Boolean(uri),
uid,
projectId,
@@ -76,13 +81,13 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
const extension = guessExtension(uri);
const sourcePath = `users/${uid}/projects/${projectId}/recordings/source-${Date.now()}.${extension}`;
console.log("source path : ", sourcePath);
console.log("[DownloadSongs] uploadSourceRecording source path", {
console.log("[PlaybackDownload] uploadSourceRecording source path", {
extension,
sourcePath,
});
const cachedBlob = getBlobForUrl(uri);
console.log("[DownloadSongs] uploadSourceRecording blob cache", {
console.log("[PlaybackDownload] uploadSourceRecording blob cache", {
hasBlob: Boolean(cachedBlob),
});
@@ -97,10 +102,11 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
return { sourcePath, videoUrl };
};
const DownloadSongs = ({ route }) => {
const PlaybackDownload = ({ route }) => {
const { currentUID } = useUserData();
const { hasActiveSubscription } = useUser() || {};
const { action, uri, project } = route.params || {};
console.log("[DownloadSongs] route params", {
console.log("[PlaybackDownload] route params", {
action,
projectId: project?.id,
hasUri: Boolean(uri),
@@ -109,6 +115,7 @@ const DownloadSongs = ({ route }) => {
const { setIsLoading, setTooltip } = useMinuit();
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] =
useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const hasShownAfterPlaybackRef = useRef(false);
const { data: video } = useDataFromRef({
@@ -138,7 +145,7 @@ const DownloadSongs = ({ route }) => {
const handleDownloadUri = async () => {
if (action === "playback" && project?.id) {
// Publication du playback
console.log("[DownloadSongs] handleDownloadUri playback", {
console.log("[PlaybackDownload] handleDownloadUri playback", {
projectId: project.id,
uri,
currentUID,
@@ -173,13 +180,16 @@ const DownloadSongs = ({ route }) => {
};
console.log(
"[DownloadSongs] calling upload-mergeVideoAndAudio",
payload
"[PlaybackDownload] calling upload-mergeVideoAndAudio",
payload,
);
const { data: result } = await callable(payload);
console.log("[DownloadSongs] upload-mergeVideoAndAudio result", result);
console.log(
"[PlaybackDownload] upload-mergeVideoAndAudio result",
result,
);
const resultURI = result?.url || null;
@@ -189,7 +199,7 @@ const DownloadSongs = ({ route }) => {
playbackUrl: resultURI,
updatedAt: serverTimestamp(),
},
{ merge: true }
{ merge: true },
);
setTooltip({
type: "success",
@@ -200,7 +210,7 @@ const DownloadSongs = ({ route }) => {
try {
await firebase.storage().ref(tempSourcePath).delete();
} catch (cleanupError) {
console.log("[DownloadSongs] unable to delete temp source", {
console.log("[PlaybackDownload] unable to delete temp source", {
message: cleanupError?.message,
code: cleanupError?.code,
});
@@ -213,7 +223,7 @@ const DownloadSongs = ({ route }) => {
});
}
} catch (error) {
console.log("[DownloadSongs] error upload playback", {
console.log("[PlaybackDownload] error upload playback", {
message: error?.message,
code: error?.code,
name: error?.name,
@@ -227,7 +237,7 @@ const DownloadSongs = ({ route }) => {
setTooltip({
type: "error",
text: String(
error?.message || "Erreur lors de la publication du playback"
error?.message || "Erreur lors de la publication du playback",
),
});
} finally {
@@ -239,6 +249,9 @@ const DownloadSongs = ({ route }) => {
// Handle song download
}
};
const continueLabel = hasActiveSubscription
? "Publier"
: "Publier sans générer de revenus";
return (
<>
<Page
@@ -250,130 +263,100 @@ const DownloadSongs = ({ route }) => {
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={50} />
<View style={{ flex: 1, paddingTop: responsiveHeight(15) }}>
<CreateLyricsHeader
gradientProps={{
start: { x: 0, y: 0 },
end: { x: 0, y: 1 },
}}
>
<Image
source={{ uri: project?.coverUrl }}
style={{
...size({ size: 158 }),
borderRadius: 13,
alignSelf: "center",
}}
/>
<View style={{ gap: 12, paddingVertical: 12 }}>
<Text style={styles.title}>
Prêt à télécharger
{action === "playback" ? " ton Playback" : " ta chanson"} ?
</Text>
<View style={{ gap: 10 }}>
<Pressable
style={styles.itemContainer}
onPress={handleDownloadUri}
// navigate(Routes.DownloadPrices, {
// action,
// uri,
// })
>
<BlurView
style={styles.blurView}
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Text style={styles.label}>
Télécharger{" "}
{action === "playback" ? "mon Playback" : "ma chanson"}
</Text>
<Text style={styles.description}>Pour moi uniquement</Text>
</BlurView>
</Pressable>
<Pressable
style={styles.itemContainer}
onPress={() =>
navigate(Routes.StreamSong, {
action,
})
}
>
<BlurView
style={styles.blurView}
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<Text style={styles.label}>
Diffuser{" "}
{action === "playback" ? "mon Playback" : "ma chanson"}{" "}
sur la plateforme de MusicLand (+ réseaux sociaux) et
participer au concours
</Text>
<Text style={styles.description}>
Top 3: 1er 15% du CA ML - 2ème 10% du CA ML - 3ème 5% du
CA ML (catégorie{" "}
{action === "playback" ? "Playback" : "chanson"})
</Text>
</BlurView>
</Pressable>
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
<CreateLyricsHeader title={"Publier ton playback"} />
<View style={styles.coverRow}>
{project?.coverUrl ? (
<ExpoImage
source={{ uri: project?.coverUrl }}
style={styles.coverImage}
contentFit="cover"
/>
) : (
<View style={[styles.coverImage, styles.coverPlaceholder]}>
<Text style={styles.placeholderText}>Aucune pochette</Text>
</View>
</View>
</CreateLyricsHeader>
</View>
)}
<Pressable style={styles.downloadTile} onPress={handleDownloadUri}>
<MaterialCommunityIcons
name="download"
size={22}
color={Palette.white}
/>
<Text style={styles.downloadText}>Télécharger le playback</Text>
</Pressable>
</View>
<ClubAdvantagesCard style={styles.clubCardSpacing} />
<BorderGradientButton
title={continueLabel}
onPress={() => {
if (hasActiveSubscription) {
navigate(Routes.SongRelease, { action });
} else {
setShowConfirmModal(true);
}
}}
containerStyle={styles.continueButton}
/>
</ScrollView>
</Page>
{action === "playback" && (
<FullscreenIntroVideo
url={afterPlaybackUrl || null}
visible={Boolean(afterPlaybackUrl) && isAfterPlaybackVideoVisible}
onClose={() => setIsAfterPlaybackVideoVisible(false)}
/>
)}
<SubscriptionConfirmModal
continueLabel={"Continuer vers la publication de playback"}
isVisible={showConfirmModal}
setIsVisible={setShowConfirmModal}
onJoinClub={() => navigate(Routes.Payments)}
onContinue={() => navigate(Routes.SongRelease, { action })}
/>
</>
);
};
export default DownloadSongs;
export default PlaybackDownload;
const styles = StyleSheet.create({
title: {
fontSize: 22,
color: Palette.white,
container: {
flexGrow: 1,
paddingHorizontal: 16,
paddingVertical: 12,
gap: 14,
},
coverRow: {
flexDirection: isWeb ? "row" : "column",
alignItems: "center",
justifyContent: isWeb ? "center" : "center",
gap: 12,
},
coverImage: {
...size({ size: 140 }),
borderRadius: 14,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.18)",
},
downloadTile: {
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 14,
backgroundColor: "#8C4BFF",
},
downloadText: {
fontFamily: FONT_FAMILY.InterSemiBold,
},
label: {
fontSize: 16,
fontSize: 15,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
},
description: {
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
clubCardSpacing: {
marginTop: 10,
},
blurView: {
paddingHorizontal: 10,
paddingBottom: 8,
paddingTop: 10,
},
itemContainer: {
borderRadius: 12,
overflow: "hidden",
backgroundColor: "#FFFFFF03",
shadowColor: "#0000001A",
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,
elevation: 5,
continueButton: {
marginTop: 10,
},
});
+1 -1
View File
@@ -23,7 +23,7 @@ const Production = () => {
>
<BorderGradientButton
title="Je télécharge ma chanson"
onPress={() => navigate(Routes.DownloadSongs)}
onPress={() => navigate(Routes.PlaybackDownload)}
/>
<View style={{ gap: 12 }}>
<GradientButton title="Je veux devenir Playbacker" />
+15 -70
View File
@@ -1,5 +1,5 @@
import { View, Text, Image } from "react-native";
import React, { useCallback, useMemo } from "react";
import React, { useCallback, useEffect } from "react";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton";
@@ -14,80 +14,25 @@ import { Routes } from "../../navigation";
import { useRoute } from "@react-navigation/native";
import { useUser } from "../../providers/UserDataProvider";
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
"trialing",
"active",
"past_due",
"unpaid",
]);
const pickStatus = (value) => {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed.toLowerCase() : null;
};
const hasActiveSubscription = (userData) => {
if (!userData) {
return false;
}
const statusCandidates = [
pickStatus(userData?.stripeSubscriptionStatus),
pickStatus(userData?.stripeSubscription?.status),
pickStatus(userData?.stripeSubscription?.stripeSubscriptionStatus),
pickStatus(userData?.stripeSubscription?.metadata?.status),
].filter(Boolean);
if (
statusCandidates.some((status) =>
ACTIVE_SUBSCRIPTION_STATUSES.has(status),
)
) {
return true;
}
const premiumUntil = userData?.premiumUntil;
if (premiumUntil) {
const asDate =
premiumUntil?.toDate?.() ||
(premiumUntil?.seconds ? new Date(premiumUntil.seconds * 1000) : null);
if (asDate && asDate > new Date()) {
return true;
}
}
if (userData?.premium?.active) {
return true;
}
return false;
};
const StreamSong = () => {
const params = useRoute().params;
const action = params?.action;
const { currentUserData = null } = useUser() || {};
const isPlaybackFlow = action === "playback";
const userHasActiveSubscription = useMemo(
() => hasActiveSubscription(currentUserData),
[currentUserData],
);
const headingText = isPlaybackFlow
? "Publier ton playback"
: "Publier ta chanson";
const project = params?.project || null;
const { hasActiveSubscription } = useUser() || {};
const userHasActiveSubscription = hasActiveSubscription;
useEffect(() => {
if (action && action !== "playback") {
navigate(Routes.SongDownload, { project });
}
}, [action, project]);
const headingText = "Publier ton playback";
const descriptionText = userHasActiveSubscription
? `Ton abonnement est actif, tu peux publier ${
isPlaybackFlow ? "ce playback" : "cette chanson"
} sur MusicLand et participer au concours.`
: `Pour publier ${isPlaybackFlow ? "ton playback" : "ta chanson"} sur la plateforme, tu dois rejoindre le Club MusicLand.`;
? "Ton abonnement est actif, publie ton playback et participe au concours."
: "Pour publier ton playback sur la plateforme, rejoins le Club MusicLand.";
const ctaLabel = userHasActiveSubscription
? isPlaybackFlow
? "Publier mon playback"
: "Publier ma chanson"
? "Publier mon playback"
: "Rejoindre le Club MusicLand";
const handlePrimaryAction = useCallback(() => {
if (userHasActiveSubscription) {