Skip to content

Commit 69ea0fd

Browse files
committed
feat(onboarding): added onborading pages
1 parent 9dc54ba commit 69ea0fd

7 files changed

Lines changed: 345 additions & 44 deletions

File tree

app/(auth)/(home)/profile/exploreGroups.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const styles = StyleSheet.create({
2929
});
3030

3131
export interface UserGroupWithKey {
32-
key: string;
32+
nameKey: string;
3333
name?: string;
3434
archivedAt?: string;
3535
archivedBy?: string;
@@ -95,7 +95,7 @@ function ExploreGroups() {
9595
{filteredGroups.length > 0 ? (
9696
filteredGroups.map((group) => (
9797
<ClickableListItem
98-
key={group.key}
98+
key={group.nameKey}
9999
name={group.key}
100100
title={group.name ?? ''}
101101
onPress={onHandleItemClick}

app/_layout.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
import 'react-native-reanimated';
2-
31
import {
42
useEffect,
53
useMemo,
64
useState,
75
} from 'react';
8-
import {
9-
ActivityIndicator,
10-
View,
11-
} from 'react-native';
6+
import { View } from 'react-native';
127
import Toast, {
138
BaseToast,
149
ErrorToast,
@@ -20,8 +15,8 @@ import { isDefined } from '@togglecorp/fujs';
2015
import { User } from 'firebase/auth';
2116
import { Provider as UrqlProvider } from 'urql';
2217

18+
import LoadingComponent from '@/components/Loader';
2319
import Page from '@/components/Page';
24-
import Text from '@/components/Text';
2520
import AuthContext, { AuthContextProps } from '@/contexts/auth';
2621
import useTheme from '@/hooks/useTheme';
2722
import { fetchCsrfToken } from '@/utils/csrfToken';
@@ -30,7 +25,7 @@ import client from '@/utils/urqlClient';
3025

3126
export {
3227
// Catch any errors thrown by the Layout component.
33-
ErrorBoundary,
28+
ErrorBoundary
3429
} from 'expo-router';
3530

3631
export const toastConfig = {
@@ -128,10 +123,7 @@ export default function AppLayout() {
128123
gap: 10,
129124
}}
130125
>
131-
<ActivityIndicator size="large" />
132-
<Text>
133-
Getting things ready...
134-
</Text>
126+
<LoadingComponent label="Getting things ready..." />
135127
</View>
136128
</Page>
137129
);
@@ -148,6 +140,9 @@ export default function AppLayout() {
148140
style="light"
149141
/>
150142
<Stack screenOptions={{ headerShown: false }}>
143+
<Stack.Protected guard={!isAuthenticated}>
144+
<Stack.Screen name="onboarding" />
145+
</Stack.Protected>
151146
<Stack.Protected guard={!isAuthenticated}>
152147
<Stack.Screen name="login" />
153148
</Stack.Protected>

app/index.tsx

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,42 @@
11
import { useEffect } from 'react';
2-
import {
3-
ActivityIndicator,
4-
StyleSheet,
5-
View,
6-
} from 'react-native';
72
import { router } from 'expo-router';
3+
import AsyncStorage from '@react-native-async-storage/async-storage';
84

5+
import LoadingComponent from '@/components/Loader';
96
import Page from '@/components/Page';
10-
import Text from '@/components/Text';
117
import useAuth from '@/hooks/useAuth';
128

13-
const styles = StyleSheet.create({
14-
mainView: {
15-
flex: 1,
16-
alignItems: 'center',
17-
justifyContent: 'center',
18-
gap: 10,
19-
},
20-
});
21-
229
function AppIndex() {
2310
const {
2411
authPending,
2512
isLoggedIn,
2613
} = useAuth();
2714

2815
useEffect(() => {
29-
if (!authPending && isLoggedIn) {
30-
router.push('/projects');
31-
}
16+
const checkNavigation = async () => {
17+
if (authPending) return;
18+
19+
if (isLoggedIn) {
20+
router.push('/projects');
21+
}
22+
try {
23+
const hasSeen = await AsyncStorage.getItem('@hasSeenOnboarding');
24+
if (hasSeen && !isLoggedIn) {
25+
router.replace('/login');
26+
} else {
27+
router.replace('/onboarding');
28+
}
29+
} catch {
30+
router.replace('/onboarding');
31+
}
32+
};
3233

33-
if (!authPending && !isLoggedIn) {
34-
router.push('/login');
35-
}
34+
checkNavigation();
3635
}, [authPending, isLoggedIn]);
3736

3837
return (
3938
<Page title="MapSwipe">
40-
<View style={styles.mainView}>
41-
<ActivityIndicator size="large" />
42-
<Text>
43-
Checking user session...
44-
</Text>
45-
</View>
39+
<LoadingComponent label="Getting things ready..." />
4640
</Page>
4741
);
4842
}

app/onboarding.tsx

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import React, {
2+
useRef,
3+
useState,
4+
} from 'react';
5+
import {
6+
Dimensions,
7+
FlatList,
8+
NativeScrollEvent,
9+
NativeSyntheticEvent,
10+
StyleSheet,
11+
View,
12+
} from 'react-native';
13+
import { Image } from 'expo-image';
14+
import { useRouter } from 'expo-router';
15+
import AsyncStorage from '@react-native-async-storage/async-storage';
16+
17+
import welcome1 from '@/assets/images/custom/welcome1.png';
18+
import welcome2 from '@/assets/images/custom/welcome2.png';
19+
import welcome3 from '@/assets/images/custom/welcome3.png';
20+
import welcome4 from '@/assets/images/custom/welcome4.png';
21+
import welcome5 from '@/assets/images/custom/welcome5.png';
22+
import BlockListView from '@/components/BlockListView';
23+
import Button from '@/components/Button';
24+
import Text from '@/components/Text';
25+
import {
26+
FONT_SIZE_3XL,
27+
FONT_SIZE_XL,
28+
SCREEN_HEIGHT,
29+
SCREEN_WIDTH,
30+
} from '@/constants/dimensions';
31+
import { AppTheme } from '@/constants/theme';
32+
import useTheme from '@/hooks/useTheme';
33+
import useThemedStyles from '@/hooks/useThemedStyles';
34+
35+
const { width, height } = Dimensions.get('window');
36+
37+
const slides = [
38+
{
39+
id: '1',
40+
title: 'Welcome to MapSwipe',
41+
description: 'Help improve humanitarian responses from the comfort of your phone',
42+
imageUrl: welcome1,
43+
},
44+
{
45+
id: '2',
46+
title: 'Part of Missing Maps',
47+
description: 'With Missing Maps, we aim to put the world\'s vulnerable communities on the map',
48+
imageUrl: welcome2,
49+
},
50+
{
51+
id: '3',
52+
title: 'Swipe',
53+
description: 'Complete tasks by swiping through satellite imagery of areas that need mapping',
54+
imageUrl: welcome3,
55+
},
56+
{
57+
id: '4',
58+
title: 'Create meaningful data',
59+
description: 'The data is used to focus the efforts of Missing Maps volunteers to add detail to OpenStreetMap',
60+
imageUrl: welcome4,
61+
},
62+
{
63+
id: '5',
64+
title: 'Save lives',
65+
description: 'The map helps organisations coordinate humanitarian efforts and save lives',
66+
imageUrl: welcome5,
67+
},
68+
];
69+
70+
const createStyles = (theme: AppTheme) => StyleSheet.create({
71+
mainContent: {
72+
width,
73+
height,
74+
justifyContent: 'center',
75+
alignItems: 'center',
76+
},
77+
icon: {
78+
resizeMode: 'contain',
79+
height: SCREEN_HEIGHT * 0.3,
80+
width: SCREEN_WIDTH * 0.8,
81+
},
82+
heading: {
83+
color: theme.primaryBlue,
84+
textAlign: 'center',
85+
fontSize: FONT_SIZE_3XL,
86+
fontWeight: 'bold',
87+
width: SCREEN_WIDTH * 0.75,
88+
},
89+
text: {
90+
color: theme.primaryBlue,
91+
width: SCREEN_WIDTH * 0.8,
92+
textAlign: 'center',
93+
fontSize: FONT_SIZE_XL,
94+
},
95+
dotContainer: {
96+
flexDirection: 'row',
97+
justifyContent: 'center',
98+
position: 'absolute',
99+
bottom: 40,
100+
width: '100%',
101+
},
102+
dotBase: {
103+
height: 8,
104+
width: 8,
105+
borderRadius: 4,
106+
marginHorizontal: 6,
107+
},
108+
109+
});
110+
export default function Onboarding() {
111+
const [index, setIndex] = useState(0);
112+
const flatListRef = useRef<FlatList>(null);
113+
const styles = useThemedStyles(createStyles);
114+
const theme = useTheme();
115+
const router = useRouter();
116+
117+
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
118+
const slideIndex = Math.round(
119+
event.nativeEvent.contentOffset.x / width,
120+
);
121+
setIndex(slideIndex);
122+
};
123+
124+
const handleSignUp = async () => {
125+
try {
126+
await AsyncStorage.setItem('@hasSeenOnboarding', 'true');
127+
router.replace('/register'); // Navigate to Sign Up screen
128+
} catch (error) {
129+
console.error('Error saving onboarding state:', error);
130+
}
131+
};
132+
133+
return (
134+
<View>
135+
<FlatList
136+
ref={flatListRef}
137+
data={slides}
138+
horizontal
139+
pagingEnabled
140+
showsHorizontalScrollIndicator={false}
141+
onScroll={handleScroll}
142+
scrollEventThrottle={16}
143+
keyExtractor={(item) => item.id}
144+
renderItem={({ item, index: itemIndex }) => (
145+
<BlockListView
146+
style={styles.mainContent}
147+
withCenteredContent
148+
withPadding
149+
>
150+
<Image
151+
style={styles.icon}
152+
source={item.imageUrl}
153+
/>
154+
<BlockListView withCenteredContent withPadding>
155+
<Text style={styles.heading}>
156+
{item.title}
157+
</Text>
158+
<Text style={styles.text}>
159+
{item.description}
160+
</Text>
161+
</BlockListView>
162+
163+
{/* Only render button on the last slide */}
164+
{itemIndex === slides.length - 1 && (
165+
<Button
166+
name="Sign Up"
167+
title="Sign Up"
168+
colorVariant="primaryRed"
169+
styleVariant="filled"
170+
onPress={handleSignUp}
171+
/>
172+
)}
173+
</BlockListView>
174+
)}
175+
/>
176+
<View style={styles.dotContainer}>
177+
{slides.map((item, i) => (
178+
<View
179+
key={item.id}
180+
style={[
181+
styles.dotBase,
182+
// eslint-disable-next-line react-native/no-inline-styles
183+
{ backgroundColor: i === index ? theme.backgroundBrand : '#ccc' },
184+
]}
185+
/>
186+
))}
187+
</View>
188+
</View>
189+
);
190+
}

0 commit comments

Comments
 (0)