├── .watchmanconfig ├── reset.d.ts ├── src ├── traduction │ ├── explore.en.json │ ├── home.en.json │ └── sign-in.en.json ├── hooks │ ├── useColorScheme.ts │ ├── useColorScheme.web.ts │ ├── useThemeColor.ts │ ├── useGenericMutation.ts │ └── useStorageState.ts ├── services │ ├── routes.ts │ ├── index.ts │ └── auth.ts ├── assets │ ├── images │ │ ├── icon.png │ │ ├── favicon.png │ │ ├── splash.png │ │ ├── react-logo.png │ │ ├── adaptive-icon.png │ │ ├── react-logo@2x.png │ │ ├── react-logo@3x.png │ │ └── partial-react-logo.png │ └── fonts │ │ └── SpaceMono-Regular.ttf ├── config │ └── env.ts ├── constants │ ├── window.ts │ └── Colors.ts ├── store │ ├── selectors │ │ └── app.ts │ ├── slices │ │ └── appSlice.ts │ ├── storage.ts │ └── index.ts ├── types │ ├── index.d.ts │ └── auth.ts ├── components │ ├── __tests__ │ │ ├── ThemedText-test.tsx │ │ └── __snapshots__ │ │ │ └── ThemedText-test.tsx.snap │ ├── navigation │ │ └── TabBarIcon.tsx │ ├── ThemedView.tsx │ ├── ExternalLink.tsx │ ├── HelloWave.tsx │ ├── Collapsible.tsx │ ├── ThemedText.tsx │ └── ParallaxScrollView.tsx ├── app │ ├── sign-in.tsx │ ├── +not-found.tsx │ ├── (tabs) │ │ ├── _layout.tsx │ │ ├── index.tsx │ │ └── explore.tsx │ ├── +html.tsx │ └── _layout.tsx ├── theme │ └── index.ts ├── providers │ ├── protected.tsx │ ├── react-query.tsx │ └── index.tsx ├── queries │ └── auth.ts ├── lib │ └── toast.ts ├── utils │ └── i18n.ts ├── contexts │ ├── session.tsx │ └── theme.tsx └── scripts │ └── reset-project.js ├── bun.lockb ├── .env.example ├── env.d.ts ├── .gitignore ├── metro.config.js ├── tsconfig.json ├── app.json ├── babel.config.js ├── biome.json ├── package.json └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset' 2 | -------------------------------------------------------------------------------- /src/traduction/explore.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Explore" 3 | } 4 | -------------------------------------------------------------------------------- /src/traduction/home.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "Welcome!" 3 | } 4 | -------------------------------------------------------------------------------- /src/hooks/useColorScheme.ts: -------------------------------------------------------------------------------- 1 | export { useColorScheme } from 'react-native' 2 | -------------------------------------------------------------------------------- /src/traduction/sign-in.en.json: -------------------------------------------------------------------------------- 1 | { 2 | "heading": "This is a sign-in page" 3 | } 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/bun.lockb -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_API_BASE_URL=https://www.microservice.com/api/v2 2 | EXPO_PUBLIC_TOKEN_KEY=web_token 3 | -------------------------------------------------------------------------------- /src/services/routes.ts: -------------------------------------------------------------------------------- 1 | export enum AUTH_ROUTES { 2 | register = '/auth/signup', 3 | login = '/auth/signin', 4 | } 5 | -------------------------------------------------------------------------------- /src/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/icon.png -------------------------------------------------------------------------------- /src/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/favicon.png -------------------------------------------------------------------------------- /src/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/splash.png -------------------------------------------------------------------------------- /src/assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/react-logo.png -------------------------------------------------------------------------------- /src/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /src/assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /src/assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /src/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /src/config/env.ts: -------------------------------------------------------------------------------- 1 | export const BACKEND_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL 2 | export const TOKEN_KEY = process.env.EXPO_PUBLIC_TOKEN_KEY 3 | -------------------------------------------------------------------------------- /src/assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/John-pels/react-native-starter-template/HEAD/src/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /src/constants/window.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from "react-native"; 2 | 3 | export const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = 4 | Dimensions.get("window"); 5 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | EXPO_PUBLIC_API_BASE_URL: string 5 | EXPO_PUBLIC_TOKEN_KEY: string 6 | } 7 | } 8 | } 9 | 10 | export type {} 11 | -------------------------------------------------------------------------------- /src/store/selectors/app.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from ".."; 2 | 3 | export const isConnectedToInternet = (state: RootState): boolean => 4 | state.app.isConnectedToInternet; 5 | export const theme = (state: RootState): string => state.app.theme; 6 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import type { SvgProps } from 'react-native-svg' 3 | const content: React.FC 4 | export default content 5 | } 6 | 7 | export type Props = SvgProps & { 8 | color?: string 9 | } 10 | -------------------------------------------------------------------------------- /src/components/__tests__/ThemedText-test.tsx: -------------------------------------------------------------------------------- 1 | import renderer from 'react-test-renderer' 2 | 3 | import { ThemedText } from '../ThemedText' 4 | 5 | it('renders correctly', () => { 6 | const tree = renderer.create(Snapshot test!).toJSON() 7 | 8 | expect(tree).toMatchSnapshot() 9 | }) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | # macOS 13 | .DS_Store 14 | 15 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 16 | # The following patterns were generated by expo-cli 17 | 18 | expo-env.d.ts 19 | # @end expo-cli 20 | -------------------------------------------------------------------------------- /src/app/sign-in.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next' 2 | import { StyleSheet, Text, View } from 'react-native' 3 | 4 | export default function SignInScreen() { 5 | const { t } = useTranslation('signIn') 6 | return ( 7 | 8 | {t('heading')} 9 | 10 | ) 11 | } 12 | 13 | const styles = StyleSheet.create({ 14 | container: { 15 | flex: 1, 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /src/components/__tests__/__snapshots__/ThemedText-test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders correctly 1`] = ` 4 | 22 | Snapshot test! 23 | 24 | `; 25 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | import { Colors } from "@constants/Colors"; 3 | import { SCREEN_HEIGHT, SCREEN_WIDTH } from "../constants/window"; 4 | 5 | export const theme = { 6 | colors: { 7 | light: { ...Colors.light }, 8 | dark: { ...Colors.dark }, 9 | }, 10 | size: { 11 | width: SCREEN_WIDTH, 12 | height: SCREEN_HEIGHT, 13 | }, 14 | platform: { 15 | android: Platform.OS === "android", 16 | ios: Platform.OS === "ios", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/hooks/useColorScheme.web.ts: -------------------------------------------------------------------------------- 1 | // NOTE: The default React Native styling doesn't support server rendering. 2 | // Server rendered styles should not change between the first render of the HTML 3 | // and the first render on the client. Typically, web developers will use CSS media queries 4 | // to render different styles on the client and server, these aren't directly supported in React Native 5 | // but can be achieved using a styling library like Nativewind. 6 | export function useColorScheme() { 7 | return 'light' 8 | } 9 | -------------------------------------------------------------------------------- /src/components/navigation/TabBarIcon.tsx: -------------------------------------------------------------------------------- 1 | // You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ 2 | 3 | import Ionicons from '@expo/vector-icons/Ionicons' 4 | import type { IconProps } from '@expo/vector-icons/build/createIconSet' 5 | import type { ComponentProps } from 'react' 6 | 7 | export function TabBarIcon({ 8 | style, 9 | ...rest 10 | }: IconProps['name']>) { 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /src/providers/protected.tsx: -------------------------------------------------------------------------------- 1 | import { type Href, Redirect } from 'expo-router' 2 | import { Text } from 'react-native' 3 | import { useSession } from '../contexts/session' 4 | 5 | export const ProtectedProvider = ({ 6 | children, 7 | }: Readonly<{ children: React.ReactNode }>) => { 8 | const { isLoading, session } = useSession() 9 | 10 | // if (isLoading) { 11 | // return Loading... 12 | // } 13 | // if (!session) { 14 | // return 15 | // } 16 | return children 17 | } 18 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig } = require('expo/metro-config') 2 | module.exports = (() => { 3 | const config = getDefaultConfig(__dirname) 4 | const { transformer, resolver } = config 5 | 6 | config.transformer = { 7 | ...transformer, 8 | babelTransformerPath: require.resolve('react-native-svg-transformer'), 9 | } 10 | config.resolver = { 11 | ...resolver, 12 | assetExts: resolver.assetExts.filter((ext) => ext !== 'svg'), 13 | sourceExts: [...resolver.sourceExts, 'svg'], 14 | } 15 | 16 | return config 17 | })() 18 | -------------------------------------------------------------------------------- /src/components/ThemedView.tsx: -------------------------------------------------------------------------------- 1 | import { View, type ViewProps } from 'react-native' 2 | 3 | import { useThemeColor } from '@/src/hooks/useThemeColor' 4 | 5 | export type ThemedViewProps = ViewProps & { 6 | lightColor?: string 7 | darkColor?: string 8 | } 9 | 10 | export function ThemedView({ 11 | style, 12 | lightColor, 13 | darkColor, 14 | ...otherProps 15 | }: ThemedViewProps) { 16 | const backgroundColor = useThemeColor( 17 | { light: lightColor, dark: darkColor }, 18 | 'background' 19 | ) 20 | 21 | return 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/useThemeColor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Learn more about light and dark modes: 3 | * https://docs.expo.dev/guides/color-schemes/ 4 | */ 5 | 6 | import { useColorScheme } from 'react-native' 7 | 8 | import { Colors } from '@/src/constants/Colors' 9 | 10 | export function useThemeColor( 11 | props: { light?: string; dark?: string }, 12 | colorName: keyof typeof Colors.light & keyof typeof Colors.dark 13 | ) { 14 | const theme = useColorScheme() ?? 'light' 15 | const colorFromProps = props[theme] 16 | 17 | if (colorFromProps) { 18 | return colorFromProps 19 | } 20 | return Colors[theme][colorName] 21 | } 22 | -------------------------------------------------------------------------------- /src/types/auth.ts: -------------------------------------------------------------------------------- 1 | export type UserRegister = { 2 | firstName: string 3 | lastName: string 4 | email: string 5 | password: string 6 | } 7 | 8 | export type UserLogin = Pick 9 | 10 | export type LoginResponse = { 11 | access_token: string 12 | success: true 13 | message: string 14 | data: { 15 | _id: string 16 | email: string 17 | firstName: string 18 | lastName: string 19 | } 20 | } 21 | 22 | export interface RegisterResponse { 23 | message: string 24 | success: boolean 25 | } 26 | 27 | export interface GenericResponse { 28 | message: string 29 | success: boolean 30 | } 31 | 32 | export type ErrorResponse = { 33 | response: { 34 | data: GenericResponse 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { BACKEND_BASE_URL, TOKEN_KEY } from "@config/env"; 2 | import axios, { type AxiosInstance } from "axios"; 3 | import * as SecureStore from "expo-secure-store"; 4 | 5 | export const Fetch: AxiosInstance = axios.create({ 6 | baseURL: BACKEND_BASE_URL, 7 | timeout: 50000, 8 | timeoutErrorMessage: 9 | "Request timeout there is maybe a problem with the server!", 10 | withCredentials: true, 11 | }); 12 | 13 | Fetch.interceptors.request.use( 14 | async (config) => { 15 | const accessToken = await SecureStore.getItemAsync(TOKEN_KEY); 16 | if (accessToken) { 17 | config.headers.Authorization = `Bearer ${accessToken}`; 18 | } 19 | return config; 20 | }, 21 | (error) => { 22 | return Promise.reject(error); 23 | } 24 | ); 25 | -------------------------------------------------------------------------------- /src/queries/auth.ts: -------------------------------------------------------------------------------- 1 | import { useGenericMutation } from '../hooks/useGenericMutation' 2 | import { authService } from '../services/auth' 3 | import type { 4 | ErrorResponse, 5 | LoginResponse, 6 | RegisterResponse, 7 | UserLogin, 8 | UserRegister, 9 | } from '../types/auth' 10 | 11 | export const useUserCreate = () => { 12 | return useGenericMutation( 13 | 'CREATE_USER_KEY', 14 | (newUser: UserRegister) => authService.CreateUser(newUser), 15 | undefined, 16 | undefined 17 | ) 18 | } 19 | 20 | export const useUserLogin = () => { 21 | return useGenericMutation( 22 | 'LOGIN_USER_KEY', 23 | (newUser: UserLogin) => authService.UserLogin(newUser), 24 | undefined, 25 | undefined 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ExternalLink.tsx: -------------------------------------------------------------------------------- 1 | import { type Href, Link } from 'expo-router' 2 | import { openBrowserAsync } from 'expo-web-browser' 3 | import type { ComponentProps } from 'react' 4 | import { Platform } from 'react-native' 5 | 6 | type Props = Omit, 'href'> & { href: string } 7 | 8 | export function ExternalLink({ href, ...rest }: Props) { 9 | return ( 10 | { 15 | if (Platform.OS !== 'web') { 16 | // Prevent the default behavior of linking to the default browser on native. 17 | event.preventDefault() 18 | // Open the link in an in-app browser. 19 | await openBrowserAsync(href) 20 | } 21 | }} 22 | /> 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/constants/Colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Below are the colors that are used in the app. The colors are defined in the light and dark mode. 3 | * There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc. 4 | */ 5 | 6 | const tintColorLight = '#0a7ea4' 7 | const tintColorDark = '#fff' 8 | 9 | export const Colors = { 10 | light: { 11 | text: '#11181C', 12 | background: '#fff', 13 | tint: tintColorLight, 14 | icon: '#687076', 15 | tabIconDefault: '#687076', 16 | tabIconSelected: tintColorLight, 17 | }, 18 | dark: { 19 | text: '#ECEDEE', 20 | background: '#151718', 21 | tint: tintColorDark, 22 | icon: '#9BA1A6', 23 | tabIconDefault: '#9BA1A6', 24 | tabIconSelected: tintColorDark, 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/toast.ts: -------------------------------------------------------------------------------- 1 | import Toast from "react-native-root-toast"; 2 | 3 | export const RNToast = { 4 | success: (msg: string) => { 5 | return Toast.show(msg, { 6 | duration: Toast.durations.LONG, 7 | shadow: true, 8 | animation: true, 9 | position: Toast.positions.TOP, 10 | backgroundColor: "#056221", 11 | }); 12 | }, 13 | error: (msg: string) => { 14 | return Toast.show(msg, { 15 | duration: Toast.durations.LONG, 16 | shadow: true, 17 | animation: true, 18 | position: Toast.positions.TOP, 19 | backgroundColor: "#ff0000", 20 | }); 21 | }, 22 | warning: (msg: string) => { 23 | return Toast.show(msg, { 24 | duration: Toast.durations.LONG, 25 | shadow: true, 26 | animation: true, 27 | position: Toast.positions.TOP, 28 | backgroundColor: "#FFAB3A", 29 | }); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/+not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Stack } from 'expo-router' 2 | import { StyleSheet } from 'react-native' 3 | 4 | import { ThemedText } from '@components/ThemedText' 5 | import { ThemedView } from '@components/ThemedView' 6 | 7 | export default function NotFoundScreen() { 8 | return ( 9 | <> 10 | 11 | 12 | This screen doesn't exist. 13 | 14 | Go to home screen! 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | const styles = StyleSheet.create({ 22 | container: { 23 | flex: 1, 24 | alignItems: 'center', 25 | justifyContent: 'center', 26 | padding: 20, 27 | }, 28 | link: { 29 | marginTop: 15, 30 | paddingVertical: 15, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /src/providers/react-query.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 2 | import * as React from 'react' 3 | import { Platform } from 'react-native' 4 | 5 | export function ReactQueryDevtools() { 6 | if (__DEV__ && Platform.OS === 'web') { 7 | const { ReactQueryDevtools } = require('@tanstack/react-query-devtools') 8 | return 9 | } 10 | return null 11 | } 12 | export function RQProvider({ 13 | children, 14 | }: Readonly<{ children: React.ReactNode }>) { 15 | const [queryClient] = React.useState( 16 | () => 17 | new QueryClient({ 18 | defaultOptions: { 19 | queries: { 20 | retry: 2, 21 | refetchOnWindowFocus: false, 22 | }, 23 | }, 24 | }) 25 | ) 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./*"], 8 | "@components/*": ["src/components/*"], 9 | "@assets/*": ["src/assets/*"], 10 | "@constants/*": ["src/constants/*"], 11 | "@theme/*": ["src/theme/*"], 12 | "@hooks/*": ["src/hooks/*"], 13 | "@contexts/*": ["src/contexts/*"], 14 | "@queries/*": ["src/queries/*"], 15 | "@providers/*": ["src/providers/*"], 16 | "@store/*": ["src/store/*"], 17 | "@screens/*": ["src/screens/*"], 18 | "@services/*": ["src/services/*"], 19 | "@utils/*": ["src/utils/*"], 20 | "@config/*": ["src/config/*"], 21 | "@data/*": ["src/data/*"], 22 | "@lib/*": ["src/lib/*"], 23 | "@types/*": ["src/types/*"], 24 | "@traduction/*": ["src/traduction/*"] 25 | } 26 | }, 27 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] 28 | } 29 | -------------------------------------------------------------------------------- /src/services/auth.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_KEY } from "@config/env"; 2 | import { setCookie } from "@lib/cookies"; 3 | import * as SecureStore from "expo-secure-store"; 4 | import { Fetch } from "."; 5 | import type { 6 | LoginResponse, 7 | RegisterResponse, 8 | UserLogin, 9 | UserRegister, 10 | } from "../types/auth"; 11 | import { AUTH_ROUTES } from "./routes"; 12 | 13 | class AuthService { 14 | async CreateUser(data: UserRegister) { 15 | const response = await Fetch.post( 16 | AUTH_ROUTES.register, 17 | data 18 | ); 19 | return response.data; 20 | } 21 | 22 | async UserLogin(payload: UserLogin) { 23 | try { 24 | const { data } = await Fetch.post( 25 | AUTH_ROUTES.login, 26 | payload 27 | ); 28 | await SecureStore.setItemAsync(TOKEN_KEY, data.access_token); 29 | return data; 30 | } catch (err) { 31 | throw err; 32 | } 33 | } 34 | } 35 | 36 | export const authService: AuthService = new AuthService(); 37 | -------------------------------------------------------------------------------- /src/components/HelloWave.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from 'react-native' 2 | import Animated, { 3 | useSharedValue, 4 | useAnimatedStyle, 5 | withTiming, 6 | withRepeat, 7 | withSequence, 8 | } from 'react-native-reanimated' 9 | 10 | import { ThemedText } from '@components/ThemedText' 11 | 12 | export function HelloWave() { 13 | const rotationAnimation = useSharedValue(0) 14 | 15 | rotationAnimation.value = withRepeat( 16 | withSequence( 17 | withTiming(25, { duration: 150 }), 18 | withTiming(0, { duration: 150 }) 19 | ), 20 | 4 // Run the animation 4 times 21 | ) 22 | 23 | const animatedStyle = useAnimatedStyle(() => ({ 24 | transform: [{ rotate: `${rotationAnimation.value}deg` }], 25 | })) 26 | 27 | return ( 28 | 29 | 👋 30 | 31 | ) 32 | } 33 | 34 | const styles = StyleSheet.create({ 35 | text: { 36 | fontSize: 28, 37 | lineHeight: 32, 38 | marginTop: -6, 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "react-native-starter-template", 4 | "slug": "react-native-starter-template", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./src/assets/images/icon.png", 8 | "scheme": "myapp", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./src/assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true, 17 | "bundleIdentifier": "com.john-pels.react-native-starter-template" 18 | }, 19 | "android": { 20 | "adaptiveIcon": { 21 | "foregroundImage": "./src/assets/images/adaptive-icon.png", 22 | "backgroundColor": "#ffffff" 23 | }, 24 | "package": "com.johnpels.reactnativestartertemplate" 25 | }, 26 | "web": { 27 | "bundler": "metro", 28 | "output": "static", 29 | "favicon": "./src/assets/images/favicon.png" 30 | }, 31 | "plugins": [ 32 | "expo-router" 33 | ], 34 | "experiments": { 35 | "typedRoutes": true 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import * as Localization from "react-native-localize"; 4 | 5 | import signIn from "@traduction/sign-in.en.json"; 6 | import home from "@traduction/home.en.json"; 7 | import explore from "@traduction/explore.en.json"; 8 | 9 | // Define your resources here 10 | const resources = { 11 | en: { 12 | signIn, 13 | home, 14 | explore, 15 | }, 16 | } as const; 17 | 18 | // Function to get the best available language 19 | const findBestAvailableLanguage = (): string => { 20 | const bestLanguage = Localization.findBestLanguageTag(Object.keys(resources)); 21 | return bestLanguage?.languageTag || "en"; 22 | }; 23 | 24 | // Initialize i18next 25 | i18n.use(initReactI18next).init({ 26 | compatibilityJSON: "v3", 27 | resources, 28 | lng: findBestAvailableLanguage(), 29 | fallbackLng: "en", 30 | debug: true, 31 | defaultNS: "translation", 32 | interpolation: { 33 | escapeValue: false, // not needed for react as it escapes by default 34 | }, 35 | react: { 36 | useSuspense: false, 37 | }, 38 | }); 39 | 40 | export default i18n; 41 | -------------------------------------------------------------------------------- /src/store/slices/appSlice.ts: -------------------------------------------------------------------------------- 1 | import type { Draft, PayloadAction } from "@reduxjs/toolkit"; 2 | import { createSlice } from "@reduxjs/toolkit"; 3 | interface App { 4 | isConnectedToInternet: boolean; 5 | theme: "light" | "dark"; 6 | } 7 | 8 | const initialState: App = { 9 | isConnectedToInternet: false, 10 | theme: "light", 11 | }; 12 | 13 | export const appSlice = createSlice({ 14 | name: "app", 15 | initialState, 16 | reducers: { 17 | setIsConnectedToInternet: ( 18 | state: Draft, 19 | action: PayloadAction 20 | ) => { 21 | state.isConnectedToInternet = action.payload; 22 | }, 23 | toggleTheme: (state: Draft) => { 24 | state.theme = state.theme === "light" ? "dark" : "light"; 25 | }, 26 | setTheme: ( 27 | state: Draft, 28 | action: PayloadAction 29 | ) => { 30 | state.theme = action.payload; 31 | }, 32 | }, 33 | }); 34 | 35 | export const { setIsConnectedToInternet, toggleTheme, setTheme } = 36 | appSlice.actions; 37 | 38 | export default appSlice.reducer; 39 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: [ 6 | [ 7 | "module-resolver", 8 | { 9 | root: ["./src"], 10 | extensions: [".ios.js", ".android.js", ".js", ".ts", ".tsx", ".json"], 11 | alias: { 12 | tests: ["./tests/"], 13 | "@components": "./src/components", 14 | "@assets": "./src/assets", 15 | "@constants": "./src/constants", 16 | "@theme": "./src/theme", 17 | "@hooks": "./src/hooks", 18 | "@contexts": "./src/contexts", 19 | "@queries": "./src/queries", 20 | "@providers": "./src/providers", 21 | "@store": "./src/store", 22 | "@screens": "./src/screens", 23 | "@services": "./src/services", 24 | "@utils": "./src/utils", 25 | "@config": "./src/config", 26 | "@data": "./src/data", 27 | "@lib": "./src/lib", 28 | "@types": "./src/types", 29 | "@traduction": "./src/traduction", 30 | }, 31 | }, 32 | ], 33 | ], 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "organizeImports": { 9 | "enabled": true 10 | }, 11 | "linter": { 12 | "enabled": true, 13 | "rules": { 14 | "recommended": true, 15 | "security": { 16 | "noDangerouslySetInnerHtml": "off" 17 | }, 18 | "correctness": { 19 | "noUnusedVariables": "error", 20 | "noUnusedImports": "error" 21 | } 22 | } 23 | }, 24 | "formatter": { 25 | "enabled": true, 26 | "lineWidth": 80, 27 | "indentStyle": "space", 28 | "indentWidth": 2, 29 | "lineEnding": "lf", 30 | "formatWithErrors": false 31 | }, 32 | "javascript": { 33 | "formatter": { 34 | "semicolons": "asNeeded", 35 | "trailingCommas": "es5", 36 | "quoteStyle": "single", 37 | "arrowParentheses": "always", 38 | "bracketSameLine": false, 39 | "bracketSpacing": true, 40 | "jsxQuoteStyle": "double", 41 | "quoteProperties": "asNeeded" 42 | } 43 | }, 44 | "json": { 45 | "formatter": { 46 | "trailingCommas": "none" 47 | }, 48 | "parser": { 49 | "allowComments": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/store/storage.ts: -------------------------------------------------------------------------------- 1 | // import { MMKV } from 'react-native-mmkv'; 2 | import AsyncStorage from "@react-native-async-storage/async-storage"; 3 | 4 | // Important note: Currently, React Native MMKV is not supported in Expo Go. 5 | // Instead, consider using Expo Dev Build. 6 | // Alternatively, you can find a workaround by using react-native-async-storage with redux-persist. 7 | // const storage = new MMKV(); 8 | 9 | export const reduxStorage = { 10 | getItem: async (key: string): Promise => { 11 | // validateKey(key); 12 | try { 13 | const value = await AsyncStorage.getItem(key); 14 | return value; 15 | } catch (error) { 16 | console.error("SecureStore getItem error:", error); 17 | return null; 18 | } 19 | }, 20 | setItem: async (key: string, value: string): Promise => { 21 | // validateKey(key); 22 | try { 23 | await AsyncStorage.setItem(key, value); 24 | } catch (error) { 25 | console.error("SecureStore setItem error:", error); 26 | } 27 | }, 28 | removeItem: async (key: string): Promise => { 29 | // validateKey(key); 30 | try { 31 | await AsyncStorage.removeItem(key); 32 | } catch (error) { 33 | console.error("SecureStore removeItem error:", error); 34 | } 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/app/(tabs)/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Tabs } from 'expo-router' 2 | 3 | import { ProtectedProvider } from '@/src/providers/protected' 4 | import { TabBarIcon } from '@components/navigation/TabBarIcon' 5 | import { Colors } from '@constants/Colors' 6 | import { useColorScheme } from '@hooks/useColorScheme' 7 | 8 | export default function TabLayout() { 9 | const colorScheme = useColorScheme() 10 | 11 | return ( 12 | 13 | 19 | ( 24 | 28 | ), 29 | }} 30 | /> 31 | ( 36 | 40 | ), 41 | }} 42 | /> 43 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import Ionicons from '@expo/vector-icons/Ionicons' 2 | import { type PropsWithChildren, useState } from 'react' 3 | import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native' 4 | 5 | import { Colors } from '@/src/constants/Colors' 6 | import { ThemedText } from '@components/ThemedText' 7 | import { ThemedView } from '@components/ThemedView' 8 | 9 | export function Collapsible({ 10 | children, 11 | title, 12 | }: PropsWithChildren & { title: string }) { 13 | const [isOpen, setIsOpen] = useState(false) 14 | const theme = useColorScheme() ?? 'light' 15 | 16 | return ( 17 | 18 | setIsOpen((value) => !value)} 21 | activeOpacity={0.8} 22 | > 23 | 28 | {title} 29 | 30 | {isOpen && {children}} 31 | 32 | ) 33 | } 34 | 35 | const styles = StyleSheet.create({ 36 | heading: { 37 | flexDirection: 'row', 38 | alignItems: 'center', 39 | gap: 6, 40 | }, 41 | content: { 42 | marginTop: 6, 43 | marginLeft: 24, 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from "@reduxjs/toolkit"; 2 | import { setupListeners } from "@reduxjs/toolkit/query/react"; 3 | import { 4 | TypedUseSelectorHook, 5 | useDispatch as useDispatchBase, 6 | useSelector as useSelectorBase, 7 | } from "react-redux"; 8 | import { 9 | FLUSH, 10 | PAUSE, 11 | PERSIST, 12 | PURGE, 13 | REGISTER, 14 | REHYDRATE, 15 | persistReducer, 16 | persistStore, 17 | } from "redux-persist"; 18 | import appSlice from "./slices/appSlice"; 19 | import { reduxStorage } from "./storage"; 20 | 21 | const rootReducer = combineReducers({ 22 | app: appSlice, 23 | }); 24 | 25 | const persistConfig = { 26 | key: "root", 27 | version: 1, 28 | storage: reduxStorage, 29 | timeout: 0, 30 | whitelist: ["app"], 31 | }; 32 | 33 | const persistedReducer = persistReducer(persistConfig, rootReducer); 34 | 35 | export const store = configureStore({ 36 | reducer: persistedReducer, 37 | middleware: (getDefaultMiddleware) => 38 | getDefaultMiddleware({ 39 | serializableCheck: { 40 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], 41 | }, 42 | }), 43 | }); 44 | 45 | export const persistor = persistStore(store); 46 | 47 | export type RootState = ReturnType; 48 | 49 | export type AppDispatch = typeof store.dispatch; 50 | 51 | export const useAppDispatch: () => AppDispatch = useDispatchBase; 52 | export const useAppSelector: TypedUseSelectorHook = useSelectorBase; 53 | 54 | setupListeners(store.dispatch); 55 | -------------------------------------------------------------------------------- /src/components/ThemedText.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet, Text, type TextProps } from 'react-native' 2 | 3 | import { useThemeColor } from '@/src/hooks/useThemeColor' 4 | 5 | export type ThemedTextProps = TextProps & { 6 | lightColor?: string 7 | darkColor?: string 8 | type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' 9 | } 10 | 11 | export function ThemedText({ 12 | style, 13 | lightColor, 14 | darkColor, 15 | type = 'default', 16 | ...rest 17 | }: ThemedTextProps) { 18 | const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text') 19 | 20 | return ( 21 | 33 | ) 34 | } 35 | 36 | const styles = StyleSheet.create({ 37 | default: { 38 | fontSize: 16, 39 | lineHeight: 24, 40 | }, 41 | defaultSemiBold: { 42 | fontSize: 16, 43 | lineHeight: 24, 44 | fontWeight: '600', 45 | }, 46 | title: { 47 | fontSize: 32, 48 | fontWeight: 'bold', 49 | lineHeight: 32, 50 | }, 51 | subtitle: { 52 | fontSize: 20, 53 | fontWeight: 'bold', 54 | }, 55 | link: { 56 | lineHeight: 30, 57 | fontSize: 16, 58 | color: '#0a7ea4', 59 | }, 60 | }) 61 | -------------------------------------------------------------------------------- /src/contexts/session.tsx: -------------------------------------------------------------------------------- 1 | import { useStorageState } from '@hooks/useStorageState' 2 | import { type ReactNode, createContext, useContext } from 'react' 3 | type DefaulSession = { 4 | id: string 5 | email: string 6 | firstName: string 7 | lastName: string 8 | profileImage: string 9 | isVerified: boolean 10 | } 11 | export interface Session { 12 | signIn: (session: DefaulSession) => void 13 | signOut: () => void 14 | session?: DefaulSession | null 15 | isLoading: boolean 16 | } 17 | 18 | const AuthContext = createContext({ 19 | signIn: () => Promise, 20 | signOut: () => null, 21 | session: null, 22 | isLoading: false, 23 | }) 24 | 25 | // This hook can be used to access the user info. 26 | export function useSession() { 27 | const value = useContext(AuthContext) 28 | if (process.env.NODE_ENV !== 'production') { 29 | if (!value) { 30 | throw new Error('useSession must be wrapped in a ') 31 | } 32 | } 33 | return value as Session 34 | } 35 | 36 | export function SessionProvider({ children }: { children: ReactNode }) { 37 | const [state, setState] = useStorageState('session') 38 | const [isLoading, session] = state 39 | const signIn = (sessionData: DefaulSession) => { 40 | setState(sessionData) 41 | } 42 | 43 | const signOut = () => { 44 | setState(null) 45 | } 46 | return ( 47 | 55 | {children} 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/app/+html.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollViewStyleReset } from 'expo-router/html' 2 | import type { PropsWithChildren } from 'react' 3 | 4 | /** 5 | * This file is web-only and used to configure the root HTML for every web page during static rendering. 6 | * The contents of this function only run in Node.js environments and do not have access to the DOM or browser APIs. 7 | */ 8 | export default function Root({ children }: PropsWithChildren) { 9 | return ( 10 | 11 | 12 | 13 | 14 | 18 | 19 | {/* 20 | Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. 21 | However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. 22 | */} 23 | 24 | 25 | {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} 26 |