pay for music
This commit is contained in:
+198
-107
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Image as ExpoImage } from 'expo-image'
|
||||
import {
|
||||
Linking,
|
||||
@@ -13,13 +13,12 @@ import {
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
import AppCheckbox from '../../components/AppCheckbox'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation/Routes'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUser } from '../../providers/UserDataProvider'
|
||||
import { useStripe } from '../../providers/StripeProvider'
|
||||
import { gutters, Palette, Style } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
@@ -31,7 +30,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import { getArtistDisplayName } from '../../utils/artistName'
|
||||
import { toDate } from '../../utils/dateFormatting'
|
||||
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
|
||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||
import AdventureChoiceModal from './components/AdventureChoiceModal'
|
||||
import ShareAndCreditsModal from './components/ShareAndCreditsModal'
|
||||
@@ -43,21 +41,26 @@ const SongDownload = ({ route }) => {
|
||||
coverOptions: routeCoverOptions,
|
||||
} = route?.params || {}
|
||||
const { selectedProject, updateProjectData, hasActiveSubscription } = useUser()
|
||||
const { createSongDownloadCheckout } = useStripe()
|
||||
const { setTooltip } = useMinuit()
|
||||
const { setLoading } = useGlobalLoading()
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
||||
const [isCheckoutLaunching, setIsCheckoutLaunching] = useState(false)
|
||||
const [downloadPaymentPending, setDownloadPaymentPending] = useState(false)
|
||||
const { videos } = useUser()
|
||||
const part1Url = isWeb ? videos?.benaiPart1 : videos?.benaiPart1
|
||||
const [showIntro, setShowIntro] = useState(null)
|
||||
const [showAdventureModal, setShowAdventureModal] = useState(false)
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [adventureChoice, setAdventureChoice] = useState('pending')
|
||||
const adventureGateTriggeredRef = useRef(false)
|
||||
|
||||
const projectForStage = useMemo(
|
||||
() => routeProject || selectedProject || null,
|
||||
[routeProject, selectedProject]
|
||||
)
|
||||
const projectForStage = useMemo(() => {
|
||||
if (routeProject?.id && selectedProject?.id && routeProject.id === selectedProject.id) {
|
||||
return selectedProject
|
||||
}
|
||||
return routeProject || selectedProject || null
|
||||
}, [routeProject, selectedProject])
|
||||
|
||||
const coverOptions = useMemo(() => {
|
||||
if (Array.isArray(routeCoverOptions) && routeCoverOptions.length) {
|
||||
@@ -93,6 +96,47 @@ const SongDownload = ({ route }) => {
|
||||
typeof projectForStage?.title === 'string' && projectForStage.title.trim()
|
||||
? projectForStage.title.trim()
|
||||
: 'Musicland Track'
|
||||
const projectId = projectForStage?.id || null
|
||||
const downloadPurchaseStatus = projectForStage?.downloadPurchase?.status || null
|
||||
const hasPaidDownload = downloadPurchaseStatus === 'paid'
|
||||
const canDownloadDirectly = hasActiveSubscription || hasPaidDownload
|
||||
const downloadLabel = canDownloadDirectly
|
||||
? 'Télécharger mon morceau'
|
||||
: 'Acheter ce morceau pour 1,99€'
|
||||
const shouldShowDownloadPage = adventureChoice === 'stop'
|
||||
|
||||
useEffect(() => {
|
||||
if (adventureGateTriggeredRef.current) {
|
||||
return
|
||||
}
|
||||
if (adventureChoice !== 'pending') {
|
||||
return
|
||||
}
|
||||
if (typeof videos === 'undefined') {
|
||||
return
|
||||
}
|
||||
adventureGateTriggeredRef.current = true
|
||||
if (part1Url) {
|
||||
setShowIntro(part1Url)
|
||||
} else {
|
||||
setShowAdventureModal(true)
|
||||
}
|
||||
}, [adventureChoice, part1Url, videos])
|
||||
|
||||
useEffect(() => {
|
||||
if (adventureChoice !== 'pending') {
|
||||
return
|
||||
}
|
||||
if (!adventureGateTriggeredRef.current) {
|
||||
return
|
||||
}
|
||||
if (showIntro) {
|
||||
return
|
||||
}
|
||||
if (!showAdventureModal) {
|
||||
setShowAdventureModal(true)
|
||||
}
|
||||
}, [adventureChoice, showAdventureModal, showIntro])
|
||||
|
||||
const continueFlow = useCallback(async () => {
|
||||
if (!selectedOption) {
|
||||
@@ -137,32 +181,6 @@ const SongDownload = ({ route }) => {
|
||||
updateProjectData,
|
||||
])
|
||||
|
||||
const handleContinue = useCallback(() => {
|
||||
if (!hasAcceptedPublication) {
|
||||
if (setTooltip) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: 'Confirme la diffusion sur Musicland et YouTube avant de continuer',
|
||||
})
|
||||
} else {
|
||||
Alert.alert(
|
||||
'Confirmation requise',
|
||||
'Confirme la diffusion sur Musicland et YouTube avant de continuer'
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (hasActiveSubscription) {
|
||||
if (part1Url) {
|
||||
setShowIntro(part1Url)
|
||||
} else {
|
||||
continueFlow()
|
||||
}
|
||||
return
|
||||
}
|
||||
setShowConfirmModal(true)
|
||||
}, [continueFlow, hasAcceptedPublication, hasActiveSubscription, part1Url, setTooltip])
|
||||
|
||||
const handleCloseIntro = useCallback(() => {
|
||||
setShowIntro(null)
|
||||
setShowAdventureModal(true)
|
||||
@@ -170,14 +188,10 @@ const SongDownload = ({ route }) => {
|
||||
|
||||
const handleContinueAdventure = useCallback(() => {
|
||||
setShowAdventureModal(false)
|
||||
setAdventureChoice('continue')
|
||||
continueFlow()
|
||||
}, [continueFlow])
|
||||
|
||||
const handleStopAdventure = useCallback(() => {
|
||||
setShowAdventureModal(false)
|
||||
setShowShareModal(true)
|
||||
}, [])
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const downloadUrl =
|
||||
projectForStage?.songUrl ||
|
||||
@@ -387,64 +401,140 @@ const SongDownload = ({ route }) => {
|
||||
}
|
||||
}, [coverUrl, isDownloading, projectForStage, selectedOption, trackTitle])
|
||||
|
||||
const startSongDownloadCheckout = useCallback(
|
||||
async ({ force = false } = {}) => {
|
||||
if (!projectId) {
|
||||
if (setTooltip) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: "Impossible d'identifier le projet pour le paiement.",
|
||||
})
|
||||
} else {
|
||||
Alert.alert('Paiement', "Impossible d'identifier le projet pour le paiement.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!force && (isCheckoutLaunching || downloadPaymentPending)) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsCheckoutLaunching(true)
|
||||
setDownloadPaymentPending(true)
|
||||
try {
|
||||
await createSongDownloadCheckout(projectId)
|
||||
} catch (error) {
|
||||
setDownloadPaymentPending(false)
|
||||
if (setTooltip) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: error?.message || "Une erreur est survenue lors du paiement.",
|
||||
})
|
||||
} else {
|
||||
Alert.alert('Paiement', error?.message || "Une erreur est survenue lors du paiement.")
|
||||
}
|
||||
} finally {
|
||||
setIsCheckoutLaunching(false)
|
||||
}
|
||||
},
|
||||
[
|
||||
createSongDownloadCheckout,
|
||||
downloadPaymentPending,
|
||||
isCheckoutLaunching,
|
||||
projectId,
|
||||
setTooltip,
|
||||
]
|
||||
)
|
||||
|
||||
const handleDownloadPress = useCallback(() => {
|
||||
if (isDownloading || isCheckoutLaunching) {
|
||||
return
|
||||
}
|
||||
if (canDownloadDirectly) {
|
||||
handleDownload()
|
||||
return
|
||||
}
|
||||
if (downloadPaymentPending) {
|
||||
Alert.alert(
|
||||
'Paiement en attente',
|
||||
"Le paiement n'est pas encore confirmé. Si tu as déjà payé, patiente quelques instants.",
|
||||
[
|
||||
{ text: 'Attendre', style: 'cancel' },
|
||||
{ text: 'Relancer le paiement', onPress: () => startSongDownloadCheckout({ force: true }) },
|
||||
]
|
||||
)
|
||||
return
|
||||
}
|
||||
startSongDownloadCheckout()
|
||||
}, [
|
||||
canDownloadDirectly,
|
||||
downloadPaymentPending,
|
||||
handleDownload,
|
||||
isCheckoutLaunching,
|
||||
isDownloading,
|
||||
startSongDownloadCheckout,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadPaymentPending || !hasPaidDownload || isDownloading) {
|
||||
return
|
||||
}
|
||||
setDownloadPaymentPending(false)
|
||||
handleDownload()
|
||||
}, [downloadPaymentPending, handleDownload, hasPaidDownload, isDownloading])
|
||||
|
||||
const handleStopAdventure = useCallback(() => {
|
||||
setShowAdventureModal(false)
|
||||
setAdventureChoice('stop')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<MusicLandHeader onPressBack={goBack} progress={84} />
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.container,
|
||||
!isWeb && styles.containerMobile,
|
||||
styles.scrollContent,
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<CreateLyricsHeader title="Ta pochette est validée" />
|
||||
<View style={styles.coverRow}>
|
||||
{coverUrl ? (
|
||||
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
||||
) : (
|
||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||
{shouldShowDownloadPage ? (
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
styles.container,
|
||||
!isWeb && styles.containerMobile,
|
||||
styles.scrollContent,
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<CreateLyricsHeader title="Ta pochette est validée" />
|
||||
<View style={styles.coverRow}>
|
||||
{coverUrl ? (
|
||||
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
||||
) : (
|
||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.downloadColumn}>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.downloadTile,
|
||||
(isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
||||
]}
|
||||
onPress={handleDownloadPress}
|
||||
disabled={isDownloading || isCheckoutLaunching}
|
||||
>
|
||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
||||
</Pressable>
|
||||
{!canDownloadDirectly && downloadPaymentPending ? (
|
||||
<Text style={styles.downloadPendingText}>
|
||||
Paiement en attente de confirmation...
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Pressable style={styles.downloadTile} onPress={handleDownload}>
|
||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||
<Text style={styles.downloadText}>Acheter ce morceau pour 1,99€</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<ClubAdvantagesCard style={styles.clubCardSpacing} />
|
||||
|
||||
<View style={styles.consentContainer}>
|
||||
<AppCheckbox
|
||||
selected={hasAcceptedPublication}
|
||||
onPress={() => setHasAcceptedPublication((prev) => !prev)}
|
||||
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
|
||||
/>
|
||||
<Text style={styles.consentDescription}>
|
||||
Cette confirmation est requise pour continuer.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<BorderGradientButton
|
||||
title={hasActiveSubscription ? 'Continuer' : 'Continuer sans générer de revenus'}
|
||||
onPress={handleContinue}
|
||||
containerStyle={styles.continueButton}
|
||||
/>
|
||||
</ScrollView>
|
||||
<SubscriptionConfirmModal
|
||||
isVisible={showConfirmModal}
|
||||
setIsVisible={setShowConfirmModal}
|
||||
onJoinClub={() => navigate(Routes.Payments)}
|
||||
onContinue={() => {
|
||||
if (part1Url) {
|
||||
setShowIntro(part1Url)
|
||||
} else {
|
||||
continueFlow()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ClubAdvantagesCard style={styles.clubCardSpacing} />
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View style={styles.adventureGatePlaceholder} />
|
||||
)}
|
||||
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
|
||||
<AdventureChoiceModal
|
||||
isVisible={showAdventureModal}
|
||||
@@ -483,16 +573,6 @@ const styles = StyleSheet.create({
|
||||
scrollContent: {
|
||||
paddingBottom: gutters * 2.6,
|
||||
},
|
||||
consentContainer: {
|
||||
gap: 6,
|
||||
marginTop: 10,
|
||||
},
|
||||
consentDescription: {
|
||||
fontSize: 13,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
color: Palette.white,
|
||||
opacity: 0.8,
|
||||
},
|
||||
coverRow: {
|
||||
flexDirection: isWeb ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
@@ -524,15 +604,26 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: '#8C4BFF',
|
||||
borderWidth: 0,
|
||||
},
|
||||
downloadColumn: {
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
downloadText: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 15,
|
||||
color: Palette.white,
|
||||
},
|
||||
clubCardSpacing: {
|
||||
marginTop: gutters * 0.5,
|
||||
downloadPendingText: {
|
||||
marginTop: 6,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
opacity: 0.8,
|
||||
},
|
||||
continueButton: {
|
||||
adventureGatePlaceholder: {
|
||||
flex: 1,
|
||||
},
|
||||
clubCardSpacing: {
|
||||
marginTop: gutters * 0.5,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user