feat: update onboarding and application flows
This commit is contained in:
@@ -250,6 +250,9 @@ export const videos = {
|
||||
test:
|
||||
Platform.OS === 'web' ? require('./video/testVideoWeb.mp4') : require('./video/testVideo.mp4'),
|
||||
club: require('./video/club.mp4'),
|
||||
entryJingle: require('./video/entry-jingle.mp4'),
|
||||
welcomeFr: require('./video/welcome-fr.mp4'),
|
||||
welcomeEn: require('./video/welcome-en.mp4'),
|
||||
}
|
||||
|
||||
export const img = {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15
-8
@@ -7,7 +7,7 @@ import { Fonts, Palette, gutters } from '../styles'
|
||||
import GradientButton from './GradientButton'
|
||||
import { LinearGradient } from './LinearGradient/LinearGradient'
|
||||
import Overlay from './Overlay'
|
||||
const WebAlertModal = ({ title, description, options }) => {
|
||||
const WebAlertModal = ({ title, description, options, onDismiss }) => {
|
||||
const [visible, setVisible] = useState(true)
|
||||
|
||||
const confirmOption = options?.find(({ style }) => style !== 'cancel')
|
||||
@@ -15,14 +15,18 @@ const WebAlertModal = ({ title, description, options }) => {
|
||||
const hasSecondaryAction = Boolean(cancelOption)
|
||||
const buttonContainerStyle = hasSecondaryAction ? styles.actionButton : styles.singleActionButton
|
||||
|
||||
const onConfirm = () => {
|
||||
const dismiss = (callback) => {
|
||||
setVisible(false)
|
||||
confirmOption?.onPress()
|
||||
setTimeout(onDismiss, 500)
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
dismiss(confirmOption?.onPress)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setVisible(false)
|
||||
cancelOption?.onPress()
|
||||
dismiss(cancelOption?.onPress)
|
||||
}
|
||||
|
||||
const renderDescription = () => {
|
||||
@@ -80,8 +84,12 @@ const alertPolyfill = (title, description, options, extra) => {
|
||||
const rootDiv = document.createElement('div')
|
||||
document.body.appendChild(rootDiv)
|
||||
|
||||
const { createRoot } = require('react-dom/client')
|
||||
const root = createRoot(rootDiv)
|
||||
|
||||
const closeModal = () => {
|
||||
document.body.removeChild(rootDiv)
|
||||
root.unmount()
|
||||
rootDiv.remove()
|
||||
}
|
||||
|
||||
const WebAlertComponent = () => (
|
||||
@@ -93,8 +101,7 @@ const alertPolyfill = (title, description, options, extra) => {
|
||||
/>
|
||||
)
|
||||
|
||||
// Render the React component into the div
|
||||
require('react-dom').render(<WebAlertComponent />, rootDiv)
|
||||
root.render(<WebAlertComponent />)
|
||||
}
|
||||
|
||||
const customAlert = (title, description, options, extra) => {
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { MaterialIcons } from '@expo/vector-icons'
|
||||
import { Asset } from 'expo-asset'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
const resolveModuleUri = async (source) => {
|
||||
const asset = Asset.fromModule(source)
|
||||
|
||||
if (!asset.localUri && !asset.uri) {
|
||||
await asset.downloadAsync()
|
||||
}
|
||||
|
||||
return asset.localUri ?? asset.uri ?? null
|
||||
}
|
||||
import { resolveMediaUri } from '../utils/resolveMediaUri'
|
||||
|
||||
const BackgroundVideo = ({ source, soundButtonStyle = null }) => {
|
||||
const videoRef = useRef(null)
|
||||
@@ -28,16 +19,9 @@ const BackgroundVideo = ({ source, soundButtonStyle = null }) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof source === 'string') {
|
||||
setUri(source)
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadSource = async () => {
|
||||
try {
|
||||
const nextUri = await resolveModuleUri(source)
|
||||
const nextUri = await resolveMediaUri(source)
|
||||
if (isMounted) {
|
||||
setUri(nextUri)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,13 @@ import { Portal } from '@gorhom/portal'
|
||||
// - onClose: () => void, called when user skips or when video ends
|
||||
const CLOSE_THRESHOLD_SECONDS = 0.35
|
||||
|
||||
const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
const FullscreenIntroVideo = ({
|
||||
url,
|
||||
visible = true,
|
||||
onClose,
|
||||
contentFit = 'contain',
|
||||
skipLabel = 'Passer la vidéo',
|
||||
}) => {
|
||||
const source = url ? (typeof url === 'string' ? { uri: url } : url) : videos.test
|
||||
|
||||
const hasClosedRef = useRef(false)
|
||||
@@ -103,7 +109,7 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
<VideoView
|
||||
player={player}
|
||||
nativeControls={false}
|
||||
contentFit="contain"
|
||||
contentFit={contentFit}
|
||||
height="100%"
|
||||
width="100%"
|
||||
// style={{
|
||||
@@ -128,7 +134,7 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
borderColor: '#FFFFFF55',
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: '#FFF', fontSize: 14 }}>Passer la vidéo</Text>
|
||||
<Text style={{ color: '#FFF', fontSize: 14 }}>{skipLabel}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Portal>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Portal } from '@gorhom/portal'
|
||||
import { Asset } from 'expo-asset'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
|
||||
import { resolveMediaUri } from '../utils/resolveMediaUri'
|
||||
|
||||
const CLOSE_THRESHOLD_SECONDS = 0.35
|
||||
const CLOSE_POLL_INTERVAL_MS = 500
|
||||
|
||||
@@ -45,20 +46,30 @@ const closeTextStyle = {
|
||||
fontSize: 14,
|
||||
}
|
||||
|
||||
const resolveModuleUri = async (module) => {
|
||||
const asset = Asset.fromModule(module)
|
||||
|
||||
if (!asset.localUri && !asset.uri) {
|
||||
await asset.downloadAsync()
|
||||
}
|
||||
|
||||
return asset.localUri ?? asset.uri ?? null
|
||||
const playButtonStyle = {
|
||||
backgroundColor: '#000000CC',
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 24,
|
||||
borderWidth: 1,
|
||||
borderColor: '#FFFFFF88',
|
||||
cursor: 'pointer',
|
||||
zIndex: 1,
|
||||
}
|
||||
|
||||
const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
const FullscreenIntroVideo = ({
|
||||
url,
|
||||
visible = true,
|
||||
onClose,
|
||||
contentFit = 'cover',
|
||||
requireAudio = false,
|
||||
skipLabel = 'Passer la vidéo',
|
||||
playLabel = 'Lire la vidéo avec le son',
|
||||
}) => {
|
||||
const videoRef = useRef(null)
|
||||
const [uri, setUri] = useState(null)
|
||||
const [muted, setMuted] = useState(false)
|
||||
const [awaitingInteraction, setAwaitingInteraction] = useState(false)
|
||||
const hasClosedRef = useRef(false)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
@@ -92,20 +103,14 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
const assignUri = (nextUri) => {
|
||||
if (isMounted) {
|
||||
setMuted(false)
|
||||
setAwaitingInteraction(false)
|
||||
setUri(nextUri)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof url === 'string') {
|
||||
assignUri(url)
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const nextUri = await resolveModuleUri(url)
|
||||
const nextUri = await resolveMediaUri(url)
|
||||
assignUri(nextUri)
|
||||
} catch {
|
||||
if (isMounted) {
|
||||
@@ -142,7 +147,9 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
|
||||
if (result?.catch) {
|
||||
result.catch((error) => {
|
||||
if (error?.name === 'NotAllowedError' && !muted) {
|
||||
if (error?.name === 'NotAllowedError' && requireAudio) {
|
||||
setAwaitingInteraction(true)
|
||||
} else if (error?.name === 'NotAllowedError' && !muted) {
|
||||
setMuted(true)
|
||||
}
|
||||
})
|
||||
@@ -160,7 +167,20 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
const video = videoRef.current
|
||||
video?.pause()
|
||||
}
|
||||
}, [evaluateShouldClose, muted, uri, visible])
|
||||
}, [evaluateShouldClose, muted, requireAudio, uri, visible])
|
||||
|
||||
const handleManualPlay = useCallback(() => {
|
||||
const video = videoRef.current
|
||||
if (!video) return
|
||||
|
||||
setMuted(false)
|
||||
video.muted = false
|
||||
video.currentTime = 0
|
||||
const result = video.play()
|
||||
if (result?.then) {
|
||||
result.then(() => setAwaitingInteraction(false)).catch(() => {})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current
|
||||
@@ -187,7 +207,7 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={uri}
|
||||
style={videoStyle}
|
||||
style={{ ...videoStyle, objectFit: contentFit }}
|
||||
playsInline
|
||||
autoPlay
|
||||
loop={false}
|
||||
@@ -199,8 +219,13 @@ const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
|
||||
onError={handleClose}
|
||||
/>
|
||||
) : null}
|
||||
{awaitingInteraction ? (
|
||||
<Pressable onPress={handleManualPlay} style={playButtonStyle}>
|
||||
<Text style={closeTextStyle}>{playLabel}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
<Pressable onPress={handleClose} style={closeButtonStyle}>
|
||||
<Text style={closeTextStyle}>Passer la vidéo</Text>
|
||||
<Text style={closeTextStyle}>{skipLabel}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Portal>
|
||||
|
||||
@@ -22,6 +22,8 @@ export default ({
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: gutters / 2,
|
||||
paddingTop: 10,
|
||||
position: 'relative',
|
||||
zIndex: 10,
|
||||
...containerStyle,
|
||||
},
|
||||
]}
|
||||
@@ -29,7 +31,23 @@ export default ({
|
||||
<View style={{ flex: 1 }}>
|
||||
{!hideBackButton && (
|
||||
<Pressable
|
||||
onPress={() => onBackPressed?.() || goBack()}
|
||||
onPress={() => {
|
||||
if (onBackPressed) {
|
||||
onBackPressed()
|
||||
return
|
||||
}
|
||||
goBack()
|
||||
}}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Retour"
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
alignSelf: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
}}
|
||||
hitSlop={{ top: 16, right: 16, bottom: 16, left: 16 }}
|
||||
>
|
||||
<Image
|
||||
|
||||
+12
-7
@@ -10,7 +10,7 @@ import { Platform } from 'react-native'
|
||||
const functionsInstances = {}
|
||||
const emulatorConfigured = {}
|
||||
const emulatorHost = Platform.OS === 'web' ? 'localhost' : '192.168.1.60'
|
||||
const USE_FUNCTIONS_EMULATOR = false // Toggle to route functions traffic to the local emulator.
|
||||
const USE_FUNCTIONS_EMULATOR = __DEV__
|
||||
|
||||
const configureFunctionsEmulator = (instance, regionKey = 'us-central1') => {
|
||||
const shouldUseEmulator = USE_FUNCTIONS_EMULATOR && instance?.useEmulator
|
||||
@@ -56,15 +56,20 @@ if (!firebase?.apps?.filter(({ name_ }) => name_ === '[DEFAULT]').length) {
|
||||
console.log('Firebase init')
|
||||
}
|
||||
|
||||
// Firestore settings for React Native/iOS: avoid streaming transport issues
|
||||
const firestore = firebase.firestore()
|
||||
try {
|
||||
// Prefer auto-detect; disable fetch streams for RN iOS stability
|
||||
firebase.firestore().settings({
|
||||
experimentalAutoDetectLongPolling: true,
|
||||
useFetchStreams: false,
|
||||
const firestoreSettings = {
|
||||
ignoreUndefinedProperties: true,
|
||||
})
|
||||
merge: true,
|
||||
}
|
||||
|
||||
// React Native can require long polling, but these settings break web streaming.
|
||||
if (Platform.OS !== 'web') {
|
||||
firestoreSettings.experimentalAutoDetectLongPolling = true
|
||||
firestoreSettings.useFetchStreams = false
|
||||
}
|
||||
|
||||
firestore.settings(firestoreSettings)
|
||||
} catch (e) {
|
||||
// Ignore if settings were already set elsewhere
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { resolveMediaUri } from '../utils/resolveMediaUri'
|
||||
|
||||
const useResolvedMediaUri = (source) => {
|
||||
const [uri, setUri] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
const loadUri = async () => {
|
||||
try {
|
||||
const resolvedUri = await resolveMediaUri(source)
|
||||
if (isMounted) {
|
||||
setUri(resolvedUri)
|
||||
}
|
||||
} catch {
|
||||
if (isMounted) {
|
||||
setUri(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadUri()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [source])
|
||||
|
||||
return uri
|
||||
}
|
||||
|
||||
export default useResolvedMediaUri
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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
@@ -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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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%',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Asset } from 'expo-asset'
|
||||
|
||||
import firebase from '../config/firebase'
|
||||
|
||||
const ABSOLUTE_URI_PATTERN = /^(https?:|blob:|data:)/i
|
||||
|
||||
export const resolveMediaUri = async (source) => {
|
||||
if (!source) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof source === 'string') {
|
||||
const trimmedSource = source.trim()
|
||||
|
||||
if (!trimmedSource) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (ABSOLUTE_URI_PATTERN.test(trimmedSource) || trimmedSource.startsWith('/')) {
|
||||
return trimmedSource
|
||||
}
|
||||
|
||||
return firebase.storage().ref(trimmedSource).getDownloadURL()
|
||||
}
|
||||
|
||||
const asset = Asset.fromModule(source)
|
||||
|
||||
if (!asset.localUri && !asset.uri) {
|
||||
await asset.downloadAsync()
|
||||
}
|
||||
|
||||
return asset.localUri ?? asset.uri ?? null
|
||||
}
|
||||
Reference in New Issue
Block a user