diff --git a/examples/common_expo/app/(app)/_layout.tsx b/examples/common_expo/app/(app)/_layout.tsx
index 01dfab2..24d8b93 100644
--- a/examples/common_expo/app/(app)/_layout.tsx
+++ b/examples/common_expo/app/(app)/_layout.tsx
@@ -16,7 +16,7 @@ export default function AppLayout() {
}
if (!session) {
- return ;
+ return ;
}
return (
diff --git a/examples/common_expo/app/onboarding.tsx b/examples/common_expo/app/onboarding.tsx
new file mode 100644
index 0000000..868b4c9
--- /dev/null
+++ b/examples/common_expo/app/onboarding.tsx
@@ -0,0 +1,74 @@
+import Animated, {
+ useAnimatedRef,
+ useAnimatedScrollHandler,
+ useSharedValue,
+} from 'react-native-reanimated';
+import { SafeAreaView, StyleSheet, View } from 'react-native';
+
+import {
+ NextButton,
+ OnboardingListItem,
+ OnboardingItem,
+ OnboardingWrapper,
+ Pagination,
+} from '@/components/Onboarding';
+
+const items: OnboardingItem[] = [
+ { text: 'First' },
+ { text: 'Second' },
+ { text: 'Third' },
+];
+
+export default function Onboarding() {
+ const offsetX = useSharedValue(0);
+ const flatListIndex = useSharedValue(0);
+ const flatListRef = useAnimatedRef>();
+
+ const handleOnScroll = useAnimatedScrollHandler({
+ onScroll: (event) => {
+ offsetX.value = event.contentOffset.x;
+ },
+ });
+
+ return (
+
+
+ index.toString()}
+ renderItem={({ item, index }) => (
+
+ )}
+ onViewableItemsChanged={({ viewableItems }) => {
+ flatListIndex.value = viewableItems[0].index ?? 0;
+ }}
+ onScroll={handleOnScroll}
+ bounces={false}
+ showsHorizontalScrollIndicator={false}
+ horizontal
+ pagingEnabled
+ />
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ bottomPanel: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ paddingHorizontal: 24,
+ },
+});
diff --git a/examples/common_expo/app/sign-in.tsx b/examples/common_expo/app/sign-in.tsx
index 8db7645..d3e9531 100644
--- a/examples/common_expo/app/sign-in.tsx
+++ b/examples/common_expo/app/sign-in.tsx
@@ -73,7 +73,7 @@ export default function SignIn() {
Don't have an account?
-
@@ -86,7 +86,6 @@ const styles = StyleSheet.create({
},
signUpContainer: {
alignItems: 'center',
- display: 'flex',
marginTop: 24,
},
});
diff --git a/examples/common_expo/components/Onboarding/NextButton.tsx b/examples/common_expo/components/Onboarding/NextButton.tsx
new file mode 100644
index 0000000..bb45373
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/NextButton.tsx
@@ -0,0 +1,79 @@
+import { FC, useCallback } from 'react';
+import { Pressable, StyleSheet } from 'react-native';
+import Animated, {
+ AnimatedRef,
+ SharedValue,
+ useAnimatedStyle,
+ withTiming,
+} from 'react-native-reanimated';
+import { router } from 'expo-router';
+import { setAsyncStorageItem } from '@/utilities/asyncStorage';
+
+import { OnboardingItem } from './OnboardingListItem';
+import { onboardingKey } from './OnboardingWrapper';
+
+type NextButtonProps = {
+ flatListRef: AnimatedRef>;
+ listIndex: SharedValue;
+ listLength: number;
+};
+
+const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
+
+export const NextButton: FC = ({
+ flatListRef,
+ listIndex,
+ listLength,
+}) => {
+ const animatedButtonStyle = useAnimatedStyle(() => ({
+ width:
+ listIndex.value === listLength - 1 ? withTiming(120) : withTiming(60),
+ }));
+
+ const animatedTextStyle = useAnimatedStyle(() => ({
+ opacity: listIndex.value === listLength - 1 ? withTiming(1) : withTiming(0),
+ transform: [
+ {
+ translateX:
+ listIndex.value === listLength - 1 ? withTiming(0) : withTiming(100),
+ },
+ ],
+ }));
+
+ const handleOnPress = useCallback(() => {
+ if (listIndex.value === listLength - 1) {
+ setAsyncStorageItem(onboardingKey, 'true');
+
+ router.replace('/sign-in');
+ return;
+ }
+
+ flatListRef.current?.scrollToIndex({ index: listIndex.value + 1 });
+ }, [flatListRef, listIndex.value, listLength]);
+
+ return (
+
+
+ Start
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ button: {
+ alignItems: 'center',
+ backgroundColor: 'blue',
+ borderRadius: 80,
+ height: 60,
+ justifyContent: 'center',
+ },
+ textStyle: {
+ color: 'white',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+});
diff --git a/examples/common_expo/components/Onboarding/OnboardingListItem.tsx b/examples/common_expo/components/Onboarding/OnboardingListItem.tsx
new file mode 100644
index 0000000..b293064
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/OnboardingListItem.tsx
@@ -0,0 +1,24 @@
+import { FC } from 'react';
+import { useWindowDimensions } from 'react-native';
+
+import { CenterView } from '@/components/CenterView';
+import { Typography } from '@/components/Typography';
+
+export type OnboardingItem = {
+ text: string;
+};
+
+type ListItemProps = {
+ item: OnboardingItem;
+ listIndex: number;
+};
+
+export const OnboardingListItem: FC = ({ item }) => {
+ const { width: SCREEN_WIDTH } = useWindowDimensions();
+
+ return (
+
+ {item.text}
+
+ );
+};
diff --git a/examples/common_expo/components/Onboarding/OnboardingWrapper.tsx b/examples/common_expo/components/Onboarding/OnboardingWrapper.tsx
new file mode 100644
index 0000000..ede4df6
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/OnboardingWrapper.tsx
@@ -0,0 +1,41 @@
+import { Redirect } from 'expo-router';
+import { FC, PropsWithChildren, useEffect, useState } from 'react';
+import { ActivityIndicator } from 'react-native';
+
+import { CenterView } from '@/components/CenterView';
+import { getAsyncStorageItem } from '@/utilities/asyncStorage';
+
+export const onboardingKey = 'onboarding';
+
+export const OnboardingWrapper: FC = ({ children }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [isOnboardingCompleted, setIsOnboardingCompleted] = useState(false);
+
+ useEffect(() => {
+ const onMount = async () => {
+ const onboardingValue = await getAsyncStorageItem(onboardingKey);
+
+ if (onboardingValue) {
+ setIsOnboardingCompleted(true);
+ }
+
+ setIsLoading(false);
+ };
+
+ onMount();
+ }, []);
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (isOnboardingCompleted) {
+ return ;
+ }
+
+ return children;
+};
diff --git a/examples/common_expo/components/Onboarding/Pagination/Pagination.tsx b/examples/common_expo/components/Onboarding/Pagination/Pagination.tsx
new file mode 100644
index 0000000..6079910
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/Pagination/Pagination.tsx
@@ -0,0 +1,30 @@
+import { FC } from 'react';
+import { StyleSheet, View } from 'react-native';
+import { SharedValue } from 'react-native-reanimated';
+
+import { PaginationItem } from './PaginationItem';
+
+type PaginationProps = {
+ length: number;
+ offsetX: SharedValue;
+};
+
+export const Pagination: FC = ({ length, offsetX }) => (
+
+ {[...Array(length).keys()].map((_, index) => (
+
+ ))}
+
+);
+
+const styles = StyleSheet.create({
+ container: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ },
+});
diff --git a/examples/common_expo/components/Onboarding/Pagination/PaginationItem.tsx b/examples/common_expo/components/Onboarding/Pagination/PaginationItem.tsx
new file mode 100644
index 0000000..b3fd8f3
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/Pagination/PaginationItem.tsx
@@ -0,0 +1,60 @@
+import { FC } from 'react';
+import { StyleSheet, useWindowDimensions } from 'react-native';
+import Animated, {
+ Extrapolation,
+ interpolate,
+ interpolateColor,
+ SharedValue,
+ useAnimatedStyle,
+} from 'react-native-reanimated';
+
+type PaginationItemProps = {
+ itemIndex: number;
+ offsetX: SharedValue;
+};
+
+export const PaginationItem: FC = ({
+ itemIndex,
+ offsetX,
+}) => {
+ const { width: SCREEN_WIDTH } = useWindowDimensions();
+
+ const animatedItemStyle = useAnimatedStyle(() => {
+ const backgroundColor = interpolateColor(
+ offsetX.value,
+ [
+ (itemIndex - 1) * SCREEN_WIDTH,
+ itemIndex * SCREEN_WIDTH,
+ (itemIndex + 1) * SCREEN_WIDTH,
+ ],
+ ['#e2e2e2', 'blue', '#e2e2e2'],
+ );
+
+ const width = interpolate(
+ offsetX.value,
+ [
+ (itemIndex - 1) * SCREEN_WIDTH,
+ itemIndex * SCREEN_WIDTH,
+ (itemIndex + 1) * SCREEN_WIDTH,
+ ],
+ [32, 16, 32],
+ Extrapolation.CLAMP,
+ );
+
+ return {
+ width,
+ backgroundColor,
+ };
+ });
+
+ return ;
+};
+
+const styles = StyleSheet.create({
+ item: {
+ borderRadius: 5,
+ height: 10,
+ marginHorizontal: 4,
+ width: 32,
+ },
+});
diff --git a/examples/common_expo/components/Onboarding/Pagination/index.ts b/examples/common_expo/components/Onboarding/Pagination/index.ts
new file mode 100644
index 0000000..e016c96
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/Pagination/index.ts
@@ -0,0 +1 @@
+export * from './Pagination';
diff --git a/examples/common_expo/components/Onboarding/index.ts b/examples/common_expo/components/Onboarding/index.ts
new file mode 100644
index 0000000..2564218
--- /dev/null
+++ b/examples/common_expo/components/Onboarding/index.ts
@@ -0,0 +1,4 @@
+export * from './NextButton';
+export * from './OnboardingListItem';
+export * from './OnboardingWrapper';
+export * from './Pagination';
diff --git a/examples/common_expo/package.json b/examples/common_expo/package.json
index 46bb27d..08fbe1b 100644
--- a/examples/common_expo/package.json
+++ b/examples/common_expo/package.json
@@ -17,6 +17,7 @@
"dependencies": {
"@expo/vector-icons": "^14.0.2",
"@hookform/resolvers": "^3.9.0",
+ "@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/native": "^6.0.2",
"expo": "~51.0.31",
"expo-font": "~12.0.9",
diff --git a/examples/common_expo/pnpm-lock.yaml b/examples/common_expo/pnpm-lock.yaml
index 0a8d0d4..c6bf849 100644
--- a/examples/common_expo/pnpm-lock.yaml
+++ b/examples/common_expo/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@hookform/resolvers':
specifier: ^3.9.0
version: 3.9.0(react-hook-form@7.53.0(react@18.2.0))
+ '@react-native-async-storage/async-storage':
+ specifier: 1.23.1
+ version: 1.23.1(react-native@0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.3(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0))
'@react-navigation/native':
specifier: ^6.0.2
version: 6.1.18(react-native@0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.3(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0))(react@18.2.0)
@@ -1155,6 +1158,11 @@ packages:
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
+ '@react-native-async-storage/async-storage@1.23.1':
+ resolution: {integrity: sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==}
+ peerDependencies:
+ react-native: ^0.0.0-0 || >=0.60 <1.0
+
'@react-native-community/cli-clean@13.6.9':
resolution: {integrity: sha512-7Dj5+4p9JggxuVNOjPbduZBAP1SUgNhLKVw5noBUzT/3ZpUZkDM+RCSwyoyg8xKWoE4OrdUAXwAFlMcFDPKykA==}
@@ -3134,6 +3142,10 @@ packages:
resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==}
engines: {node: '>=8'}
+ is-plain-obj@2.1.0:
+ resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==}
+ engines: {node: '>=8'}
+
is-plain-object@2.0.4:
resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
engines: {node: '>=0.10.0'}
@@ -3649,6 +3661,10 @@ packages:
memory-cache@0.2.0:
resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==}
+ merge-options@3.0.4:
+ resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==}
+ engines: {node: '>=10'}
+
merge-stream@2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -6890,6 +6906,11 @@ snapshots:
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
react: 18.2.0
+ '@react-native-async-storage/async-storage@1.23.1(react-native@0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.3(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0))':
+ dependencies:
+ merge-options: 3.0.4
+ react-native: 0.74.5(@babel/core@7.25.2)(@babel/preset-env@7.25.3(@babel/core@7.25.2))(@types/react@18.2.79)(react@18.2.0)
+
'@react-native-community/cli-clean@13.6.9':
dependencies:
'@react-native-community/cli-tools': 13.6.9
@@ -9366,6 +9387,8 @@ snapshots:
is-path-inside@3.0.3: {}
+ is-plain-obj@2.1.0: {}
+
is-plain-object@2.0.4:
dependencies:
isobject: 3.0.1
@@ -10133,6 +10156,10 @@ snapshots:
memory-cache@0.2.0: {}
+ merge-options@3.0.4:
+ dependencies:
+ is-plain-obj: 2.1.0
+
merge-stream@2.0.0: {}
merge2@1.4.1: {}
diff --git a/examples/common_expo/utilities/asyncStorage.ts b/examples/common_expo/utilities/asyncStorage.ts
new file mode 100644
index 0000000..696c55e
--- /dev/null
+++ b/examples/common_expo/utilities/asyncStorage.ts
@@ -0,0 +1,37 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+export const getAsyncStorageItem = async (key: string) => {
+ try {
+ const jsonValue = await AsyncStorage.getItem(key);
+
+ if (jsonValue) {
+ return jsonValue;
+ }
+
+ return null;
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const setAsyncStorageItem = async (
+ key: string,
+ value: string | object,
+) => {
+ try {
+ await AsyncStorage.setItem(
+ key,
+ typeof value === 'string' ? value : JSON.stringify(value),
+ );
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const removeAsyncStorageItem = async (key: string) => {
+ try {
+ await AsyncStorage.removeItem(key);
+ } catch (error) {
+ console.error(error);
+ }
+};