diff --git a/src/components/bottomsheets/CommentsBottomSheet.js b/src/components/bottomsheets/CommentsBottomSheet.js index 4377a1c..c1eaca8 100644 --- a/src/components/bottomsheets/CommentsBottomSheet.js +++ b/src/components/bottomsheets/CommentsBottomSheet.js @@ -30,6 +30,7 @@ import { getArtistDisplayName } from '../../utils/artistName'; import { Palette } from '../../styles'; import { FONT_FAMILY } from '../../styles/Fonts'; import ProfilePicture from '../ProfilePicture'; +import { ensureAuthenticated } from '../../utils/authRedirect'; let externalOpen; let externalClose; @@ -72,6 +73,16 @@ const CommentsBottomSheet = () => { const [text, setText] = useState(''); const [typing, setTyping] = useState(false); + const requireAuth = useCallback(() => { + return ensureAuthenticated(currentUID, { + onIntercept: () => { + try { + modalRef.current?.dismiss?.(); + } catch (_error) {} + }, + }); + }, [currentUID]); + useEffect(() => { externalOpen = (pid) => { setProjectId(pid || null); @@ -127,7 +138,8 @@ const CommentsBottomSheet = () => { const onSend = async () => { const value = (text || '').trim(); - if (!value || !projectId || !currentUID) return; + if (!value || !projectId) return; + if (!requireAuth()) return; try { setText(''); const docRef = await projectsRef @@ -354,11 +366,23 @@ const CommentsBottomSheet = () => { placeholderTextColor='#FFFFFFAA' value={text} onChangeText={(t) => { + if (!currentUID && !requireAuth()) { + return; + } setText(t); }} onFocus={() => { + if (!requireAuth()) { + setTyping(false); + return; + } setTyping(true); }} + onPressIn={() => { + if (!requireAuth()) { + setTyping(false); + } + }} onBlur={() => setTyping(false)} style={{ flex: 1, diff --git a/src/components/player/GlobalAudioPlayer.js b/src/components/player/GlobalAudioPlayer.js index 852f4eb..ab5545b 100644 --- a/src/components/player/GlobalAudioPlayer.js +++ b/src/components/player/GlobalAudioPlayer.js @@ -12,6 +12,7 @@ import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails"; import { useUser } from "../../providers/UserDataProvider"; import { gutters, Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; +import { ensureAuthenticated } from "../../utils/authRedirect"; const formatDuration = (ms) => { const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000)); @@ -82,22 +83,34 @@ const GlobalAudioPlayer = () => { } }, [pendingLike, likedFromDoc]); - const handleToggleLike = useCallback(async (event) => { - event?.stopPropagation?.(); - if (!projectId || !currentUID || isLikeProcessing) return; - const next = !effectiveIsLiked; - setPendingLike(next); - try { - await projectsRef.doc(projectId).set( - { - likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID), - }, - { merge: true } - ); - } catch (_error) { - setPendingLike(null); - } - }, [currentUID, effectiveIsLiked, isLikeProcessing, projectId]); + const handleToggleLike = useCallback( + async (event) => { + event?.stopPropagation?.(); + if (!projectId || isLikeProcessing) return; + if ( + !ensureAuthenticated(currentUID, { + onIntercept: () => { + setPendingLike(null); + }, + }) + ) { + return; + } + const next = !effectiveIsLiked; + setPendingLike(next); + try { + await projectsRef.doc(projectId).set( + { + likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID), + }, + { merge: true } + ); + } catch (_error) { + setPendingLike(null); + } + }, + [currentUID, effectiveIsLiked, isLikeProcessing, projectId] + ); const insets = useSafeAreaInsets(); @@ -199,11 +212,11 @@ const GlobalAudioPlayer = () => { diff --git a/src/navigation/BottomTab.js b/src/navigation/BottomTab.js index b6be09a..39774db 100644 --- a/src/navigation/BottomTab.js +++ b/src/navigation/BottomTab.js @@ -1,6 +1,7 @@ import { Motion } from "@legendapp/motion"; import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import React from "reactn"; +import { useGlobal } from "reactn"; import { Palette } from "../styles"; import { Routes } from "./Routes"; import { tabs } from "../assets"; @@ -15,6 +16,9 @@ import HomeStack from "./HomeStack"; const BottomTab = createBottomTabNavigator(); export const BottomTabScreen = () => { + const [currentUID] = useGlobal("currentUID"); + const isAuthenticated = !!currentUID; + const renderIcon = (icon, focused) => ( { headerShown: false, tabBarShowLabel: true, tabBarIcon: ({ focused }) => renderIcon(tabs.albums, focused), + hide: !isAuthenticated, }} /> { headerShown: false, tabBarShowLabel: true, tabBarIcon: ({ focused }) => renderIcon(tabs.person, focused), + hide: !isAuthenticated, }} /> diff --git a/src/navigation/TabBar.js b/src/navigation/TabBar.js index b7d2fe6..9c497c2 100644 --- a/src/navigation/TabBar.js +++ b/src/navigation/TabBar.js @@ -19,6 +19,13 @@ const TabBar = ({ state = {}, descriptors = {}, navigation = {} }) => { : isIOS ? Math.max(bottomInset, fallbackBottomOffset) : bottomInset + fallbackBottomOffset; + const visibleRoutesCount = Math.max( + 1, + (state?.routes || []).reduce((count, route) => { + const descriptor = descriptors[route.key]; + return descriptor?.options?.hide ? count : count + 1; + }, 0) + ); return ( { key={route.key} style={{ alignSelf: "center", - width: `${100 / state?.routes?.length}%`, + width: `${100 / visibleRoutesCount}%`, height: "100%", ...Style.containerCenter, }} diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index be7e7c3..f6a2a77 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -15,6 +15,7 @@ import firebase, { } from "../config/firebase"; import { getUserPreferredArtistName } from "../utils/artistName"; import { getLikeFieldPath, LIKE_TARGET } from "../utils/likes"; +import { ensureAuthenticated } from "../utils/authRedirect"; const SONG_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.SONG); const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK); @@ -191,6 +192,9 @@ export default ({ children }) => { }; const createNewProject = async ({ hasLyrics = false } = {}) => { + if (!ensureAuthenticated(currentUID)) { + return null; + } const existingProjectsCount = Array.isArray(userProjects) ? userProjects.length : 0; @@ -225,7 +229,10 @@ export default ({ children }) => { }; const followUser = async (userId) => { try { - if (currentUID && userId) { + if (!ensureAuthenticated(currentUID)) { + return; + } + if (userId) { await usersRef.doc(userId).set( { followedBy: arrayUnion(currentUID), @@ -243,7 +250,10 @@ export default ({ children }) => { const unfollowUser = async (userId) => { try { - if (currentUID && userId) { + if (!ensureAuthenticated(currentUID)) { + return; + } + if (userId) { await usersRef.doc(userId).set( { followedBy: arrayRemove(currentUID), diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 87573ee..1d64815 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -21,6 +21,7 @@ import { projectsRef, usersRef, videosRef } from "../../config/firebase"; import useDataFromRef from "../../hooks/useDataFromRef"; import { isWeb } from "../../hooks/useLayoutType.js"; import Page from "../../layouts/Page"; +import LandingPage from "../LandingPage"; import { navigate } from "../../navigation/NavigationService"; import { Routes } from "../../navigation/Routes"; import { useUser } from "../../providers/UserDataProvider"; @@ -54,6 +55,10 @@ const Home = ({ navigation, route }) => { } = useUser(); const { setTooltip } = useMinuit(); + if (!currentUID) { + return ; + } + const projects = useMemo( () => (Array.isArray(userProjects) ? userProjects : []), [userProjects] diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js index 2b026a6..d73d6b7 100644 --- a/src/screens/Library/MusicDetails.js +++ b/src/screens/Library/MusicDetails.js @@ -1,16 +1,20 @@ import { Feather } from "@expo/vector-icons"; -import useTrackController from "../../hooks/useTrackController"; -import usePlayer from "../../hooks/usePlayer"; import { Image as ExpoImage } from "expo-image"; -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { + Linking, Pressable, Image as RNImage, ScrollView, StyleSheet, Text, View, - Linking, } from "react-native"; import { SheetManager } from "react-native-actions-sheet"; import { @@ -29,21 +33,24 @@ import { usersRef, } from "../../config/firebase"; import useDataFromRef from "../../hooks/useDataFromRef"; +import usePlayer from "../../hooks/usePlayer"; +import useTrackController from "../../hooks/useTrackController"; import Page from "../../layouts/Page"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import Style, { gutters, size } from "../../styles/Style"; import { getArtistDisplayName } from "../../utils/artistName"; +import { ensureAuthenticated } from "../../utils/authRedirect"; +import { + createMusicSharePayload, + openShareSheet, +} from "../../utils/shareSheet"; import { formatStructureLabel, getPromptLabelForStructure, getSegmentMeta, normalizeStructureType, } from "../../utils/songStructure"; -import { - createMusicSharePayload, - openShareSheet, -} from "../../utils/shareSheet"; // 20 secondes const timeBeforeIncrement = 20000; @@ -413,13 +420,7 @@ const MusicDetails = ({ route }) => { console.log("MusicDetails seekBy error", e?.message); } }, - [ - trackDescriptor, - positionMs, - isCurrentTrack, - ensureLoaded, - seekTrackBy, - ] + [trackDescriptor, positionMs, isCurrentTrack, ensureLoaded, seekTrackBy] ); const handleLyricsSeek = useCallback( @@ -495,7 +496,8 @@ const MusicDetails = ({ route }) => { .replace(/\s+/g, " ") .trim(); const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim()); - const stripSectionTag = (txt) => String(txt || "").replace(SECTION_TAG_REGEX, ""); + const stripSectionTag = (txt) => + String(txt || "").replace(SECTION_TAG_REGEX, ""); const parseSectionTag = (txt) => { const match = String(txt || "").match(SECTION_TAG_REGEX); if (!match) return null; @@ -503,7 +505,8 @@ const MusicDetails = ({ route }) => { const lower = label.toLowerCase(); let type = "section"; if (lower.includes("refrain") || lower.includes("chorus")) type = "refrain"; - else if (lower.includes("couplet") || lower.includes("verse")) type = "couplet"; + else if (lower.includes("couplet") || lower.includes("verse")) + type = "couplet"; else if ( lower.includes("pré") || lower.includes("prechorus") || @@ -632,7 +635,9 @@ const MusicDetails = ({ route }) => { current = null; return; } - const lines = groupAlignedWordsToLines(current.words, { removeTags: true }); + const lines = groupAlignedWordsToLines(current.words, { + removeTags: true, + }); if (!lines.length) { current = null; return; @@ -785,7 +790,10 @@ const MusicDetails = ({ route }) => { justifyContent: "center", opacity: isCurrentTrack ? 1 : 0.4, }} - contentStyle={{ alignItems: "center", justifyContent: "center" }} + contentStyle={{ + alignItems: "center", + justifyContent: "center", + }} > { { - if (!projectId || !currentUID) return; + if (!projectId) return; + if (!ensureAuthenticated(currentUID)) return; const next = !fav; setFav(next); try { diff --git a/src/screens/Library/MusicDetails.web.js b/src/screens/Library/MusicDetails.web.js index dc77215..2cad411 100644 --- a/src/screens/Library/MusicDetails.web.js +++ b/src/screens/Library/MusicDetails.web.js @@ -32,6 +32,7 @@ import { projectsRef, usersRef, } from "../../config/firebase"; +import { ensureAuthenticated } from "../../utils/authRedirect"; import useDataFromRef from "../../hooks/useDataFromRef"; import useTrackController from "../../hooks/useTrackController"; import usePlayer from "../../hooks/usePlayer"; @@ -1001,7 +1002,8 @@ const MusicDetails = ({ route }) => { { - if (!projectId || !currentUID) return; + if (!projectId) return; + if (!ensureAuthenticated(currentUID)) return; const next = !fav; setFav(next); try { diff --git a/src/screens/Library/components/MusicCard.js b/src/screens/Library/components/MusicCard.js index b1fe313..ccc9555 100644 --- a/src/screens/Library/components/MusicCard.js +++ b/src/screens/Library/components/MusicCard.js @@ -14,6 +14,7 @@ import { useGlobal } from "reactn"; import { icons, img } from "../../../assets"; import PressableScale from "../../../components/PressableScale"; import { LIKE_TARGET, toggleProjectLike } from "../../../utils/likes"; +import { ensureAuthenticated } from "../../../utils/authRedirect"; import { Palette, Style } from "../../../styles"; import { FONT_FAMILY } from "../../../styles/Fonts"; import { size } from "../../../styles/Style"; @@ -55,7 +56,8 @@ const MusicCard = ({ }, [JSON.stringify(likedBy), currentUID]); const toggleLike = async () => { - if (!projectId || !currentUID) return; + if (!projectId) return; + if (!ensureAuthenticated(currentUID)) return; const next = !selected; setSelected(next); try { diff --git a/src/screens/Playbacks/Playbacks.js b/src/screens/Playbacks/Playbacks.js index f31b273..2ee192b 100644 --- a/src/screens/Playbacks/Playbacks.js +++ b/src/screens/Playbacks/Playbacks.js @@ -28,6 +28,7 @@ import { LIKE_TARGET, toggleProjectLike, } from "../../utils/likes"; +import { ensureAuthenticated } from "../../utils/authRedirect"; import { createPlaybackSharePayload, openShareSheet, @@ -260,11 +261,20 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { imageProps={{ priority: "high" }} /> - {owner?.id && currentUID && owner.id !== currentUID && ( + {owner?.id && owner.id !== currentUID && ( { if (isFollowActionPending) return; + if ( + !ensureAuthenticated(currentUID, { + onIntercept: () => { + setIsFollowActionPending(false); + }, + }) + ) { + return; + } try { setIsFollowActionPending(true); @@ -327,7 +337,10 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => { { try { - if (!currentUID || !item?.id) return; + if (!item?.id) return; + if (!ensureAuthenticated(currentUID)) { + return; + } const nextLiked = !isLiked; setIsLiked(nextLiked); setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1))); diff --git a/src/screens/Playbacks/components/CommentsPanel.web.js b/src/screens/Playbacks/components/CommentsPanel.web.js index dab9209..18204d7 100644 --- a/src/screens/Playbacks/components/CommentsPanel.web.js +++ b/src/screens/Playbacks/components/CommentsPanel.web.js @@ -31,6 +31,7 @@ import { Palette } from "../../../styles"; import { FONT_FAMILY } from "../../../styles/Fonts"; import { size } from "../../../styles/Style"; import { getArtistDisplayName } from "../../../utils/artistName"; +import { ensureAuthenticated } from "../../../utils/authRedirect"; moment.locale("fr"); @@ -58,6 +59,10 @@ const CommentsPanel = ({ ); const [text, setText] = useState(""); const scrollRef = useRef(null); + const requireAuth = useCallback( + () => ensureAuthenticated(currentUID), + [currentUID] + ); useEffect(() => { setText(""); @@ -111,7 +116,8 @@ const CommentsPanel = ({ const onSend = useCallback(async () => { const value = (text || "").trim(); - if (!value || !projectId || !currentUID) return; + if (!value || !projectId) return; + if (!requireAuth()) return; try { setText(""); const docRef = await projectsRef @@ -153,6 +159,7 @@ const CommentsPanel = ({ currentUserData, onCommentAdded, projectId, + requireAuth, setComments, text, ]); @@ -226,23 +233,42 @@ const CommentsPanel = ({ { + if (!requireAuth()) { + return; + } + setText(value); + }} placeholder={ canComment ? "Commente" : "Connecte-toi pour laisser un commentaire" } placeholderTextColor="#FFFFFF90" - editable={canComment} + editable + onFocus={() => { + if (!requireAuth()) { + try { + inputRef.current?.blur?.(); + } catch (_e) {} + } + }} + onPressIn={() => { + if (!requireAuth()) { + try { + inputRef.current?.blur?.(); + } catch (_e) {} + } + }} style={[styles.commentInput, !canComment && { opacity: 0.6 }]} /> [ styles.sendButton, - (!canComment || !text.trim()) && styles.sendButtonDisabled, - pressed && canComment && styles.sendButtonPressed, + !text.trim() && styles.sendButtonDisabled, + pressed && text.trim() && styles.sendButtonPressed, ]} > )} - {owner?.id && currentUID && owner.id !== currentUID && ( + {owner?.id && owner.id !== currentUID && ( { + if ( + !ensureAuthenticated(currentUID, { + onIntercept: () => { + setIsFollowing(false); + }, + }) + ) { + return; + } try { const next = !isFollowing; setIsFollowing(next); @@ -465,7 +475,10 @@ const PlaybackItem = ({ { try { - if (!currentUID || !item?.id) return; + if (!item?.id) return; + if (!ensureAuthenticated(currentUID)) { + return; + } const nextLiked = !isLiked; setIsLiked(nextLiked); setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1))); diff --git a/src/screens/Splash.js b/src/screens/Splash.js index 6a398b4..baf0d3e 100644 --- a/src/screens/Splash.js +++ b/src/screens/Splash.js @@ -33,12 +33,12 @@ export default ({ navigation }) => { ], }); } catch (error) { - // Pas d'utilisateur connecté => vers Login + // Pas d'utilisateur connecté => accueil avec tab bar navigation.reset({ index: 0, routes: [ { - name: Routes.LandingPage, + name: Routes.BottomTab, }, ], }); diff --git a/src/utils/authRedirect.js b/src/utils/authRedirect.js new file mode 100644 index 0000000..af25001 --- /dev/null +++ b/src/utils/authRedirect.js @@ -0,0 +1,40 @@ +import { getGlobal } from "reactn"; +import { navigate } from "../navigation/NavigationService"; +import { Routes } from "../navigation/Routes"; + +const redirectToRegister = () => { + try { + const activeRoute = getGlobal()?.activeRouteName; + if (activeRoute === Routes.Register) { + return; + } + } catch (error) { + console.warn("authRedirect: unable to read active route", error); + } + navigate(Routes.Register); +}; + +export const ensureAuthenticated = (currentUID, options = {}) => { + const { onIntercept } = options || {}; + if (currentUID) { + return true; + } + try { + if (typeof onIntercept === "function") { + onIntercept(); + } + } catch (error) { + console.warn("authRedirect: onIntercept error", error); + } + redirectToRegister(); + return false; +}; + +export const withAuthGuard = async (currentUID, handler, options = {}) => { + if (!ensureAuthenticated(currentUID, options)) { + return; + } + return handler?.(); +}; + +export default ensureAuthenticated; diff --git a/src/utils/likes.js b/src/utils/likes.js index 8990b54..e2f66c8 100644 --- a/src/utils/likes.js +++ b/src/utils/likes.js @@ -1,4 +1,5 @@ import { arrayRemove, arrayUnion, projectsRef } from "../config/firebase"; +import { ensureAuthenticated } from "./authRedirect"; export const LIKE_TARGET = { SONG: "song", @@ -31,7 +32,8 @@ export const toggleProjectLike = async ({ currentUID, next, }) => { - if (!projectId || !currentUID) return; + if (!projectId) return; + if (!ensureAuthenticated(currentUID)) return; const fieldPath = getLikeFieldPath(target); await projectsRef.doc(projectId).set( { @@ -42,4 +44,3 @@ export const toggleProjectLike = async ({ { merge: true } ); }; -