[APP-1801] New splash screen (#9780)
* MVP splash * MVP of login screen * Hack a footer into the logged out view * revert unneeded (cherry picked from commit 296420aa0df433b101389c44b05a40696e7389ae) * Update config (cherry picked from commit eac421966dfdee99d4c818ac07835a2a97e7590f) * Bump size slightly
@@ -310,26 +310,24 @@ module.exports = function (_config) {
|
|||||||
[
|
[
|
||||||
'expo-splash-screen',
|
'expo-splash-screen',
|
||||||
{
|
{
|
||||||
ios: {
|
enableFullScreenImage_legacy: true, // iOS only
|
||||||
enableFullScreenImage_legacy: true,
|
backgroundColor: '#EEF5FF', // based on illustration
|
||||||
backgroundColor: '#ffffff',
|
image: './assets/splash/splash-mobile.png',
|
||||||
image: './assets/splash.png',
|
resizeMode: 'cover',
|
||||||
|
dark: {
|
||||||
|
enableFullScreenImage_legacy: true, // iOS only
|
||||||
|
backgroundColor: '#446DA9', // based on illustration
|
||||||
|
image: './assets/splash/splash-mobile-dark.png',
|
||||||
resizeMode: 'cover',
|
resizeMode: 'cover',
|
||||||
dark: {
|
|
||||||
enableFullScreenImage_legacy: true,
|
|
||||||
backgroundColor: '#001429',
|
|
||||||
image: './assets/splash-dark.png',
|
|
||||||
resizeMode: 'cover',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
backgroundColor: '#0c7cff',
|
backgroundColor: '#A8CCFF', // primary_200
|
||||||
image: './assets/splash-android-icon.png',
|
image: './assets/splash/android-splash-logo-white.png',
|
||||||
imageWidth: 150,
|
imageWidth: 105,
|
||||||
dark: {
|
dark: {
|
||||||
backgroundColor: '#0c2a49',
|
backgroundColor: '#00398A', // primary_800
|
||||||
image: './assets/splash-android-icon-dark.png',
|
image: './assets/splash/android-splash-logo-white.png',
|
||||||
imageWidth: 150,
|
imageWidth: 105,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 606 KiB |
|
Before Width: | Height: | Size: 563 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
@@ -9,55 +9,30 @@ import {
|
|||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
interpolate,
|
interpolate,
|
||||||
runOnJS,
|
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
withTiming,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
||||||
import Svg, {Path, type SvgProps} from 'react-native-svg'
|
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import * as SplashScreen from 'expo-splash-screen'
|
import * as SplashScreen from 'expo-splash-screen'
|
||||||
|
|
||||||
import {Logotype} from '#/view/icons/Logotype'
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import splashImagePointer from '../assets/splash.png'
|
import splashImagePointer from '../assets/splash/splash-mobile.png'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import darkSplashImagePointer from '../assets/splash-dark.png'
|
import darkSplashImagePointer from '../assets/splash/splash-mobile-dark.png'
|
||||||
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
||||||
const darkSplashImageUri = RNImage.resolveAssetSource(
|
const darkSplashImageUri = RNImage.resolveAssetSource(
|
||||||
darkSplashImagePointer,
|
darkSplashImagePointer,
|
||||||
).uri
|
).uri
|
||||||
|
|
||||||
export const Logo = React.forwardRef(function LogoImpl(props: SvgProps, ref) {
|
|
||||||
const width = 1000
|
|
||||||
const height = width * (67 / 64)
|
|
||||||
return (
|
|
||||||
<Svg
|
|
||||||
fill="none"
|
|
||||||
// @ts-ignore it's fiiiiine
|
|
||||||
ref={ref}
|
|
||||||
viewBox="0 0 64 66"
|
|
||||||
style={[{width, height}, props.style]}>
|
|
||||||
<Path
|
|
||||||
fill={props.fill || '#fff'}
|
|
||||||
d="M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z"
|
|
||||||
/>
|
|
||||||
</Svg>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isReady: boolean
|
isReady: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Splash(props: React.PropsWithChildren<Props>) {
|
export function Splash(props: React.PropsWithChildren<Props>) {
|
||||||
'use no memo'
|
'use no memo'
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
const intro = useSharedValue(0)
|
|
||||||
const outroLogo = useSharedValue(0)
|
|
||||||
const outroApp = useSharedValue(0)
|
|
||||||
const outroAppOpacity = useSharedValue(0)
|
const outroAppOpacity = useSharedValue(0)
|
||||||
|
const colorScheme = useColorScheme()
|
||||||
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
|
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
|
||||||
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
|
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
|
||||||
const [isLayoutReady, setIsLayoutReady] = React.useState(false)
|
const [isLayoutReady, setIsLayoutReady] = React.useState(false)
|
||||||
@@ -69,62 +44,10 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
isImageLoaded &&
|
isImageLoaded &&
|
||||||
isLayoutReady &&
|
isLayoutReady &&
|
||||||
reduceMotion !== undefined
|
reduceMotion !== undefined
|
||||||
|
|
||||||
const colorScheme = useColorScheme()
|
|
||||||
const isDarkMode = colorScheme === 'dark'
|
const isDarkMode = colorScheme === 'dark'
|
||||||
|
|
||||||
const logoAnimation = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scale: interpolate(
|
|
||||||
outroLogo.get(),
|
|
||||||
[0, 0.08, 1],
|
|
||||||
[1, 0.8, 500],
|
|
||||||
'clamp',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const bottomLogoAnimation = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const reducedLogoAnimation = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
scale: interpolate(intro.get(), [0, 1], [0.8, 1], 'clamp'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
opacity: interpolate(intro.get(), [0, 1], [0, 1], 'clamp'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const logoWrapperAnimation = useAnimatedStyle(() => {
|
|
||||||
return {
|
|
||||||
opacity: interpolate(
|
|
||||||
outroAppOpacity.get(),
|
|
||||||
[0, 0.1, 0.2, 1],
|
|
||||||
[1, 1, 0, 0],
|
|
||||||
'clamp',
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const appAnimation = useAnimatedStyle(() => {
|
const appAnimation = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
transform: [
|
|
||||||
{
|
|
||||||
scale: interpolate(outroApp.get(), [0, 1], [1.1, 1], 'clamp'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
opacity: interpolate(
|
opacity: interpolate(
|
||||||
outroAppOpacity.get(),
|
outroAppOpacity.get(),
|
||||||
[0, 0.1, 0.2, 1],
|
[0, 0.1, 0.2, 1],
|
||||||
@@ -142,50 +65,21 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
if (isReady) {
|
if (isReady) {
|
||||||
SplashScreen.hideAsync()
|
SplashScreen.hideAsync()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
intro.set(() =>
|
outroAppOpacity.set(() =>
|
||||||
withTiming(
|
withTiming(1, {
|
||||||
1,
|
duration: 1200,
|
||||||
{duration: 400, easing: Easing.out(Easing.cubic)},
|
easing: Easing.in(Easing.cubic),
|
||||||
async () => {
|
}),
|
||||||
// set these values to check animation at specific point
|
|
||||||
outroLogo.set(() =>
|
|
||||||
withTiming(
|
|
||||||
1,
|
|
||||||
{duration: 1200, easing: Easing.in(Easing.cubic)},
|
|
||||||
() => {
|
|
||||||
runOnJS(onFinish)()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
outroApp.set(() =>
|
|
||||||
withTiming(1, {
|
|
||||||
duration: 1200,
|
|
||||||
easing: Easing.inOut(Easing.cubic),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
outroAppOpacity.set(() =>
|
|
||||||
withTiming(1, {
|
|
||||||
duration: 1200,
|
|
||||||
easing: Easing.in(Easing.cubic),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}
|
}
|
||||||
}, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
|
}, [onFinish, outroAppOpacity, isReady])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
|
AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const logoAnimations =
|
|
||||||
reduceMotion === true ? reducedLogoAnimation : logoAnimation
|
|
||||||
// special off-spec color for dark mode
|
|
||||||
const logoBg = isDarkMode ? '#0F1824' : '#fff'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{flex: 1}} onLayout={onLayout}>
|
<View style={{flex: 1}} onLayout={onLayout}>
|
||||||
{!isAnimationComplete && (
|
{!isAnimationComplete && (
|
||||||
@@ -196,22 +90,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
|
source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
|
||||||
style={StyleSheet.absoluteFillObject}
|
style={StyleSheet.absoluteFillObject}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
bottomLogoAnimation,
|
|
||||||
{
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: insets.bottom + 40,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
opacity: 0,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Logotype fill="#fff" width={90} />
|
|
||||||
</Animated.View>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -220,24 +98,6 @@ export function Splash(props: React.PropsWithChildren<Props>) {
|
|||||||
<Animated.View style={[{flex: 1}, appAnimation]}>
|
<Animated.View style={[{flex: 1}, appAnimation]}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
{!isAnimationComplete && (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
StyleSheet.absoluteFillObject,
|
|
||||||
logoWrapperAnimation,
|
|
||||||
{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
transform: [{translateY: -(insets.top / 2)}, {scale: 0.1}], // scale from 1000px to 100px
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Animated.View style={[logoAnimations]}>
|
|
||||||
<Logo fill={logoBg} />
|
|
||||||
</Animated.View>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import {View} from 'react-native'
|
import {useMemo} from 'react'
|
||||||
|
import {Image as RNImage, View} from 'react-native'
|
||||||
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
import Animated, {FadeIn, FadeOut} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {Image} from 'expo-image'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
|
||||||
import {CenteredView} from '#/view/com/util/Views'
|
|
||||||
import {Logo} from '#/view/icons/Logo'
|
import {Logo} from '#/view/icons/Logo'
|
||||||
import {Logotype} from '#/view/icons/Logotype'
|
import {Logotype} from '#/view/icons/Logotype'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
|
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {Text} from '#/components/Typography'
|
// @ts-ignore
|
||||||
|
import splashImagePointer from '../../../../assets/splash/splash-mobile.png'
|
||||||
|
// @ts-ignore
|
||||||
|
import darkSplashImagePointer from '../../../../assets/splash/splash-mobile-dark.png'
|
||||||
|
const splashImageUri = RNImage.resolveAssetSource(splashImagePointer).uri
|
||||||
|
const darkSplashImageUri = RNImage.resolveAssetSource(
|
||||||
|
darkSplashImagePointer,
|
||||||
|
).uri
|
||||||
|
|
||||||
export const SplashScreen = ({
|
export const SplashScreen = ({
|
||||||
onPressSignin,
|
onPressSignin,
|
||||||
@@ -23,38 +28,68 @@ export const SplashScreen = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const isDarkMode = t.name !== 'light'
|
||||||
|
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
|
const styles = useMemo(() => {
|
||||||
|
const logoFill = isDarkMode ? 'white' : t.palette.primary_500
|
||||||
|
return {
|
||||||
|
logoFill,
|
||||||
|
logoShadow: isDarkMode
|
||||||
|
? [
|
||||||
|
t.atoms.shadow_md,
|
||||||
|
{
|
||||||
|
shadowColor: logoFill,
|
||||||
|
shadowOpacity: 0.5,
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
}, [t, isDarkMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CenteredView style={[a.h_full, a.flex_1]}>
|
<>
|
||||||
|
<Image
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
source={{uri: isDarkMode ? darkSplashImageUri : splashImageUri}}
|
||||||
|
style={[a.absolute, a.inset_0]}
|
||||||
|
/>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.duration(90)}
|
entering={FadeIn.duration(90)}
|
||||||
exiting={FadeOut.duration(90)}
|
exiting={FadeOut.duration(90)}
|
||||||
style={[a.flex_1]}>
|
style={[a.flex_1]}>
|
||||||
<ErrorBoundary>
|
<View
|
||||||
<View style={[a.flex_1, a.justify_center, a.align_center]}>
|
style={[a.justify_center, a.align_center, {gap: 6, paddingTop: 46}]}>
|
||||||
<Logo width={92} fill="sky" />
|
<Logo width={76} fill={styles.logoFill} style={styles.logoShadow} />
|
||||||
|
<Logotype
|
||||||
|
width={91}
|
||||||
|
fill={styles.logoFill}
|
||||||
|
style={styles.logoShadow}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={[a.pb_sm, a.pt_5xl]}>
|
<View style={[a.flex_1]} />
|
||||||
<Logotype width={161} fill={t.atoms.text.color} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_md,
|
|
||||||
a.font_semi_bold,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
a.text_center,
|
|
||||||
]}>
|
|
||||||
<Trans>What's up?</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
<View
|
||||||
|
testID="signinOrCreateAccount"
|
||||||
|
style={[a.px_xl, a.gap_md, a.pb_sm]}>
|
||||||
<View
|
<View
|
||||||
testID="signinOrCreateAccount"
|
style={[
|
||||||
style={[a.px_xl, a.gap_md, a.pb_2xl]}>
|
t.atoms.shadow_md,
|
||||||
|
{
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}>
|
||||||
<Button
|
<Button
|
||||||
testID="createAccountButton"
|
testID="createAccountButton"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
@@ -66,45 +101,30 @@ export const SplashScreen = ({
|
|||||||
msg`Opens flow to create a new Bluesky account`,
|
msg`Opens flow to create a new Bluesky account`,
|
||||||
)}
|
)}
|
||||||
size="large"
|
size="large"
|
||||||
variant="solid"
|
color={isDarkMode ? 'secondary_inverted' : 'secondary'}>
|
||||||
color="primary">
|
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Create account</Trans>
|
<Trans>Create account</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
testID="signInButton"
|
|
||||||
onPress={() => {
|
|
||||||
onPressSignin()
|
|
||||||
playHaptic('Light')
|
|
||||||
}}
|
|
||||||
label={_(msg`Sign in`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Opens flow to sign in to your existing Bluesky account`,
|
|
||||||
)}
|
|
||||||
size="large"
|
|
||||||
variant="solid"
|
|
||||||
color="secondary">
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Sign in</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
</View>
|
</View>
|
||||||
<View
|
|
||||||
style={[
|
<Button
|
||||||
a.px_lg,
|
testID="signInButton"
|
||||||
a.pt_md,
|
onPress={() => {
|
||||||
a.pb_2xl,
|
onPressSignin()
|
||||||
a.justify_center,
|
playHaptic('Light')
|
||||||
a.align_center,
|
}}
|
||||||
]}>
|
label={_(msg`Sign in`)}
|
||||||
<View>
|
accessibilityHint={_(
|
||||||
<AppLanguageDropdown />
|
msg`Opens flow to sign in to your existing Bluesky account`,
|
||||||
</View>
|
)}
|
||||||
</View>
|
size="large">
|
||||||
<View style={{height: insets.bottom}} />
|
<ButtonText style={{color: 'white'}}>
|
||||||
</ErrorBoundary>
|
<Trans>Sign in</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</CenteredView>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||