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

fix: onboarding screen #351

Merged
merged 4 commits into from
Feb 20, 2025
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
70 changes: 7 additions & 63 deletions apps/native/app/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,14 @@
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import { Slot } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import React, { useEffect } from "react";
import "react-native-reanimated";
import {
MD3LightTheme as DefaultTheme,
PaperProvider,
} from "react-native-paper";

import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
import { StyleSheet } from "react-native";

export { ErrorBoundary } from "expo-router";

const theme = {
...DefaultTheme,
colors: {
...DefaultTheme.colors,
primary: "tomato",
secondary: "yellow",
},
};

SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
const [loaded, error] = useFonts({
export default function AppLayout() {
const [areFontsLoaded, fontLoadError] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
Roboto: require("../assets/fonts/Roboto-Regular.ttf"),
RobotoBold: require("../assets/fonts/Roboto-Bold.ttf"),
Expand All @@ -35,48 +17,10 @@ export default function RootLayout() {
});

useEffect(() => {
if (error) throw error;
}, [error]);

useEffect(() => {
if (loaded) {
if (areFontsLoaded || fontLoadError) {
SplashScreen.hideAsync();
}
}, [loaded]);
}, [areFontsLoaded, fontLoadError]);

if (!loaded) {
return null;
}

return (
<SafeAreaProvider>
<SafeAreaView style={styles.container}>
<RootLayoutNav />
</SafeAreaView>
</SafeAreaProvider>
);
return <Slot />;
}

export const options = {
headerShown: false,
};

function RootLayoutNav() {
return (
<PaperProvider theme={theme}>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={options} />
<Stack.Screen name="(auth)/login" options={{ headerShown: false }} />
<Stack.Screen name="(auth)/register" options={{ headerShown: false }} />
<Stack.Screen name="accountConfirmation" options={options} />
</Stack>
</PaperProvider>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
},
});
26 changes: 24 additions & 2 deletions apps/native/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
import OnboardingScreen from "@/screens/onboarding/Onboarding.screen";
export default () => <OnboardingScreen />;
import React, { useEffect, useState } from "react";
import { Redirect, useRouter } from "expo-router";
import AsyncStorage from "@react-native-async-storage/async-storage";

