Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 37 additions & 18 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';

import analytics from '@react-native-firebase/analytics';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { View, useColorScheme } from 'react-native';
import { View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MenuProvider } from 'react-native-popup-menu';
import { configureReanimatedLogger, ReanimatedLogLevel } from 'react-native-reanimated';
Expand All @@ -21,6 +21,7 @@ import { GameSheetContextProvider } from './src/components/Sheets/GameSheetConte
import { PointValuesSheetContextProvider } from './src/components/Sheets/PointValuesSheetContext';
import { SplashOverlay } from './src/components/SplashOverlay';
import { Navigation } from './src/Navigation';
import { useTheme } from './src/theme';

// Keep the native splash screen up until our animated SplashOverlay mounts and
// calls hideAsync(). Without this, Expo auto-hides the native splash on first JS
Expand All @@ -31,32 +32,50 @@ if (process.env.EXPO_PUBLIC_FIREBASE_ANALYTICS == 'false') {
analytics().setAnalyticsCollectionEnabled(false);
}

export default function App() {
const colorScheme = useColorScheme();
const bgColor = colorScheme === 'dark' ? '#000000' : '#F2F2F7';
const AppContent = () => {
const theme = useTheme();
const [appReady, setAppReady] = useState(false);
const [splashDone, setSplashDone] = useState(false);
const handleBeforeLift = useCallback(() => setAppReady(true), []);
const handleSplashDone = useCallback(() => setSplashDone(true), []);

return (
<View style={{ flex: 1, backgroundColor: bgColor }}>
<View style={{ flex: 1, backgroundColor: theme.background }}>
<SafeAreaProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<Provider store={store}>
<GameSheetContextProvider>
<MenuProvider>
<PointValuesSheetContextProvider>
<ChooseWinnersSheetContextProvider>
<PersistGate loading={null} persistor={persistor}>
<GameSheetContextProvider>
<MenuProvider>
<PointValuesSheetContextProvider>
<ChooseWinnersSheetContextProvider>
<PersistGate
loading={null}
onBeforeLift={handleBeforeLift}
persistor={persistor}
>
<StatusBar />
<Navigation />
</PersistGate>
</ChooseWinnersSheetContextProvider>
</PointValuesSheetContextProvider>
</MenuProvider>
</GameSheetContextProvider>
</Provider>
</ChooseWinnersSheetContextProvider>
</PointValuesSheetContextProvider>
</MenuProvider>
</GameSheetContextProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
{!splashDone && <SplashOverlay onDone={() => setSplashDone(true)} />}
{!splashDone && (
<SplashOverlay
backgroundColor={theme.background}
onDone={handleSplashDone}
ready={appReady}
/>
)}
</View>
);
};

export default function App() {
return (
<Provider store={store}>
<AppContent />
</Provider>
);
};
78 changes: 78 additions & 0 deletions src/components/SplashOverlay.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';

import { act, render } from '@testing-library/react-native';
import * as SplashScreen from 'expo-splash-screen';
import { StyleSheet } from 'react-native';
import { withDelay, withTiming } from 'react-native-reanimated';

import { SplashOverlay } from './SplashOverlay';

jest.mock('expo-image', () => ({
Image: () => null,
}));

jest.mock('expo-splash-screen', () => ({
hideAsync: jest.fn(() => Promise.resolve()),
}));

jest.mock('react-native-reanimated', () => {
const { View } = jest.requireActual('react-native');

return {
__esModule: true,
default: { View },
Easing: {
cubic: 'cubic',
in: jest.fn(value => value),
out: jest.fn(value => value),
quad: 'quad',
},
runOnJS: jest.fn(callback => callback),
useAnimatedStyle: jest.fn(callback => callback()),
useSharedValue: jest.fn(value => ({ value })),
withDelay: jest.fn((_delay, animation) => animation),
withSequence: jest.fn((...animations) => animations.at(-1)),
withTiming: jest.fn((value, _config, callback) => {
callback?.(true);
return value;
}),
};
});

describe('SplashOverlay', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('keeps the overlay visible until persisted state is ready', () => {
const onDone = jest.fn();
const view = render(
<SplashOverlay backgroundColor="#000000" onDone={onDone} ready={false} />
);

expect(SplashScreen.hideAsync).toHaveBeenCalledTimes(1);
expect(withDelay).not.toHaveBeenCalled();
expect(withTiming).not.toHaveBeenCalled();
expect(onDone).not.toHaveBeenCalled();

act(() => {
view.rerender(
<SplashOverlay backgroundColor="#000000" onDone={onDone} ready />
);
});

expect(withDelay).toHaveBeenCalled();
expect(withTiming).toHaveBeenCalled();
expect(onDone).toHaveBeenCalledTimes(1);
});

it('uses the persisted app theme color', () => {
const { getByTestId } = render(
<SplashOverlay backgroundColor="#123456" onDone={jest.fn()} ready={false} />
);

expect(StyleSheet.flatten(getByTestId('splash-overlay').props.style)).toMatchObject({
backgroundColor: '#123456',
});
});
});
20 changes: 12 additions & 8 deletions src/components/SplashOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';

import { Image } from 'expo-image';
import * as SplashScreen from 'expo-splash-screen';
import { StyleSheet, useColorScheme } from 'react-native';
import { StyleSheet } from 'react-native';
import Animated, {
Easing,
runOnJS,
Expand All @@ -16,21 +16,24 @@ import Animated, {
import icon from '../../assets/icon.png';

interface Props {
backgroundColor: string;
onDone: () => void;
ready: boolean;
}

export const SplashOverlay: React.FC<Props> = ({ onDone }) => {
const colorScheme = useColorScheme();
const bgColor = colorScheme === 'dark' ? '#000000' : '#F2F2F7';

export const SplashOverlay: React.FC<Props> = ({ backgroundColor, onDone, ready }) => {
const overlayOpacity = useSharedValue(1);
const iconOpacity = useSharedValue(0);
const iconScale = useSharedValue(0.92);

const runDone = useCallback(() => onDone(), [onDone]);

useEffect(() => {
SplashScreen.hideAsync();
void SplashScreen.hideAsync().catch(() => undefined);
}, []);

useEffect(() => {
if (!ready) return;

iconOpacity.value = withTiming(1, { duration: 350 });

Expand All @@ -44,7 +47,7 @@ export const SplashOverlay: React.FC<Props> = ({ onDone }) => {
withTiming(1, { duration: 450, easing: Easing.out(Easing.cubic) }),
withDelay(250, withTiming(1.04, { duration: 350, easing: Easing.in(Easing.quad) }))
);
}, []);
}, [ready]);

const overlayStyle = useAnimatedStyle(() => ({
opacity: overlayOpacity.value,
Expand All @@ -57,8 +60,9 @@ export const SplashOverlay: React.FC<Props> = ({ onDone }) => {

return (
<Animated.View
style={[styles.container, { backgroundColor: bgColor }, overlayStyle]}
style={[styles.container, { backgroundColor }, overlayStyle]}
pointerEvents="none"
testID="splash-overlay"
>
<Animated.View style={iconStyle}>
<Image
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"extends": "expo/tsconfig.base",
"include": [
"./App.tsx",
"./src/**/*.tsx",
"./src/**/*.ts",
"./src/**/*.js",
Expand Down
Loading