Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert Settings and Onboarding screens to a Modal #420

Merged
merged 4 commits into from
Apr 20, 2024
Merged
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
54 changes: 17 additions & 37 deletions src/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import React from 'react';

import { DarkTheme, NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import * as Application from 'expo-application';
import { parse, SemVer } from 'semver';
import { SemVer } from 'semver';

import { useAppSelector } from '../redux/hooks';
import AppInfoHeader from '../src/components/Headers/AppInfoHeader';
import GameHeader from '../src/components/Headers/GameHeader';
import HomeHeader from '../src/components/Headers/HomeHeader';
Expand All @@ -18,13 +16,12 @@ import SettingsScreen from '../src/screens/SettingsScreen';

import EditPlayerHeader from './components/Headers/EditPlayerHeader';
import ShareHeader from './components/Headers/ShareHeader';
import { getOnboardingSemVer } from './components/Onboarding/Onboarding';
import logger from './Logger';
import EditPlayerScreen from './screens/EditPlayerScreen';
import ShareScreen from './screens/ShareScreen';

export type OnboardingScreenParamList = {
onboarding: boolean;
version: SemVer;
};

export type RootStackParamList = {
Expand Down Expand Up @@ -54,28 +51,9 @@ const MyTheme = {
};

export const Navigation = () => {
const onboardedStr = useAppSelector(state => state.settings.onboarded);
const onboardedSemVer = parse(onboardedStr);
const appVersion = new SemVer(Application.nativeApplicationVersion || '0.0.0');

logger.info(`App Version: ${appVersion}`);
logger.info(`Onboarded Version: ${onboardedSemVer}`);

const onboarded = getOnboardingSemVer(onboardedSemVer) === undefined;

return (
<NavigationContainer theme={MyTheme}>
<Stack.Navigator>
{!onboarded &&
<Stack.Screen name="Onboarding" component={OnboardingScreen}
initialParams={{ onboarding: true }}
options={{
orientation: 'portrait',
title: 'Onboarding',
headerShown: false,
}}
/>
}
<Stack.Navigator initialRouteName='List' >
<Stack.Screen name="List" component={ListScreen}
options={{
orientation: 'portrait',
Expand All @@ -86,15 +64,6 @@ export const Navigation = () => {
},
}}
/>
<Stack.Screen name="AppInfo" component={AppInfoScreen}
options={{
orientation: 'portrait',
title: 'Info',
header: ({ navigation }) => {
return <AppInfoHeader navigation={navigation} />;
},
}}
/>
<Stack.Screen name="Game" component={GameScreen}
options={{
orientation: 'all',
Expand Down Expand Up @@ -132,11 +101,22 @@ export const Navigation = () => {
},
})}
/>
<Stack.Screen name="Tutorial" component={OnboardingScreen}
initialParams={{ onboarding: false }}
<Stack.Screen name="Onboarding" component={OnboardingScreen}
options={{
presentation: 'modal',
orientation: 'portrait',
title: 'Onboarding',
headerShown: false,
}}
/>
<Stack.Screen name="AppInfo" component={AppInfoScreen}
options={{
presentation: 'modal',
orientation: 'portrait',
title: 'Tutorial',
title: 'Info',
header: ({ navigation }) => {
return <AppInfoHeader navigation={navigation} />;
},
}}
/>
</Stack.Navigator>
Expand Down
3 changes: 1 addition & 2 deletions src/components/AppInfo/RotatingIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Image } from 'expo-image';
import { TouchableWithoutFeedback } from 'react-native';
import Animated, {
Easing,
PinwheelIn,
useAnimatedStyle,
useSharedValue,
withTiming
Expand All @@ -28,7 +27,7 @@ const RotatingIcon: React.FunctionComponent = ({ }) => {

await analytics().logEvent('app_icon');
}}>
<Animated.View style={[animatedStyles]} entering={PinwheelIn.delay(0).duration(2000).easing(Easing.elastic(1))}>
<Animated.View style={[animatedStyles]}>
<Image source={require('../../../assets/icon.png')}
contentFit='contain'
style={{
Expand Down
32 changes: 17 additions & 15 deletions src/components/Headers/AppInfoHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';

import { ParamListBase } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { Text, StyleSheet } from 'react-native';
import { Text, View } from 'react-native';

import HomeButton from '../Buttons/HomeButton';
import { systemBlue } from '../../constants';
import HeaderButton from '../Buttons/HeaderButton';

import CustomHeader from './CustomHeader';

interface Props {
navigation: NativeStackNavigationProp<ParamListBase, string, undefined>;
Expand All @@ -15,19 +15,21 @@ interface Props {
const AppInfoHeader: React.FunctionComponent<Props> = ({ navigation }: Props) => {

return (
<CustomHeader navigation={navigation}
headerLeft={<HomeButton navigation={navigation} />}
headerCenter={<Text style={styles.title}>Settings</Text>}
/>
<View style={{
justifyContent: 'flex-end',
alignItems: 'center',
flexDirection: 'row',
backgroundColor: '#F2F2F7',
}}>
<HeaderButton
accessibilityLabel='Save Game'
onPress={async () => {
navigation.goBack();
}}>
<Text style={{ color: systemBlue, fontSize: 20 }}>Done</Text>
</HeaderButton>
</View>
);
};

const styles = StyleSheet.create({
title: {
color: 'white',
fontSize: 20,
fontVariant: ['tabular-nums'],
},
});

export default AppInfoHeader;
2 changes: 1 addition & 1 deletion src/components/Headers/CustomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';

interface Props {
navigation: NativeStackNavigationProp<ParamListBase, string, undefined>;
headerLeft: React.ReactNode;
headerLeft?: React.ReactNode;
headerCenter: React.ReactNode;
headerRight?: React.ReactNode;
animated?: boolean;
Expand Down
10 changes: 5 additions & 5 deletions src/components/Onboarding/Onboarding.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { SemVer } from 'semver';

import { getOnboardingSemVer } from './Onboarding';
import { getPendingOnboardingSemVer } from './Onboarding';

describe('onboarding', () => {
it('should return the default if onboarded to 0.0.0', () => {
const applicableVersion = getOnboardingSemVer(new SemVer('0.0.0'));
const applicableVersion = getPendingOnboardingSemVer(new SemVer('0.0.0'));

expect(applicableVersion).toBe('2.2.2');
});

it('should return the default if null', () => {
const applicableVersion = getOnboardingSemVer(null);
const applicableVersion = getPendingOnboardingSemVer(null);

expect(applicableVersion).toBe('2.2.2');
});

it('should return the latest applicable screens', () => {
const applicableVersion = getOnboardingSemVer(new SemVer('2.4.0'));
const applicableVersion = getPendingOnboardingSemVer(new SemVer('2.4.0'));

expect(applicableVersion).toBe('2.5.0');
});

it('should not return if caught up', () => {
const applicableVersion = getOnboardingSemVer(new SemVer('2.6.0'));
const applicableVersion = getPendingOnboardingSemVer(new SemVer('2.6.0'));

expect(applicableVersion).toBeUndefined();
});
Expand Down
4 changes: 2 additions & 2 deletions src/components/Onboarding/Onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ const finalScreen: OnboardingScreenItem[] = [{
backgroundColor: '#8ca2b8',
}];

export const getOnboardingSemVer = (onboardedSemVer: SemVer | null): string | undefined => {
export const getPendingOnboardingSemVer = (onboardedSemVer: SemVer | null): string | undefined => {
const keys = Object.keys(onboardingScreens)
.sort((a, b) => compare(new SemVer(a), new SemVer(b)));

Expand All @@ -134,7 +134,7 @@ export const getOnboardingSemVer = (onboardedSemVer: SemVer | null): string | un
};

export const getOnboardingScreens = (onboardedSemVer: SemVer): OnboardingScreenItem[] => {
const applicableVersion = getOnboardingSemVer(onboardedSemVer);
const applicableVersion = getPendingOnboardingSemVer(onboardedSemVer);

if (!applicableVersion) {
return finalScreen;
Expand Down
144 changes: 144 additions & 0 deletions src/components/Onboarding/OnboardingPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React from 'react';

import {
ImageURISource,
Animated as RNAnimated,
StyleSheet,
Text,
View
} from 'react-native';
import { Button } from 'react-native-elements';
import Animated from 'react-native-reanimated';
import Video from 'react-native-video';

import { OnboardingScreenItem } from './Onboarding';

interface Props {
active?: boolean;
closeOnboarding: () => void;
index: number;
isLast: boolean;
item: OnboardingScreenItem;
width: number;
}

const OnboardingPage: React.FC<Props> = React.memo(({
active = false,
closeOnboarding,
isLast,
item,
width,
}) => {
let media;
switch (item.media.type) {
case 'image':
media = (
<RNAnimated.Image source={item.media.source as ImageURISource}
style={{
width: item.media.width || '100%',
height: item.media.height || '100%',
borderRadius: item.media.borderRadius || 0,
resizeMode: 'contain',
}} />
);
break;
case 'video':
media = (
<View style={{
backgroundColor: 'black',
padding: 5,
width: item.media.width || '100%',
height: item.media.height || '100%',
borderRadius: 10,
}}>
<Video
source={item.media.source as number}
paused={!active}
repeat={true}
resizeMode='contain'
style={{
width: '100%',
height: '100%',
}}
/>
</View>
);
break;
}

return (
<View style={[styles.itemContainer, { width: width }]} >
<Animated.View style={[styles.titleContainer]}>
<Text style={[styles.title]}>{item.title}</Text>
</Animated.View>

<Animated.View style={[styles.imageContainer]}>
{media}
</Animated.View>

<Animated.View style={[styles.descriptionContainer]}>
<Text style={[styles.description, { color: item.color }]}>
{item.description}
</Text>
<View style={{ alignContent: 'center' }}>
{isLast &&
<Button
title="Get Started"
titleStyle={{ color: 'black' }}
onPress={closeOnboarding}
buttonStyle={[styles.finishButton]}
type='outline'
/>
}
</View>
</Animated.View>
</View>
);
});

export default OnboardingPage;

const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
width: '100%',
},
itemContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'space-around',
},
titleContainer: {
height: '15%',
justifyContent: 'flex-end',
},
title: {
fontSize: 30,
fontWeight: 'bold',
textAlign: 'center',
},
imageContainer: {
height: '40%',
justifyContent: 'center',
alignItems: 'center',
width: '80%',
padding: 20,
},
descriptionContainer: {
height: '25%',
justifyContent: 'flex-start',
padding: 20,
},
description: {
fontSize: 25,
textAlign: 'center',
},
finishButton: {
borderColor: 'black',
borderRadius: 20,
padding: 10,
margin: 15,
},
});
Loading