[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
This commit is contained in:
Eric Bailey
2026-01-28 20:05:14 -06:00
committed by GitHub
parent d13df6c7e6
commit 0ac5b07bfd
10 changed files with 104 additions and 226 deletions
+14 -16
View File
@@ -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,
}, },
}, },
}, },
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 606 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

+9 -149
View File
@@ -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>
+81 -61
View File
@@ -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> </>
) )
} }