From 5c2bdc593cb487d72087d160b39c9b7b15674e43 Mon Sep 17 00:00:00 2001 From: Thomas Demirdjian Date: Thu, 12 Mar 2026 13:46:19 +0100 Subject: [PATCH] fix favicon, playlist subscription only --- app.json | 3 +- assets/favicon.png | Bin 0 -> 4618 bytes src/components/MobileCoinBadge.js | 2 +- src/components/modal/PlaylistModal.js | 38 ++++-- src/components/modal/PlaylistModal.web.js | 36 ++++-- src/hooks/usePlaylistCreationGuard.js | 29 +++++ src/layouts/Page.js | 2 +- src/screens/Home/Home.js | 2 +- src/screens/Library/AllMyPlaylist.js | 15 ++- src/screens/Library/AllMyPlaylist.web.js | 20 ++-- src/screens/Library/Playlists/playlist.js | 5 +- src/screens/Library/components/MyPlaylist.js | 25 ++-- src/screens/Subscriptions.js | 120 ++++++++++++++++--- src/utils/playlistCreationAccess.js | 10 ++ 14 files changed, 250 insertions(+), 57 deletions(-) create mode 100644 assets/favicon.png create mode 100644 src/hooks/usePlaylistCreationGuard.js create mode 100644 src/utils/playlistCreationAccess.js diff --git a/app.json b/app.json index 45133ca..808957f 100644 --- a/app.json +++ b/app.json @@ -82,7 +82,8 @@ ] }, "web": { - "bundler": "metro" + "bundler": "metro", + "favicon": "./assets/favicon.png" }, "plugins": [ "react-native-compressor", diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3d55754a37cec972e94bf216423c2f69687219a8 GIT binary patch literal 4618 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 literal 0 HcmV?d00001 diff --git a/src/components/MobileCoinBadge.js b/src/components/MobileCoinBadge.js index b17b31f..49aa69f 100644 --- a/src/components/MobileCoinBadge.js +++ b/src/components/MobileCoinBadge.js @@ -25,7 +25,7 @@ const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left }, [currentUserData?.coins]) const handlePress = useCallback(() => { - navigate(Routes.Payments) + navigate(Routes.Payments, { pack: 'packs' }) }, []) if (!currentUID || Platform.OS === 'web') { diff --git a/src/components/modal/PlaylistModal.js b/src/components/modal/PlaylistModal.js index 438d146..aa4e20e 100644 --- a/src/components/modal/PlaylistModal.js +++ b/src/components/modal/PlaylistModal.js @@ -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 { SheetManager } from 'react-native-actions-sheet' import useMinuit from 'react-native-minuit/src/hooks/useMinuit' import SwiperFlatList from 'react-native-swiper-flatlist' import { icons } from '../../assets' +import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard' import { useUserData } from '../../providers/UserDataProvider' import { addMusicToPlaylist, createPlaylist } from '../../screens/Library/Playlists/playlist' import { Palette } from '../../styles' @@ -20,8 +21,9 @@ const PlaylistModal = (props) => { const [selectedId, setSelectedId] = useState('') const [playlist, setPlaylist] = useState('') const [isCreating, setIsCreating] = useState(false) - const { currentUID, userPlaylists = [] } = useUserData() + const { currentUID, userPlaylists = [], hasActiveSubscription } = useUserData() const startAtCreate = props?.payload?.startAtCreate || false + const ensureCanCreatePlaylist = usePlaylistCreationGuard() const { setTooltip } = useMinuit() const sanitizedPlaylists = useMemo( @@ -35,9 +37,19 @@ const PlaylistModal = (props) => { return sanitizedPlaylists.some((p) => (p?.name || '').trim().toLowerCase() === lower) }, [playlist, sanitizedPlaylists]) - const hideSheet = () => { + const hideSheet = useCallback(() => { SheetManager.hide('Playlist') - } + }, []) + + useEffect(() => { + if (!startAtCreate || hasActiveSubscription) { + return + } + + if (!ensureCanCreatePlaylist()) { + hideSheet() + } + }, [ensureCanCreatePlaylist, hasActiveSubscription, hideSheet, startAtCreate]) return ( @@ -45,7 +57,7 @@ const PlaylistModal = (props) => { style={{ width, left: -14 }} ref={scrollRef} disableGesture - index={startAtCreate ? 1 : 0} + index={startAtCreate && hasActiveSubscription ? 1 : 0} > @@ -70,12 +82,17 @@ const PlaylistModal = (props) => { ...Style.containerRow, gap: 10, }} - onPress={() => + onPress={() => { + if (!ensureCanCreatePlaylist()) { + hideSheet() + return + } + scrollRef.current?.scrollToIndex({ index: 1, animated: true, }) - } + }} > { onPress={async () => { if (!playlist?.trim()?.length || !currentUID) return try { + if (!ensureCanCreatePlaylist()) { + hideSheet() + return + } + setIsCreating(true) const name = playlist.trim() const lower = name.toLowerCase() @@ -232,7 +254,7 @@ const PlaylistModal = (props) => { musics: props?.payload?.projectId ? [props.payload.projectId] : [], createdAt: new Date(), updatedAt: new Date(), - }) + }, { hasActiveSubscription }) setTooltip({ type: 'success', diff --git a/src/components/modal/PlaylistModal.web.js b/src/components/modal/PlaylistModal.web.js index 2c03191..1062fd2 100644 --- a/src/components/modal/PlaylistModal.web.js +++ b/src/components/modal/PlaylistModal.web.js @@ -1,11 +1,12 @@ import { Portal } from '@gorhom/portal' 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 { SheetManager, useProviderContext } from 'react-native-actions-sheet' import { actionSheetEventManager } from 'react-native-actions-sheet/dist/src/eventmanager' import useMinuit from 'react-native-minuit/src/hooks/useMinuit' import { icons } from '../../assets' +import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard' import { useUserData } from '../../providers/UserDataProvider' import { addMusicToPlaylist, createPlaylist } from '../../screens/Library/Playlists/playlist' import { Palette } from '../../styles' @@ -18,16 +19,17 @@ const PlaylistModal = (props) => { const [selectedId, setSelectedId] = useState('') const [playlist, setPlaylist] = useState('') const [isCreating, setIsCreating] = useState(false) - const { currentUID, userPlaylists = [] } = useUserData() + const { currentUID, userPlaylists = [], hasActiveSubscription } = useUserData() + const ensureCanCreatePlaylist = usePlaylistCreationGuard() const { setTooltip } = useMinuit() 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 providerContext = useProviderContext() useEffect(() => { - setMode(startAtCreate ? 'create' : 'select') - }, [startAtCreate]) + setMode(startAtCreate && hasActiveSubscription ? 'create' : 'select') + }, [hasActiveSubscription, startAtCreate]) const sanitizedPlaylists = useMemo( () => (Array.isArray(userPlaylists) ? userPlaylists : []), @@ -44,7 +46,7 @@ const PlaylistModal = (props) => { setVisible(true) }, [props?.sheetId, props?.payload]) - const hideModal = () => { + const hideModal = useCallback(() => { setVisible(false) props?.payload?.onClose?.() @@ -56,7 +58,17 @@ const PlaylistModal = (props) => { // close event manually to let the SheetProvider unmount it. 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 () => { if (!selectedId) return @@ -84,6 +96,10 @@ const PlaylistModal = (props) => { const handleCreatePlaylist = async () => { if (!playlist?.trim()?.length || !currentUID) return + if (!ensureCanCreatePlaylist()) { + hideModal() + return + } try { setIsCreating(true) const name = playlist.trim() @@ -103,7 +119,7 @@ const PlaylistModal = (props) => { musics: props?.payload?.projectId ? [props.payload.projectId] : [], createdAt: new Date(), updatedAt: new Date(), - }) + }, { hasActiveSubscription }) setTooltip({ type: 'success', @@ -152,6 +168,10 @@ const PlaylistModal = (props) => { alignSelf: 'center', }} onPress={() => { + if (!ensureCanCreatePlaylist()) { + hideModal() + return + } setPlaylist('') setMode('create') }} diff --git a/src/hooks/usePlaylistCreationGuard.js b/src/hooks/usePlaylistCreationGuard.js new file mode 100644 index 0000000..9295e9a --- /dev/null +++ b/src/hooks/usePlaylistCreationGuard.js @@ -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 diff --git a/src/layouts/Page.js b/src/layouts/Page.js index d9381a8..5f301c7 100644 --- a/src/layouts/Page.js +++ b/src/layouts/Page.js @@ -122,7 +122,7 @@ export default ({ const handleCoinPress = React.useCallback(() => { if (!showCoinBadge) return - navigate(Routes.Payments) + navigate(Routes.Payments, { pack: 'packs' }) }, [showCoinBadge]) const handleCloseCoinModal = React.useCallback(() => { diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index a7c8b9d..0c29949 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -434,7 +434,7 @@ const Home = ({ navigation, route }) => { }, []) const handleOpenCoinModal = useCallback(() => { - navigate(Routes.Payments) + navigate(Routes.Payments, { pack: 'packs' }) }, []) const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack diff --git a/src/screens/Library/AllMyPlaylist.js b/src/screens/Library/AllMyPlaylist.js index d907526..27cd006 100644 --- a/src/screens/Library/AllMyPlaylist.js +++ b/src/screens/Library/AllMyPlaylist.js @@ -3,6 +3,7 @@ import React from 'react' import { Image, Platform, Pressable, Text, View } from 'react-native' import { SheetManager } from 'react-native-actions-sheet' import { background, icons } from '../../assets' +import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard' import Page from '../../layouts/Page' import { Routes } from '../../navigation' import { navigate } from '../../navigation/NavigationService' @@ -12,15 +13,23 @@ import { FONT_FAMILY } from '../../styles/Fonts' import { size } from '../../styles/Style' const AllMyPlaylist = () => { const { userPlaylists = [] } = useUser() + const ensureCanCreatePlaylist = usePlaylistCreationGuard() + + const handleCreatePlaylistPress = () => { + if (!ensureCanCreatePlaylist()) { + return + } + + SheetManager.show('Playlist', { payload: { startAtCreate: true } }) + } + return ( ( - SheetManager.show('Playlist', { payload: { startAtCreate: true } })} - > + )} diff --git a/src/screens/Library/AllMyPlaylist.web.js b/src/screens/Library/AllMyPlaylist.web.js index 9d82d00..47b941d 100644 --- a/src/screens/Library/AllMyPlaylist.web.js +++ b/src/screens/Library/AllMyPlaylist.web.js @@ -7,6 +7,7 @@ import { background, icons } from '../../assets' import MoreMenu from '../../components/MoreMenu' import { playlistsRef, projectsRef } from '../../config/firebase' import useDataFromArrayDocId from '../../hooks/useDataFromArrayId' +import usePlaylistCreationGuard from '../../hooks/usePlaylistCreationGuard' import useDataFromRef from '../../hooks/useDataFromRef' import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails' import usePlayer from '../../hooks/usePlayer' @@ -22,6 +23,7 @@ const AllMyPlaylist = ({ route }) => { const { playListId } = route?.params || {} const [, setTooltip] = useGlobal('_tooltip') const { userPlaylists = [] } = useUser() + const ensureCanCreatePlaylist = usePlaylistCreationGuard() const [selectedPlaylistId, setSelectedPlaylistId] = useState(playListId || null) const { data: playlist } = useDataFromRef({ ref: selectedPlaylistId ? playlistsRef.doc(selectedPlaylistId) : null, @@ -140,6 +142,16 @@ const AllMyPlaylist = ({ route }) => { }) }, [playlist?.musics, selectedPlaylistId]) + const handleCreatePlaylistPress = useCallback(() => { + if (!ensureCanCreatePlaylist()) { + return + } + + SheetManager.show('Playlist', { + payload: { startAtCreate: true }, + }) + }, [ensureCanCreatePlaylist]) + return ( { ) })} - - SheetManager.show('Playlist', { - payload: { startAtCreate: true }, - }) - } - > + { +export const createPlaylist = async (payload = {}, options = {}) => { try { + assertCanCreatePlaylist(options?.hasActiveSubscription) + const createdBy = payload?.createdBy const name = payload?.name?.trim?.() const musics = Array.isArray(payload?.musics) ? payload.musics : [] diff --git a/src/screens/Library/components/MyPlaylist.js b/src/screens/Library/components/MyPlaylist.js index dc18cde..223ab8d 100644 --- a/src/screens/Library/components/MyPlaylist.js +++ b/src/screens/Library/components/MyPlaylist.js @@ -5,6 +5,7 @@ import { SheetManager } from 'react-native-actions-sheet' import { icons } from '../../../assets' import GradientButton from '../../../components/GradientButton' import useLayoutType from '../../../hooks/useLayoutType' +import usePlaylistCreationGuard from '../../../hooks/usePlaylistCreationGuard' import { Routes } from '../../../navigation' import { navigate } from '../../../navigation/NavigationService' import { useUser } from '../../../providers/UserDataProvider' @@ -16,16 +17,24 @@ const MyPlaylist = () => { const { userPlaylists = [] } = useUser() const playlists = Array.isArray(userPlaylists) ? userPlaylists.slice(0, 2) : [] const { isWeb } = useLayoutType() + const ensureCanCreatePlaylist = usePlaylistCreationGuard() const hasPlaylists = playlists.length > 0 + + const handleCreatePlaylistPress = () => { + if (!ensureCanCreatePlaylist()) { + return + } + + SheetManager.show('Playlist', { + payload: { startAtCreate: true }, + }) + } + return ( navigate(Routes.AllMyPlaylist)} - onPressPlus={() => - SheetManager.show('Playlist', { - payload: { startAtCreate: true }, - }) - } + onPressPlus={handleCreatePlaylistPress} > {hasPlaylists ? ( @@ -91,11 +100,7 @@ const MyPlaylist = () => { - SheetManager.show('Playlist', { - payload: { startAtCreate: true }, - }) - } + onPress={handleCreatePlaylistPress} title=" Créer une playlist" /> diff --git a/src/screens/Subscriptions.js b/src/screens/Subscriptions.js index 8ce636b..03dc195 100644 --- a/src/screens/Subscriptions.js +++ b/src/screens/Subscriptions.js @@ -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 ( + + + + + {displayName} + {creditsText ? ( + {creditsText} + ) : ( + + )} + {details?.disclaimer ? ( + {details.disclaimer} + ) : null} + + + + {displayPrice ? {displayPrice} : null} + + + + {isPopular ? ( + + Le plus populaire ! + + ) : null} + + onBuy(pack)} + disabled={isProcessing} + gradientStyle={styles.coinPackButtonGradient} + /> + + + ) +} + const PRICE_PRIORITY_BY_PERIOD = { monthly: [ 'price_1SPgitCzf2o5bDRdbnhLFx6f', // Starter @@ -248,6 +297,7 @@ export default function Subscriptions() { const rawPack = route?.params?.pack return typeof rawPack === 'string' ? rawPack.toLowerCase() : null }, [route?.params?.pack]) + const isCoinPackView = isMobile && initialPack === 'packs' const initialSubscriptionPack = initialPack && initialPack !== 'packs' ? initialPack : null @@ -285,6 +335,7 @@ export default function Subscriptions() { const hasAnyPlan = availablePeriods.length > 0 const isLoadingPlans = isCatalogLoading && !hasAnyPlan const combinedErrorMessage = errorMessage || catalogError + const screenTitle = isCoinPackView ? COIN_PACK_SECTION_TITLE : 'Rejoignez le club MusicLand' React.useEffect(() => { initialPackHandledRef.current = false @@ -410,9 +461,9 @@ export default function Subscriptions() { const mobileListContentInset = React.useMemo( () => ({ - paddingBottom: mobileActionSafePadding + gutters * 3, + paddingBottom: isCoinPackView ? gutters * 2 : mobileActionSafePadding + gutters * 3, }), - [mobileActionSafePadding] + [isCoinPackView, mobileActionSafePadding] ) const handleBackPress = React.useCallback(() => { goBack() @@ -455,7 +506,7 @@ export default function Subscriptions() { return ( <> - Rejoignez le club MusicLand + {screenTitle} Votre solde actuel : @@ -464,9 +515,13 @@ export default function Subscriptions() { {combinedErrorMessage ? {combinedErrorMessage} : null} ) - }, [combinedErrorMessage]) + }, [combinedErrorMessage, currentUserData?.coins, screenTitle]) const renderSegmentedControl = React.useCallback(() => { + if (isCoinPackView) { + return null + } + const segments = [ { key: 'monthly', label: 'Mensuel', plans: normalizedPlansByPeriod?.monthly }, { key: 'annual', label: 'Annuel', plans: normalizedPlansByPeriod?.annual }, @@ -504,7 +559,7 @@ export default function Subscriptions() { })} ) - }, [handlePeriodChange, isMobile, normalizedPlansByPeriod, selectedPeriodKey]) + }, [handlePeriodChange, isCoinPackView, isMobile, normalizedPlansByPeriod, selectedPeriodKey]) const renderMobilePlanItem = React.useCallback( ({ item }) => ( @@ -520,30 +575,51 @@ export default function Subscriptions() { [handleSelect, selectedPeriodKey, selectedPriceId] ) + const renderMobileCoinPackItem = React.useCallback( + ({ item }) => ( + + + + ), + [handleBuyCredits, processingPriceId] + ) + const renderMobileEmptyComponent = React.useCallback(() => { return ( - {isLoadingPlans ? ( + {(isCoinPackView ? isCatalogLoading && !coinPacks.length : isLoadingPlans) ? ( ) : ( - Aucun abonnement Stripe disponible pour le moment. + + {isCoinPackView + ? "Aucun pack de crédits n'est disponible pour le moment." + : 'Aucun abonnement Stripe disponible pour le moment.'} + )} ) - }, [isLoadingPlans]) + }, [coinPacks.length, isCatalogLoading, isCoinPackView, isLoadingPlans]) const renderMobileFooterComponent = React.useCallback(() => { return ( - {isLoadingPlans && currentPlanCount > 0 ? ( + {(isCoinPackView ? isCatalogLoading && coinPacks.length > 0 : isLoadingPlans && currentPlanCount > 0) ? ( ) : null} - {SUBSCRIPTION_DISCLAIMER} + + {isCoinPackView + ? 'Les crédits sont ajoutés dès que le paiement Stripe est validé.' + : SUBSCRIPTION_DISCLAIMER} + ) - }, [currentPlanCount, isLoadingPlans]) + }, [coinPacks.length, currentPlanCount, isCatalogLoading, isCoinPackView, isLoadingPlans]) const renderMobileTopSticky = React.useCallback(() => { return ( @@ -566,6 +642,7 @@ export default function Subscriptions() { ) }, [actionButtonTitle, handleCheckout, isActionDisabled, mobileActionSafePadding]) + const renderWebBackButton = React.useCallback(() => { if (isMobile) return null return ( @@ -586,7 +663,7 @@ export default function Subscriptions() { {isMobile ? ( plan.priceId} - renderItem={renderMobilePlanItem} + data={isCoinPackView ? coinPacks : currentPlans} + keyExtractor={(item) => item.productId || item.priceId} + renderItem={isCoinPackView ? renderMobileCoinPackItem : renderMobilePlanItem} style={styles.mobileList} contentContainerStyle={[styles.mobileListContent, mobileListContentInset]} ListEmptyComponent={renderMobileEmptyComponent} @@ -716,7 +793,7 @@ export default function Subscriptions() { - {isMobile ? renderMobileBottomActions() : null} + {isMobile && !isCoinPackView ? renderMobileBottomActions() : null} ) } @@ -1162,6 +1239,17 @@ const styles = StyleSheet.create({ fontSize: isWeb ? 22 : 20, color: Palette.white, }, + coinPackAmount: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 16, + color: Palette.white, + }, + coinPackAmountValue: { + fontSize: 20, + }, + coinPackButtonGradient: { + width: '100%', + }, period: { fontFamily: FONT_FAMILY.InterRegular, fontSize: 14, diff --git a/src/utils/playlistCreationAccess.js b/src/utils/playlistCreationAccess.js new file mode 100644 index 0000000..7b9cc46 --- /dev/null +++ b/src/utils/playlistCreationAccess.js @@ -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) + } +}