export default function InitialRoute() {
const [shouldShowOnboarding, setShouldShowOnboarding] = useState(false);
const router = useRouter();

useEffect(() => {
const verifyAppLaunchStatus = async () => {
const hasCompletedOnboarding = await AsyncStorage.getItem("hasLaunched");

if (hasCompletedOnboarding === null) {
setShouldShowOnboarding(true);
} else {
router.replace("/(auth)/login");
}
};

verifyAppLaunchStatus();
}, [router]);

return shouldShowOnboarding ? <Redirect href="/onboarding" /> : null;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { router } from "expo-router";
import React, { useState } from "react";
import { useRouter } from "expo-router";
import React, { useState, useCallback } from "react";
import {
View,
Text,
Expand All @@ -15,6 +15,8 @@ import Leafs from "@/assets/svg/leafs.svg";
import Wallet from "@/assets/svg/wallet.svg";
import Cloud from "@/assets/svg/cloud.svg";
import { SvgProps } from "react-native-svg";
import CustomButton from "@/components/ui/common/CustomButton";
import AsyncStorage from "@react-native-async-storage/async-storage";

const { width, height } = Dimensions.get("window");

Expand Down Expand Up @@ -49,9 +51,25 @@ const DATA: OnboardingItem[] = [
const OnboardingScreen = () => {
const [currentIndex, setCurrentIndex] = useState(0);

const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width);
setCurrentIndex(slideIndex);
const router = useRouter();
let flashListRef = React.useRef<FlashList<OnboardingItem>>(null);

const handleScroll = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
const slideIndex = Math.round(event.nativeEvent.contentOffset.x / width);
setCurrentIndex(slideIndex);
},
[],
);

const handleSignUp = async () => {
await AsyncStorage.setItem("hasLaunched", "true");
router.push("/(auth)/register");
};

const handleLogIn = async () => {
await AsyncStorage.setItem("hasLaunched", "true");
router.push("/(auth)/login");
};

const renderItem = ({ item }: { item: OnboardingItem }) => {
Expand All @@ -68,18 +86,24 @@ const OnboardingScreen = () => {
return (
<SafeAreaView style={styles.safeArea}>
<FlashList
ref={flashListRef}
data={DATA}
renderItem={renderItem}
horizontal
pagingEnabled
onScroll={handleScroll}
onMomentumScrollEnd={handleScroll}
showsHorizontalScrollIndicator={false}
estimatedItemSize={height}
keyExtractor={item => item.id}
/>

<View style={styles.pagination}>
{DATA.map((_, index) => (
<View
<TouchableOpacity
key={index}
onPress={() =>
flashListRef.current?.scrollToIndex({ index, animated: true })
}
style={[
styles.circleWrapper,
currentIndex === index ? styles.activeCircle : null,
Expand All @@ -90,24 +114,18 @@ const OnboardingScreen = () => {
currentIndex === index ? styles.activeDot : styles.inactiveDot,
]}
/>
</View>
</TouchableOpacity>
))}
</View>

<View style={styles.buttonContainer}>
{currentIndex === DATA.length - 1 ? (
<TouchableOpacity
onPress={() => router.push("/(auth)/login")}
style={styles.button}>
<Text style={styles.buttonText}>GET STARTED</Text>
</TouchableOpacity>
) : (
<TouchableOpacity onPress={() => {}} style={styles.button}>
<Text style={styles.buttonText}>CONTINUE</Text>
</TouchableOpacity>
)}
<TouchableOpacity>
<Text style={styles.skipText}>SKIP THE TOUR</Text>
</TouchableOpacity>
<CustomButton title="SIGN UP" onPress={handleSignUp} />

<CustomButton
title="LOG IN"
variant="secondary"
onPress={handleLogIn}
/>
</View>
</SafeAreaView>
);
Expand Down Expand Up @@ -150,11 +168,9 @@ const styles = StyleSheet.create({
justifyContent: "center",
width: 20,
height: 20,
marginHorizontal: 5,
marginHorizontal: 1,
},
activeCircle: {
borderWidth: 2,
borderColor: "#4CAF50",
borderRadius: 10,
},
dot: {
Expand All @@ -163,16 +179,17 @@ const styles = StyleSheet.create({
borderRadius: 6,
},
activeDot: {
backgroundColor: "#4CAF50",
backgroundColor: "#FF7A00",
},
inactiveDot: {
backgroundColor: "transparent",
backgroundColor: "#BDBDBD",
borderWidth: 2,
borderColor: "#ccc",
},
buttonContainer: {
alignItems: "center",
paddingHorizontal: 20,
gap: 8,
marginBottom: 20,
},
button: {
Expand Down
70 changes: 70 additions & 0 deletions apps/native/components/ui/common/CustomButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from "react";
import {
TouchableOpacity,
Text,
StyleSheet,
ViewStyle,
TextStyle,
} from "react-native";

type ButtonProps = {
title: string;
onPress: () => void;
variant?: "primary" | "secondary";
style?: ViewStyle;
textStyle?: TextStyle;
};

const CustomButton: React.FC<ButtonProps> = ({
title,
onPress,
variant = "primary",
style,
textStyle,
}) => {
const isSecondary = variant === "secondary";

return (
<TouchableOpacity
style={StyleSheet.flatten([
styles.button,
isSecondary && styles.secondaryButton,
style,
])}
onPress={onPress}>
<Text
style={StyleSheet.flatten([
styles.buttonText,
isSecondary && styles.secondaryText,
textStyle,
])}>
{title}
</Text>
</TouchableOpacity>
);
};

const styles = StyleSheet.create({
button: {
backgroundColor: "#61892F",
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: "center",
justifyContent: "center",
width: "100%",
},
secondaryButton: {
backgroundColor: "transparent",
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "bold",
},
secondaryText: {
color: "#6B6E70",
},
});

export default CustomButton;