feat: update onboarding and application flows
This commit is contained in:
+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
|
||||
|
||||
Reference in New Issue
Block a user