feat: update onboarding and application flows

This commit is contained in:
Jacques_tirot
2026-08-25 15:55:11 +02:00
parent 4fb8a3e1e0
commit bc7edcf3a7
38 changed files with 7234 additions and 289 deletions
@@ -5,6 +5,7 @@ import { img } from '../../../assets'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import SubscriptionBadge from '../../../components/SubscriptionBadge'
import useResolvedMediaUri from '../../../hooks/useResolvedMediaUri'
const PlaybacksCard = ({
rank = 1,
@@ -18,6 +19,7 @@ const PlaybacksCard = ({
const resolveUri = (value) =>
typeof value === 'string' && value.trim().length > 0 ? value : null
const imageUri = resolveUri(thumbnailUrl) ?? resolveUri(coverUrl)
const resolvedImageUri = useResolvedMediaUri(imageUri)
return (
<Pressable onPress={onPress}>
@@ -63,7 +65,7 @@ const PlaybacksCard = ({
{rank}
</Text>
<Image
source={imageUri ? { uri: imageUri } : img.placeholder3}
source={resolvedImageUri ? { uri: resolvedImageUri } : img.placeholder3}
style={{ width: 67, height: 108, borderRadius: 16 }}
/>
<View style={{ flex: 1 }}>
+5 -2
View File
@@ -7,6 +7,7 @@ import Style, { size } from '../../../styles/Style'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import SubscriptionBadge from '../../../components/SubscriptionBadge'
import useResolvedMediaUri from '../../../hooks/useResolvedMediaUri'
const SongCard = ({
rank = 1,
@@ -17,6 +18,8 @@ const SongCard = ({
subscriptionLevel = null,
views = null,
}) => {
const resolvedCoverUrl = useResolvedMediaUri(coverUrl)
return (
<Pressable
onPress={onPress || undefined}
@@ -25,9 +28,9 @@ const SongCard = ({
gap: 6,
}}
>
{coverUrl ? (
{resolvedCoverUrl ? (
<ExpoImage
source={{ uri: coverUrl }}
source={{ uri: resolvedCoverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
+66 -67
View File
@@ -1,19 +1,8 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
ActivityIndicator,
Image,
Platform,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { background, icons } from '../assets'
import EntryJingle from '../components/EntryJingle'
import { useCallback, useMemo, useState } from 'react'
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { background, icons, videos } from '../assets'
import FullscreenIntroVideo from '../components/FullscreenIntroVideo'
import { videosRef } from '../config/firebase'
import useDataFromRef from '../hooks/useDataFromRef'
import Page from '../layouts/Page'
import { Routes } from '../navigation'
import { navigate, reset } from '../navigation/NavigationService'
@@ -22,45 +11,25 @@ import { Palette } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
const LANGUAGE_STORAGE_KEY = 'preferredLanguage'
const FLOW_PHASE = {
JINGLE: 'jingle',
LANGUAGE: 'language',
WELCOME_VIDEO: 'welcomeVideo',
}
export default function LandingPage() {
export default function LandingPage({ route }) {
const { currentUID } = useUser()
const [videoUrl, setVideoUrl] = useState(null)
const [phase, setPhase] = useState(() =>
route?.params?.startAtLanguage ? FLOW_PHASE.LANGUAGE : FLOW_PHASE.JINGLE
)
const [selectedLanguage, setSelectedLanguage] = useState(null)
const continuationHandledRef = useRef(false)
const isAuthenticated = !!currentUID
const { data: welcomeVideo, loading: welcomeVideoLoading } = useDataFromRef({
ref: selectedLanguage ? videosRef.doc(selectedLanguage) : null,
initialState: null,
simpleRef: true,
condition: !!selectedLanguage,
refreshArray: [selectedLanguage],
})
const openRegistration = useCallback(() => {
navigate(Routes.Register)
}, [])
const handleSelectLanguage = useCallback(async (language) => {
try {
await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, language)
} catch (error) {
console.warn('LandingPage: failed to persist language', error)
}
continuationHandledRef.current = false
setSelectedLanguage(language)
}, [])
const handleVideoClose = useCallback(() => {
setVideoUrl(null)
openRegistration()
}, [openRegistration])
useEffect(() => {
if (!isAuthenticated) {
return
}
const openMainMenu = useCallback(() => {
reset({
index: 0,
routes: [
@@ -73,33 +42,55 @@ export default function LandingPage() {
},
],
})
}, [isAuthenticated])
}, [])
useEffect(() => {
if (
!selectedLanguage ||
welcomeVideoLoading ||
continuationHandledRef.current ||
isAuthenticated
) {
const handleJingleClose = useCallback(() => {
if (isAuthenticated) {
openMainMenu()
return
}
setPhase(FLOW_PHASE.LANGUAGE)
}, [isAuthenticated, openMainMenu])
continuationHandledRef.current = true
const nextVideoUrl =
Platform.OS === 'web' ? welcomeVideo?.registrationIntroWeb : welcomeVideo?.registrationIntro
if (typeof nextVideoUrl === 'string' && nextVideoUrl.trim().length > 0) {
setVideoUrl(nextVideoUrl)
return
const handleSelectLanguage = useCallback(async (language) => {
try {
await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, language)
} catch (error) {
console.warn('LandingPage: failed to persist language', error)
}
setSelectedLanguage(language)
setPhase(FLOW_PHASE.WELCOME_VIDEO)
}, [])
openRegistration()
}, [isAuthenticated, openRegistration, selectedLanguage, welcomeVideo, welcomeVideoLoading])
const handleWelcomeVideoClose = useCallback(() => {
setSelectedLanguage(null)
setPhase(FLOW_PHASE.LANGUAGE)
requestAnimationFrame(openRegistration)
}, [openRegistration])
const activeVideo = useMemo(() => {
if (phase === FLOW_PHASE.JINGLE) return videos.entryJingle
if (phase !== FLOW_PHASE.WELCOME_VIDEO) return null
return selectedLanguage === 'en' ? videos.welcomeEn : videos.welcomeFr
}, [phase, selectedLanguage])
const activeVideoClose =
phase === FLOW_PHASE.JINGLE ? handleJingleClose : handleWelcomeVideoClose
const skipLabel =
phase === FLOW_PHASE.JINGLE
? 'Passer / Skip'
: selectedLanguage === 'en'
? 'Skip video'
: 'Passer la vidéo'
const playLabel =
phase === FLOW_PHASE.JINGLE
? 'Lancer le jingle / Play jingle'
: selectedLanguage === 'en'
? 'Play video with sound'
: 'Lire la vidéo avec le son'
return (
<>
<EntryJingle active={!isAuthenticated && !selectedLanguage} />
<Page title="Landing Page" backgroundImg={background.homeBGWeb}>
<View
style={{
@@ -109,7 +100,7 @@ export default function LandingPage() {
}}
>
<View style={styles.content}>
{!selectedLanguage && (
{phase === FLOW_PHASE.LANGUAGE ? (
<View style={styles.languageRow}>
<TouchableOpacity
style={[styles.languageButton]}
@@ -126,14 +117,22 @@ export default function LandingPage() {
<Text style={styles.languageText}>English</Text>
</TouchableOpacity>
</View>
)}
{selectedLanguage && welcomeVideoLoading ? (
<ActivityIndicator color={Palette.white} size="large" />
) : null}
</View>
</View>
</Page>
<FullscreenIntroVideo url={videoUrl} visible={!!videoUrl} onClose={handleVideoClose} />
{activeVideo ? (
<FullscreenIntroVideo
key={phase}
url={activeVideo}
visible
onClose={activeVideoClose}
contentFit="contain"
requireAudio
skipLabel={skipLabel}
playLabel={playLabel}
/>
) : null}
</>
)
}
+4 -2
View File
@@ -6,6 +6,7 @@ import { responsiveWidth } from 'react-native-responsive-dimensions'
import { useGlobal } from 'reactn'
import { icons, img } from '../../../assets'
import PressableScale from '../../../components/PressableScale'
import useResolvedMediaUri from '../../../hooks/useResolvedMediaUri'
import { LIKE_TARGET, toggleProjectLike } from '../../../utils/likes'
import { ensureAuthenticated } from '../../../utils/authRedirect'
import { Palette, Style } from '../../../styles'
@@ -23,6 +24,7 @@ const MusicCard = ({
likeTarget = LIKE_TARGET.SONG,
}) => {
const [currentUID] = useGlobal('currentUID')
const resolvedImageUri = useResolvedMediaUri(imageUri)
const [selected, setSelected] = useState(false)
const [layout, setLayout] = useState(null)
const [menuPos, setMenuPos] = useState({
@@ -113,9 +115,9 @@ const MusicCard = ({
onPress={onPress}
onLayout={(e) => setLayout(e.nativeEvent.layout)}
>
{imageUri ? (
{resolvedImageUri ? (
<ExpoImage
source={{ uri: imageUri }}
source={{ uri: resolvedImageUri }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
+19 -1
View File
@@ -31,7 +31,7 @@ import { getRouteAfterAuthentication } from '../utils/registrationFlow'
const LANGUAGE_STORAGE_KEY = 'preferredLanguage'
const Register = () => {
const Register = ({ navigation }) => {
const [email, setEmail] = useState('')
const [firstName, setFirstName] = useState('')
const [lastName, setLastName] = useState('')
@@ -40,6 +40,23 @@ const Register = () => {
const [showDatePicker, setShowDatePicker] = useState(false)
const [, setTooltip] = useGlobal('_tooltip')
const handleBack = useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
return
}
navigation.reset({
index: 0,
routes: [
{
name: Routes.LandingPage,
params: { startAtLanguage: true },
},
],
})
}, [navigation])
const afterSocialAuth = useCallback(async (authResult) => {
const uid = firebase.auth().currentUser?.uid
if (!uid) throw new Error('Aucun utilisateur après connexion')
@@ -225,6 +242,7 @@ const Register = () => {
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
headerType="NAVIGATION"
title="Inscription"
onBackPressed={handleBack}
>
<View style={styles.pageContent}>
<ItemContainer
+1 -9
View File
@@ -36,15 +36,7 @@ export default ({ navigation }) => {
} finally {
navigation.reset({
index: 0,
routes: [
{
name: Routes.BottomTab,
params: {
screen: Routes.HomeStack,
params: { screen: Routes.Home },
},
},
],
routes: [{ name: Routes.LandingPage }],
})
setIsFullyLoaded(true)
}
-5
View File
@@ -307,7 +307,6 @@ const ComposeSong = () => {
) : null
}
coinBadgeContainerStyle={styles.rightSideCredits}
containerStyle={styles.videoQuestionPage}
headerType="NONE"
>
<MusicLandHeader
@@ -502,10 +501,6 @@ const ComposeSong = () => {
export default ComposeSong
const styles = StyleSheet.create({
videoQuestionPage: {
alignSelf: 'flex-start',
marginLeft: '4%',
},
rightSideControl: {
right: 24,
left: 'auto',
+3 -16
View File
@@ -1,8 +1,7 @@
import React, { useMemo, useState } from 'react'
import { Platform, StyleSheet, View } from 'react-native'
import { Platform, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../assets'
import BackgroundVideo from '../../components/BackgroundVideo'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from '../../data/data'
@@ -25,7 +24,7 @@ const MAX_STEP_INDEX = 8
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false }
const CreateLyricsWithAi = () => {
const { selectedProject, videos } = useUser()
const { selectedProject } = useUser()
const [selectedIndex, setSelectedIndex] = useState(0)
const handleLyricsError = React.useCallback(() => {
setSelectedIndex(7)
@@ -386,18 +385,13 @@ const CreateLyricsWithAi = () => {
]
const currentStep = steps[selectedIndex]
const isQuestionStep = selectedIndex < MAX_STEP_INDEX
return (
<Page
headerType="NONE"
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 64 : 100}
backgroundImg={isQuestionStep ? null : background.libraryBgWeb}
backgroundContent={
isQuestionStep ? <BackgroundVideo source={videos?.celineWeb} /> : null
}
containerStyle={isQuestionStep ? styles.videoQuestionPage : undefined}
backgroundImg={background.libraryBgWeb}
>
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
<View
@@ -435,10 +429,3 @@ const CreateLyricsWithAi = () => {
}
export default CreateLyricsWithAi
const styles = StyleSheet.create({
videoQuestionPage: {
alignSelf: 'flex-end',
marginRight: '4%',
},
})
+3 -2
View File
@@ -5,7 +5,7 @@ import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import alert from '../../components/Alert'
import GradientButton from '../../components/GradientButton'
import ProgressBar from '../../components/ProgressBar'
import firebase from '../../config/firebase'
import { getFunctionsClient } from '../../config/firebase'
import { Routes } from '../../navigation'
import { navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
@@ -20,6 +20,7 @@ import {
const FAKE_PROGRESS_MAX = 96
const PROGRESS_INTERVAL_MS = 250
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false }
const FUNCTIONS_REGION = 'europe-west1'
const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
const [progress, setProgress] = useState(0)
@@ -73,7 +74,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
platform: Platform.OS,
})
const sanitizedStructure = sanitizeStructureList(config?.structure, CUSTOM_SANITIZE_OPTIONS)
const callable = firebase.functions().httpsCallable('lyrics-generateLyrics')
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable('lyrics-generateLyrics')
const { data } = await callable({
lang: projectLang,
objective: config?.objective,
+5 -2
View File
@@ -8,7 +8,7 @@ import GradientButton from '../../components/GradientButton'
import ItemContainer from '../../components/ItemContainer/ItemContainer'
import MusicLandHeader from '../../components/MusicLandHeader'
import Overlay from '../../components/Overlay'
import firebase, { projectsRef } from '../../config/firebase'
import firebase, { getFunctionsClient, projectsRef } from '../../config/firebase'
import { strings } from '../../constants/strings'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
@@ -31,6 +31,7 @@ import CustomInput from './components/CustomInput'
const RESPONSIBILITY_CHECKBOX_LABEL =
'Vous êtes responsable du contenu que vous validez et Musicland ne sera en aucun cas tenu responsable du contenu que vous validez.'
const FUNCTIONS_REGION = 'europe-west1'
const Lyrics = ({ navigation }) => {
const scrollRef = useRef(null)
@@ -239,7 +240,9 @@ const Lyrics = ({ navigation }) => {
// 1) Appel de la Cloud Function de modération avant tout enregistrement
try {
const callable = firebase.functions().httpsCallable('lyrics-analyseLyricsToxicity')
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
'lyrics-analyseLyricsToxicity'
)
const { data } = await callable({
title: titleTrimmed,
lyrics: normalizedNewLyrics,