fix favicon, playlist subscription only

This commit is contained in:
Thomas Demirdjian
2026-03-12 13:46:19 +01:00
parent ebdf9862a7
commit 5c2bdc593c
14 changed files with 250 additions and 57 deletions
+2 -1
View File
@@ -82,7 +82,8 @@
] ]
}, },
"web": { "web": {
"bundler": "metro" "bundler": "metro",
"favicon": "./assets/favicon.png"
}, },
"plugins": [ "plugins": [
"react-native-compressor", "react-native-compressor",
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

+1 -1
View File
@@ -25,7 +25,7 @@ const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left
}, [currentUserData?.coins]) }, [currentUserData?.coins])
const handlePress = useCallback(() => { const handlePress = useCallback(() => {
navigate(Routes.Payments) navigate(Routes.Payments, { pack: 'packs' })
}, []) }, [])
if (!currentUID || Platform.OS === 'web') { if (!currentUID || Platform.OS === 'web') {
+30 -8
View File
@@ -1,9 +1,10 @@
import React, { useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Dimensions, FlatList, Image, Pressable, Text, TextInput, View } from 'react-native' import { Dimensions, FlatList, Image, Pressable, Text, TextInput, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet' import { SheetManager } from 'react-native-actions-sheet'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit' import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
import SwiperFlatList from 'react-native-swiper-flatlist' import SwiperFlatList from 'react-native-swiper-flatlist'
import { icons } from '../../assets' import { icons } from '../../assets'
import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard'
import { useUserData } from '../../providers/UserDataProvider' import { useUserData } from '../../providers/UserDataProvider'
import { addMusicToPlaylist, createPlaylist } from '../../screens/Library/Playlists/playlist' import { addMusicToPlaylist, createPlaylist } from '../../screens/Library/Playlists/playlist'
import { Palette } from '../../styles' import { Palette } from '../../styles'
@@ -20,8 +21,9 @@ const PlaylistModal = (props) => {
const [selectedId, setSelectedId] = useState('') const [selectedId, setSelectedId] = useState('')
const [playlist, setPlaylist] = useState('') const [playlist, setPlaylist] = useState('')
const [isCreating, setIsCreating] = useState(false) const [isCreating, setIsCreating] = useState(false)
const { currentUID, userPlaylists = [] } = useUserData() const { currentUID, userPlaylists = [], hasActiveSubscription } = useUserData()
const startAtCreate = props?.payload?.startAtCreate || false const startAtCreate = props?.payload?.startAtCreate || false
const ensureCanCreatePlaylist = usePlaylistCreationGuard()
const { setTooltip } = useMinuit() const { setTooltip } = useMinuit()
const sanitizedPlaylists = useMemo( const sanitizedPlaylists = useMemo(
@@ -35,9 +37,19 @@ const PlaylistModal = (props) => {
return sanitizedPlaylists.some((p) => (p?.name || '').trim().toLowerCase() === lower) return sanitizedPlaylists.some((p) => (p?.name || '').trim().toLowerCase() === lower)
}, [playlist, sanitizedPlaylists]) }, [playlist, sanitizedPlaylists])
const hideSheet = () => { const hideSheet = useCallback(() => {
SheetManager.hide('Playlist') SheetManager.hide('Playlist')
} }, [])
useEffect(() => {
if (!startAtCreate || hasActiveSubscription) {
return
}
if (!ensureCanCreatePlaylist()) {
hideSheet()
}
}, [ensureCanCreatePlaylist, hasActiveSubscription, hideSheet, startAtCreate])
return ( return (
<AppActionSheet id="Playlist"> <AppActionSheet id="Playlist">
@@ -45,7 +57,7 @@ const PlaylistModal = (props) => {
style={{ width, left: -14 }} style={{ width, left: -14 }}
ref={scrollRef} ref={scrollRef}
disableGesture disableGesture
index={startAtCreate ? 1 : 0} index={startAtCreate && hasActiveSubscription ? 1 : 0}
> >
<View style={{ width, paddingHorizontal: 14 }}> <View style={{ width, paddingHorizontal: 14 }}>
<View style={{ gap: 20 }}> <View style={{ gap: 20 }}>
@@ -70,12 +82,17 @@ const PlaylistModal = (props) => {
...Style.containerRow, ...Style.containerRow,
gap: 10, gap: 10,
}} }}
onPress={() => onPress={() => {
if (!ensureCanCreatePlaylist()) {
hideSheet()
return
}
scrollRef.current?.scrollToIndex({ scrollRef.current?.scrollToIndex({
index: 1, index: 1,
animated: true, animated: true,
}) })
} }}
> >
<Image source={icons.add} style={size({ size: 15 })} /> <Image source={icons.add} style={size({ size: 15 })} />
<Text <Text
@@ -212,6 +229,11 @@ const PlaylistModal = (props) => {
onPress={async () => { onPress={async () => {
if (!playlist?.trim()?.length || !currentUID) return if (!playlist?.trim()?.length || !currentUID) return
try { try {
if (!ensureCanCreatePlaylist()) {
hideSheet()
return
}
setIsCreating(true) setIsCreating(true)
const name = playlist.trim() const name = playlist.trim()
const lower = name.toLowerCase() const lower = name.toLowerCase()
@@ -232,7 +254,7 @@ const PlaylistModal = (props) => {
musics: props?.payload?.projectId ? [props.payload.projectId] : [], musics: props?.payload?.projectId ? [props.payload.projectId] : [],
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}) }, { hasActiveSubscription })
setTooltip({ setTooltip({
type: 'success', type: 'success',
+28 -8
View File
@@ -1,11 +1,12 @@
import { Portal } from '@gorhom/portal' import { Portal } from '@gorhom/portal'
import { BlurView } from 'expo-blur' import { BlurView } from 'expo-blur'
import React, { useEffect, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { FlatList, Image, Pressable, StyleSheet, Text, TextInput, View } from 'react-native' import { FlatList, Image, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'
import { SheetManager, useProviderContext } from 'react-native-actions-sheet' import { SheetManager, useProviderContext } from 'react-native-actions-sheet'
import { actionSheetEventManager } from 'react-native-actions-sheet/dist/src/eventmanager' import { actionSheetEventManager } from 'react-native-actions-sheet/dist/src/eventmanager'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit' import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
import { icons } from '../../assets' import { icons } from '../../assets'
import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard'
import { useUserData } from '../../providers/UserDataProvider' import { useUserData } from '../../providers/UserDataProvider'
import { addMusicToPlaylist, createPlaylist } from '../../screens/Library/Playlists/playlist' import { addMusicToPlaylist, createPlaylist } from '../../screens/Library/Playlists/playlist'
import { Palette } from '../../styles' import { Palette } from '../../styles'
@@ -18,16 +19,17 @@ const PlaylistModal = (props) => {
const [selectedId, setSelectedId] = useState('') const [selectedId, setSelectedId] = useState('')
const [playlist, setPlaylist] = useState('') const [playlist, setPlaylist] = useState('')
const [isCreating, setIsCreating] = useState(false) const [isCreating, setIsCreating] = useState(false)
const { currentUID, userPlaylists = [] } = useUserData() const { currentUID, userPlaylists = [], hasActiveSubscription } = useUserData()
const ensureCanCreatePlaylist = usePlaylistCreationGuard()
const { setTooltip } = useMinuit() const { setTooltip } = useMinuit()
const startAtCreate = props?.payload?.startAtCreate || false const startAtCreate = props?.payload?.startAtCreate || false
const [mode, setMode] = useState(startAtCreate ? 'create' : 'select') const [mode, setMode] = useState(startAtCreate && hasActiveSubscription ? 'create' : 'select')
const [visible, setVisible] = useState(true) const [visible, setVisible] = useState(true)
const providerContext = useProviderContext() const providerContext = useProviderContext()
useEffect(() => { useEffect(() => {
setMode(startAtCreate ? 'create' : 'select') setMode(startAtCreate && hasActiveSubscription ? 'create' : 'select')
}, [startAtCreate]) }, [hasActiveSubscription, startAtCreate])
const sanitizedPlaylists = useMemo( const sanitizedPlaylists = useMemo(
() => (Array.isArray(userPlaylists) ? userPlaylists : []), () => (Array.isArray(userPlaylists) ? userPlaylists : []),
@@ -44,7 +46,7 @@ const PlaylistModal = (props) => {
setVisible(true) setVisible(true)
}, [props?.sheetId, props?.payload]) }, [props?.sheetId, props?.payload])
const hideModal = () => { const hideModal = useCallback(() => {
setVisible(false) setVisible(false)
props?.payload?.onClose?.() props?.payload?.onClose?.()
@@ -56,7 +58,17 @@ const PlaylistModal = (props) => {
// close event manually to let the SheetProvider unmount it. // close event manually to let the SheetProvider unmount it.
actionSheetEventManager.publish(`onclose_${sheetId}`, undefined, providerContext) actionSheetEventManager.publish(`onclose_${sheetId}`, undefined, providerContext)
} }
} }, [props?.payload, props?.sheetId, providerContext])
useEffect(() => {
if (!startAtCreate || hasActiveSubscription) {
return
}
if (!ensureCanCreatePlaylist()) {
hideModal()
}
}, [ensureCanCreatePlaylist, hasActiveSubscription, hideModal, startAtCreate])
const handleValidateSelection = async () => { const handleValidateSelection = async () => {
if (!selectedId) return if (!selectedId) return
@@ -84,6 +96,10 @@ const PlaylistModal = (props) => {
const handleCreatePlaylist = async () => { const handleCreatePlaylist = async () => {
if (!playlist?.trim()?.length || !currentUID) return if (!playlist?.trim()?.length || !currentUID) return
if (!ensureCanCreatePlaylist()) {
hideModal()
return
}
try { try {
setIsCreating(true) setIsCreating(true)
const name = playlist.trim() const name = playlist.trim()
@@ -103,7 +119,7 @@ const PlaylistModal = (props) => {
musics: props?.payload?.projectId ? [props.payload.projectId] : [], musics: props?.payload?.projectId ? [props.payload.projectId] : [],
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}) }, { hasActiveSubscription })
setTooltip({ setTooltip({
type: 'success', type: 'success',
@@ -152,6 +168,10 @@ const PlaylistModal = (props) => {
alignSelf: 'center', alignSelf: 'center',
}} }}
onPress={() => { onPress={() => {
if (!ensureCanCreatePlaylist()) {
hideModal()
return
}
setPlaylist('') setPlaylist('')
setMode('create') setMode('create')
}} }}
+29
View File
@@ -0,0 +1,29 @@
import { useCallback } from 'react'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
import { Routes } from '../navigation'
import { navigate } from '../navigation/NavigationService'
import { useUserData } from '../providers/UserDataProvider'
import {
canCreatePlaylist,
PLAYLIST_CREATION_SUBSCRIPTION_MESSAGE,
} from '../utils/playlistCreationAccess'
const usePlaylistCreationGuard = () => {
const { hasActiveSubscription } = useUserData() || {}
const { setTooltip } = useMinuit()
return useCallback(() => {
if (canCreatePlaylist(hasActiveSubscription)) {
return true
}
setTooltip?.({
type: 'error',
text: PLAYLIST_CREATION_SUBSCRIPTION_MESSAGE,
})
navigate(Routes.Payments)
return false
}, [hasActiveSubscription, setTooltip])
}
export default usePlaylistCreationGuard
+1 -1
View File
@@ -122,7 +122,7 @@ export default ({
const handleCoinPress = React.useCallback(() => { const handleCoinPress = React.useCallback(() => {
if (!showCoinBadge) return if (!showCoinBadge) return
navigate(Routes.Payments) navigate(Routes.Payments, { pack: 'packs' })
}, [showCoinBadge]) }, [showCoinBadge])
const handleCloseCoinModal = React.useCallback(() => { const handleCloseCoinModal = React.useCallback(() => {
+1 -1
View File
@@ -434,7 +434,7 @@ const Home = ({ navigation, route }) => {
}, []) }, [])
const handleOpenCoinModal = useCallback(() => { const handleOpenCoinModal = useCallback(() => {
navigate(Routes.Payments) navigate(Routes.Payments, { pack: 'packs' })
}, []) }, [])
const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack
+12 -3
View File
@@ -3,6 +3,7 @@ import React from 'react'
import { Image, Platform, Pressable, Text, View } from 'react-native' import { Image, Platform, Pressable, Text, View } from 'react-native'
import { SheetManager } from 'react-native-actions-sheet' import { SheetManager } from 'react-native-actions-sheet'
import { background, icons } from '../../assets' import { background, icons } from '../../assets'
import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { Routes } from '../../navigation' import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService' import { navigate } from '../../navigation/NavigationService'
@@ -12,15 +13,23 @@ import { FONT_FAMILY } from '../../styles/Fonts'
import { size } from '../../styles/Style' import { size } from '../../styles/Style'
const AllMyPlaylist = () => { const AllMyPlaylist = () => {
const { userPlaylists = [] } = useUser() const { userPlaylists = [] } = useUser()
const ensureCanCreatePlaylist = usePlaylistCreationGuard()
const handleCreatePlaylistPress = () => {
if (!ensureCanCreatePlaylist()) {
return
}
SheetManager.show('Playlist', { payload: { startAtCreate: true } })
}
return ( return (
<Page <Page
headerType="NAVIGATION" headerType="NAVIGATION"
backgroundImg={background.libraryBG} backgroundImg={background.libraryBG}
title="Mes Playlists" title="Mes Playlists"
rightComponent={() => ( rightComponent={() => (
<Pressable <Pressable onPress={handleCreatePlaylistPress}>
onPress={() => SheetManager.show('Playlist', { payload: { startAtCreate: true } })}
>
<Image source={icons.add} style={size({ size: 20 })} resizeMode="contain" /> <Image source={icons.add} style={size({ size: 20 })} resizeMode="contain" />
</Pressable> </Pressable>
)} )}
+13 -7
View File
@@ -7,6 +7,7 @@ import { background, icons } from '../../assets'
import MoreMenu from '../../components/MoreMenu' import MoreMenu from '../../components/MoreMenu'
import { playlistsRef, projectsRef } from '../../config/firebase' import { playlistsRef, projectsRef } from '../../config/firebase'
import useDataFromArrayDocId from '../../hooks/useDataFromArrayId' import useDataFromArrayDocId from '../../hooks/useDataFromArrayId'
import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard'
import useDataFromRef from '../../hooks/useDataFromRef' import useDataFromRef from '../../hooks/useDataFromRef'
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails' import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
import usePlayer from '../../hooks/usePlayer' import usePlayer from '../../hooks/usePlayer'
@@ -22,6 +23,7 @@ const AllMyPlaylist = ({ route }) => {
const { playListId } = route?.params || {} const { playListId } = route?.params || {}
const [, setTooltip] = useGlobal('_tooltip') const [, setTooltip] = useGlobal('_tooltip')
const { userPlaylists = [] } = useUser() const { userPlaylists = [] } = useUser()
const ensureCanCreatePlaylist = usePlaylistCreationGuard()
const [selectedPlaylistId, setSelectedPlaylistId] = useState(playListId || null) const [selectedPlaylistId, setSelectedPlaylistId] = useState(playListId || null)
const { data: playlist } = useDataFromRef({ const { data: playlist } = useDataFromRef({
ref: selectedPlaylistId ? playlistsRef.doc(selectedPlaylistId) : null, ref: selectedPlaylistId ? playlistsRef.doc(selectedPlaylistId) : null,
@@ -140,6 +142,16 @@ const AllMyPlaylist = ({ route }) => {
}) })
}, [playlist?.musics, selectedPlaylistId]) }, [playlist?.musics, selectedPlaylistId])
const handleCreatePlaylistPress = useCallback(() => {
if (!ensureCanCreatePlaylist()) {
return
}
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}, [ensureCanCreatePlaylist])
return ( return (
<Page <Page
headerType="NAVIGATION" headerType="NAVIGATION"
@@ -233,13 +245,7 @@ const AllMyPlaylist = ({ route }) => {
) )
})} })}
</View> </View>
<Pressable <Pressable onPress={handleCreatePlaylistPress}>
onPress={() =>
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
>
<View <View
style={{ style={{
borderRadius: 12, borderRadius: 12,
+4 -1
View File
@@ -1,7 +1,10 @@
import { arrayUnion, playlistsRef } from '../../../config/firebase' import { arrayUnion, playlistsRef } from '../../../config/firebase'
import { assertCanCreatePlaylist } from '../../../utils/playlistCreationAccess'
export const createPlaylist = async (payload = {}) => { export const createPlaylist = async (payload = {}, options = {}) => {
try { try {
assertCanCreatePlaylist(options?.hasActiveSubscription)
const createdBy = payload?.createdBy const createdBy = payload?.createdBy
const name = payload?.name?.trim?.() const name = payload?.name?.trim?.()
const musics = Array.isArray(payload?.musics) ? payload.musics : [] const musics = Array.isArray(payload?.musics) ? payload.musics : []
+15 -10
View File
@@ -5,6 +5,7 @@ import { SheetManager } from 'react-native-actions-sheet'
import { icons } from '../../../assets' import { icons } from '../../../assets'
import GradientButton from '../../../components/GradientButton' import GradientButton from '../../../components/GradientButton'
import useLayoutType from '../../../hooks/useLayoutType' import useLayoutType from '../../../hooks/useLayoutType'
import usePlaylistCreationGuard from '../../../hooks/usePlaylistCreationGuard'
import { Routes } from '../../../navigation' import { Routes } from '../../../navigation'
import { navigate } from '../../../navigation/NavigationService' import { navigate } from '../../../navigation/NavigationService'
import { useUser } from '../../../providers/UserDataProvider' import { useUser } from '../../../providers/UserDataProvider'
@@ -16,16 +17,24 @@ const MyPlaylist = () => {
const { userPlaylists = [] } = useUser() const { userPlaylists = [] } = useUser()
const playlists = Array.isArray(userPlaylists) ? userPlaylists.slice(0, 2) : [] const playlists = Array.isArray(userPlaylists) ? userPlaylists.slice(0, 2) : []
const { isWeb } = useLayoutType() const { isWeb } = useLayoutType()
const ensureCanCreatePlaylist = usePlaylistCreationGuard()
const hasPlaylists = playlists.length > 0 const hasPlaylists = playlists.length > 0
const handleCreatePlaylistPress = () => {
if (!ensureCanCreatePlaylist()) {
return
}
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
return ( return (
<CardContainer <CardContainer
label="Mes Playlists" label="Mes Playlists"
onPress={() => navigate(Routes.AllMyPlaylist)} onPress={() => navigate(Routes.AllMyPlaylist)}
onPressPlus={() => onPressPlus={handleCreatePlaylistPress}
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
> >
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
{hasPlaylists ? ( {hasPlaylists ? (
@@ -91,11 +100,7 @@ const MyPlaylist = () => {
</Text> </Text>
<GradientButton <GradientButton
containerStyle={{ padding: 2 }} containerStyle={{ padding: 2 }}
onPress={() => onPress={handleCreatePlaylistPress}
SheetManager.show('Playlist', {
payload: { startAtCreate: true },
})
}
title=" Créer une playlist" title=" Créer une playlist"
/> />
</View> </View>
+104 -16
View File
@@ -183,6 +183,55 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
) )
} }
function CoinPackCard({ pack, onBuy, isProcessing }) {
const rawAmount = pack?.unitAmount ?? pack?.amount ?? pack?.price ?? 0
const formattedPrice = formatCurrency(rawAmount, pack?.currency)
const coins = Number(pack?.coinAmount ?? pack?.metadata?.coins ?? 0)
const packKey = getCoinPackKey(pack)
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
const displayName = details?.label || pack?.name || 'Pack'
const creditsText = details?.creditsDisplay
const displayPrice = details?.priceDisplay || formattedPrice || `${rawAmount / 100}`
const isPopular = details?.popular === true
return (
<View style={styles.cardWrapper}>
<BlurView intensity={18} tint="dark" style={styles.cardBlur}>
<View style={styles.cardContent}>
<View style={styles.cardHeader}>
<Text style={styles.planName}>{displayName}</Text>
{creditsText ? (
<Text style={styles.coinPackAmount}>{creditsText}</Text>
) : (
<CreditAmount value={coins} iconSize={24} textStyle={styles.coinPackAmountValue} />
)}
{details?.disclaimer ? (
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
) : null}
</View>
<View style={styles.priceBlock}>
{displayPrice ? <Text style={styles.priceValue}>{displayPrice}</Text> : null}
</View>
</View>
{isPopular ? (
<View style={styles.popularBadge}>
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
</View>
) : null}
<GradientButton
title={isProcessing ? 'Redirection...' : 'Acheter ce pack'}
onPress={() => onBuy(pack)}
disabled={isProcessing}
gradientStyle={styles.coinPackButtonGradient}
/>
</BlurView>
</View>
)
}
const PRICE_PRIORITY_BY_PERIOD = { const PRICE_PRIORITY_BY_PERIOD = {
monthly: [ monthly: [
'price_1SPgitCzf2o5bDRdbnhLFx6f', // Starter 'price_1SPgitCzf2o5bDRdbnhLFx6f', // Starter
@@ -248,6 +297,7 @@ export default function Subscriptions() {
const rawPack = route?.params?.pack const rawPack = route?.params?.pack
return typeof rawPack === 'string' ? rawPack.toLowerCase() : null return typeof rawPack === 'string' ? rawPack.toLowerCase() : null
}, [route?.params?.pack]) }, [route?.params?.pack])
const isCoinPackView = isMobile && initialPack === 'packs'
const initialSubscriptionPack = initialPack && initialPack !== 'packs' ? initialPack : null const initialSubscriptionPack = initialPack && initialPack !== 'packs' ? initialPack : null
@@ -285,6 +335,7 @@ export default function Subscriptions() {
const hasAnyPlan = availablePeriods.length > 0 const hasAnyPlan = availablePeriods.length > 0
const isLoadingPlans = isCatalogLoading && !hasAnyPlan const isLoadingPlans = isCatalogLoading && !hasAnyPlan
const combinedErrorMessage = errorMessage || catalogError const combinedErrorMessage = errorMessage || catalogError
const screenTitle = isCoinPackView ? COIN_PACK_SECTION_TITLE : 'Rejoignez le club MusicLand'
React.useEffect(() => { React.useEffect(() => {
initialPackHandledRef.current = false initialPackHandledRef.current = false
@@ -410,9 +461,9 @@ export default function Subscriptions() {
const mobileListContentInset = React.useMemo( const mobileListContentInset = React.useMemo(
() => ({ () => ({
paddingBottom: mobileActionSafePadding + gutters * 3, paddingBottom: isCoinPackView ? gutters * 2 : mobileActionSafePadding + gutters * 3,
}), }),
[mobileActionSafePadding] [isCoinPackView, mobileActionSafePadding]
) )
const handleBackPress = React.useCallback(() => { const handleBackPress = React.useCallback(() => {
goBack() goBack()
@@ -455,7 +506,7 @@ export default function Subscriptions() {
return ( return (
<> <>
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.title}>Rejoignez le club MusicLand</Text> <Text style={styles.title}>{screenTitle}</Text>
<View style={styles.balanceContainer}> <View style={styles.balanceContainer}>
<Text style={styles.balanceLabel}>Votre solde actuel :</Text> <Text style={styles.balanceLabel}>Votre solde actuel :</Text>
<CreditAmount value={currentUserData?.coins || 0} iconSize={20} showPlus={false} /> <CreditAmount value={currentUserData?.coins || 0} iconSize={20} showPlus={false} />
@@ -464,9 +515,13 @@ export default function Subscriptions() {
{combinedErrorMessage ? <Text style={styles.errorText}>{combinedErrorMessage}</Text> : null} {combinedErrorMessage ? <Text style={styles.errorText}>{combinedErrorMessage}</Text> : null}
</> </>
) )
}, [combinedErrorMessage]) }, [combinedErrorMessage, currentUserData?.coins, screenTitle])
const renderSegmentedControl = React.useCallback(() => { const renderSegmentedControl = React.useCallback(() => {
if (isCoinPackView) {
return null
}
const segments = [ const segments = [
{ key: 'monthly', label: 'Mensuel', plans: normalizedPlansByPeriod?.monthly }, { key: 'monthly', label: 'Mensuel', plans: normalizedPlansByPeriod?.monthly },
{ key: 'annual', label: 'Annuel', plans: normalizedPlansByPeriod?.annual }, { key: 'annual', label: 'Annuel', plans: normalizedPlansByPeriod?.annual },
@@ -504,7 +559,7 @@ export default function Subscriptions() {
})} })}
</View> </View>
) )
}, [handlePeriodChange, isMobile, normalizedPlansByPeriod, selectedPeriodKey]) }, [handlePeriodChange, isCoinPackView, isMobile, normalizedPlansByPeriod, selectedPeriodKey])
const renderMobilePlanItem = React.useCallback( const renderMobilePlanItem = React.useCallback(
({ item }) => ( ({ item }) => (
@@ -520,30 +575,51 @@ export default function Subscriptions() {
[handleSelect, selectedPeriodKey, selectedPriceId] [handleSelect, selectedPeriodKey, selectedPriceId]
) )
const renderMobileCoinPackItem = React.useCallback(
({ item }) => (
<View style={styles.mobileCard}>
<CoinPackCard
pack={item}
onBuy={handleBuyCredits}
isProcessing={processingPriceId === item.productId}
/>
</View>
),
[handleBuyCredits, processingPriceId]
)
const renderMobileEmptyComponent = React.useCallback(() => { const renderMobileEmptyComponent = React.useCallback(() => {
return ( return (
<View style={styles.mobileEmptyWrapper}> <View style={styles.mobileEmptyWrapper}>
{isLoadingPlans ? ( {(isCoinPackView ? isCatalogLoading && !coinPacks.length : isLoadingPlans) ? (
<ActivityIndicator color={Palette.white} /> <ActivityIndicator color={Palette.white} />
) : ( ) : (
<Text style={styles.emptyState}>Aucun abonnement Stripe disponible pour le moment.</Text> <Text style={styles.emptyState}>
{isCoinPackView
? "Aucun pack de crédits n'est disponible pour le moment."
: 'Aucun abonnement Stripe disponible pour le moment.'}
</Text>
)} )}
</View> </View>
) )
}, [isLoadingPlans]) }, [coinPacks.length, isCatalogLoading, isCoinPackView, isLoadingPlans])
const renderMobileFooterComponent = React.useCallback(() => { const renderMobileFooterComponent = React.useCallback(() => {
return ( return (
<View style={styles.mobileFooter}> <View style={styles.mobileFooter}>
{isLoadingPlans && currentPlanCount > 0 ? ( {(isCoinPackView ? isCatalogLoading && coinPacks.length > 0 : isLoadingPlans && currentPlanCount > 0) ? (
<View style={styles.inlineLoader}> <View style={styles.inlineLoader}>
<ActivityIndicator color={Palette.white} size="small" /> <ActivityIndicator color={Palette.white} size="small" />
</View> </View>
) : null} ) : null}
<Text style={styles.disclaimer}>{SUBSCRIPTION_DISCLAIMER}</Text> <Text style={styles.disclaimer}>
{isCoinPackView
? 'Les crédits sont ajoutés dès que le paiement Stripe est validé.'
: SUBSCRIPTION_DISCLAIMER}
</Text>
</View> </View>
) )
}, [currentPlanCount, isLoadingPlans]) }, [coinPacks.length, currentPlanCount, isCatalogLoading, isCoinPackView, isLoadingPlans])
const renderMobileTopSticky = React.useCallback(() => { const renderMobileTopSticky = React.useCallback(() => {
return ( return (
@@ -566,6 +642,7 @@ export default function Subscriptions() {
</View> </View>
) )
}, [actionButtonTitle, handleCheckout, isActionDisabled, mobileActionSafePadding]) }, [actionButtonTitle, handleCheckout, isActionDisabled, mobileActionSafePadding])
const renderWebBackButton = React.useCallback(() => { const renderWebBackButton = React.useCallback(() => {
if (isMobile) return null if (isMobile) return null
return ( return (
@@ -586,7 +663,7 @@ export default function Subscriptions() {
<View style={styles.root}> <View style={styles.root}>
<Page <Page
headerType="NAVIGATE" headerType="NAVIGATE"
title="Abonnements" title={isCoinPackView ? COIN_PACK_SECTION_TITLE : 'Abonnements'}
scrollEnabled={isWeb} scrollEnabled={isWeb}
width={isWeb ? 960 : undefined} width={isWeb ? 960 : undefined}
containerStyle={styles.page} containerStyle={styles.page}
@@ -601,9 +678,9 @@ export default function Subscriptions() {
<View style={[styles.content, isMobile && styles.mobileContent]}> <View style={[styles.content, isMobile && styles.mobileContent]}>
{isMobile ? ( {isMobile ? (
<FlatList <FlatList
data={currentPlans} data={isCoinPackView ? coinPacks : currentPlans}
keyExtractor={(plan) => plan.priceId} keyExtractor={(item) => item.productId || item.priceId}
renderItem={renderMobilePlanItem} renderItem={isCoinPackView ? renderMobileCoinPackItem : renderMobilePlanItem}
style={styles.mobileList} style={styles.mobileList}
contentContainerStyle={[styles.mobileListContent, mobileListContentInset]} contentContainerStyle={[styles.mobileListContent, mobileListContentInset]}
ListEmptyComponent={renderMobileEmptyComponent} ListEmptyComponent={renderMobileEmptyComponent}
@@ -716,7 +793,7 @@ export default function Subscriptions() {
</View> </View>
</View> </View>
</Page> </Page>
{isMobile ? renderMobileBottomActions() : null} {isMobile && !isCoinPackView ? renderMobileBottomActions() : null}
</View> </View>
) )
} }
@@ -1162,6 +1239,17 @@ const styles = StyleSheet.create({
fontSize: isWeb ? 22 : 20, fontSize: isWeb ? 22 : 20,
color: Palette.white, color: Palette.white,
}, },
coinPackAmount: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 16,
color: Palette.white,
},
coinPackAmountValue: {
fontSize: 20,
},
coinPackButtonGradient: {
width: '100%',
},
period: { period: {
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
fontSize: 14, fontSize: 14,
+10
View File
@@ -0,0 +1,10 @@
export const PLAYLIST_CREATION_SUBSCRIPTION_MESSAGE =
'La création de playlist est réservée aux abonnés.'
export const canCreatePlaylist = (hasActiveSubscription = false) => hasActiveSubscription === true
export const assertCanCreatePlaylist = (hasActiveSubscription = false) => {
if (!canCreatePlaylist(hasActiveSubscription)) {
throw new Error(PLAYLIST_CREATION_SUBSCRIPTION_MESSAGE)
}
}