chore: initial commit

This commit is contained in:
Théo LUDWIG 2024-02-16 22:51:50 +01:00
commit edd662ce44
Signed by: theoludwig
GPG Key ID: ADFE5A563D718F3B
46 changed files with 21414 additions and 0 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
# https://editorconfig.org/
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

43
.eslintrc.json Normal file
View File

@ -0,0 +1,43 @@
{
"extends": [
"conventions",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"ignorePatterns": ["jest.setup.ts"],
"plugins": ["prettier"],
"env": {
"browser": true,
"node": true,
"jest": true
},
"settings": {
"react": {
"version": "detect"
}
},
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"prettier/prettier": "error",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"react/self-closing-comp": [
"error",
{
"component": true,
"html": true
}
],
"react/void-dom-elements-no-children": "error",
"react/jsx-boolean-value": "error"
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
}
]
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

45
.gitignore vendored Normal file
View File

@ -0,0 +1,45 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
.npm/
# Expo
.expo/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
# tests
coverage/
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
# The following patterns were generated by expo-cli
expo-env.d.ts
# @end expo-cli

1
.npmrc Normal file
View File

@ -0,0 +1 @@
save-exact=true

3
.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"semi": false
}

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint"
]
}

14
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,14 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.bracketPairColorization.enabled": true,
"prettier.configPath": ".prettierrc.json",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"eslint.options": {
"ignorePath": ".gitignore"
},
"prettier.ignorePath": ".gitignore"
}

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# P61 - Projet
## À propos
Application mobile en React Native pour le projet du module P61 Développement avancé.
[Sujet](./SUJET.md)
## Membres du Groupe 7
- [Théo LUDWIG](https://git.unistra.fr/t.ludwig)
- [Haoxuan LI](https://git.unistra.fr/haoxuan.li)
- [Maxime RUMPLER](https://git.unistra.fr/m.rumpler)
- [Maxime RICHARD](https://git.unistra.fr/maximerichard)
## Prérequis
- [Node.js](https://nodejs.org/) >= 20.0.0
- [npm](https://www.npmjs.com/) >= 10.0.0
- [Expo Go](https://expo.io/client)
## Installation
```sh
npm clean-install
```
## Usage
```sh
# Run the application
npm run start
# Lint and Test
npm run lint:prettier
npm run lint:eslint
npm run lint:typescript
npm run test
```

1
SUJET.md Normal file
View File

@ -0,0 +1 @@
# Sujet

35
app.json Normal file
View File

@ -0,0 +1,35 @@
{
"expo": {
"name": "p61-project",
"slug": "p61-project",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splashscreen.jpg",
"resizeMode": "cover",
"backgroundColor": "#74b6cb"
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": ["expo-router"],
"experiments": {
"typedRoutes": true
}
}
}

View File

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<HomePage /> renders correctly 1`] = `
<RNCSafeAreaView
edges={
{
"bottom": "additive",
"left": "additive",
"right": "additive",
"top": "additive",
}
}
style={
{
"alignItems": "center",
"flex": 1,
"justifyContent": "center",
}
}
>
<Text
style={
{
"color": "#006CFF",
"fontFamily": "Georama",
"fontSize": 36,
"marginVertical": 20,
}
}
>
P61 Project
</Text>
</RNCSafeAreaView>
`;

View File

@ -0,0 +1,14 @@
import renderer from "react-test-renderer"
import HomePage from "@/app/(pages)/index"
describe("<HomePage />", () => {
beforeEach(async () => {
jest.clearAllMocks()
})
it("renders correctly", () => {
const tree = renderer.create(<HomePage />).toJSON()
expect(tree).toMatchSnapshot()
})
})

37
app/(pages)/_layout.tsx Normal file
View File

@ -0,0 +1,37 @@
import FontAwesome from "@expo/vector-icons/FontAwesome"
import { Tabs } from "expo-router"
import React from "react"
/**
* @see https://icons.expo.fyi/
* @param props
* @returns
*/
const TabBarIcon: React.FC<{
name: React.ComponentProps<typeof FontAwesome>["name"]
color: string
}> = (props) => {
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />
}
const TabLayout: React.FC = () => {
return (
<Tabs
screenOptions={{
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => {
return <TabBarIcon name="home" color={color} />
},
}}
/>
</Tabs>
)
}
export default TabLayout

29
app/(pages)/index.tsx Normal file
View File

@ -0,0 +1,29 @@
import { StatusBar } from "expo-status-bar"
import { StyleSheet, Text } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
const HomePage: React.FC = () => {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.title}>P61 Project</Text>
<StatusBar style="auto" />
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
title: {
fontFamily: "Canterbury",
fontSize: 36,
color: "#006CFF",
marginVertical: 20,
},
})
export default HomePage

19
app/+html.tsx Normal file
View File

@ -0,0 +1,19 @@
import { ScrollViewStyleReset } from "expo-router/html"
const Root: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<ScrollViewStyleReset />
</head>
<body>{children}</body>
</html>
)
}
export default Root

56
app/_layout.tsx Normal file
View File

@ -0,0 +1,56 @@
import { useFonts } from "expo-font"
import { Stack } from "expo-router"
import * as SplashScreen from "expo-splash-screen"
import { useEffect } from "react"
import CanterburyFont from "../assets/fonts/Canterbury.ttf"
import GeoramFont from "../assets/fonts/Georama-Black.ttf"
import SpaceMonoFont from "../assets/fonts/SpaceMono-Regular.ttf"
export { ErrorBoundary } from "expo-router"
export const unstableSettings = {
initialRouteName: "index",
}
SplashScreen.preventAutoHideAsync().catch((error) => {
console.error(error)
})
const RootLayout: React.FC = () => {
const [loaded, error] = useFonts({
Georama: GeoramFont,
SpaceMono: SpaceMonoFont,
Canterbury: CanterburyFont,
})
useEffect(() => {
if (error != null) {
throw error
}
}, [error])
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync().catch((error) => {
console.error(error)
})
}
}, [loaded])
if (!loaded) {
return null
}
return (
<Stack
screenOptions={{
headerShown: false,
}}
>
<Stack.Screen name="(pages)" />
</Stack>
)
}
export default RootLayout

BIN
assets/fonts/Canterbury.ttf Executable file

Binary file not shown.

BIN
assets/fonts/Georama-Black.ttf Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/images/splash.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
assets/images/splashscreen.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true)
return {
presets: ["babel-preset-expo"],
}
}

View File

@ -0,0 +1,34 @@
import type { StyleProp, ViewStyle } from "react-native"
import { Pressable, StyleSheet, Text } from "react-native"
export interface ButtonCustomProps
extends React.ComponentProps<typeof Pressable> {
children: React.ReactNode
style?: StyleProp<ViewStyle>
}
export const ButtonCustom: React.FC<ButtonCustomProps> = (props) => {
const { children, style, ...rest } = props
return (
<Pressable style={[styles.button, style]} {...rest}>
<Text style={styles.text}>{children}</Text>
</Pressable>
)
}
const styles = StyleSheet.create({
button: {
padding: 10,
borderWidth: 1,
borderColor: "black",
borderRadius: 10,
marginTop: 10,
backgroundColor: "#152B5D",
},
text: {
color: "white",
textAlign: "center",
fontSize: 15,
},
})

View File

@ -0,0 +1,27 @@
import { Link } from "expo-router"
import * as WebBrowser from "expo-web-browser"
import { Platform } from "react-native"
type LinkProps = React.ComponentProps<typeof Link>
export const ExternalLink: React.FC<
Omit<LinkProps, "href"> & {
href: string
}
> = (props) => {
const { href, ...rest } = props
return (
<Link
target="_blank"
href={href as unknown as LinkProps["href"]}
{...rest}
onPress={async (event) => {
if (Platform.OS !== "web") {
event.preventDefault()
await WebBrowser.openBrowserAsync(href)
}
}}
/>
)
}

13
components/MonoText.tsx Normal file
View File

@ -0,0 +1,13 @@
import { StyleSheet, Text } from "react-native"
export const MonoText: React.FC<Text["props"]> = (props) => {
const { style, ...rest } = props
return <Text style={[style, styles.text]} {...rest} />
}
const styles = StyleSheet.create({
text: {
fontFamily: "SpaceMono",
},
})

View File

@ -0,0 +1,12 @@
import renderer from "react-test-renderer"
import { ButtonCustom } from "@/components/ButtonCustom"
describe("<ButtonCustom />", () => {
it("renders correctly", () => {
const tree = renderer
.create(<ButtonCustom>Awesome Button!</ButtonCustom>)
.toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,16 @@
import renderer from "react-test-renderer"
import { ExternalLink } from "@/components/ExternalLink"
describe("<ExternalLink />", () => {
it("renders correctly", () => {
const tree = renderer
.create(
<ExternalLink href="https://www.unistra.fr/">
Awesome Link!
</ExternalLink>,
)
.toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,10 @@
import renderer from "react-test-renderer"
import { MonoText } from "@/components/MonoText"
describe("<MonoText />", () => {
it("renders correctly", () => {
const tree = renderer.create(<MonoText>Awesome text!</MonoText>).toJSON()
expect(tree).toMatchSnapshot()
})
})

View File

@ -0,0 +1,60 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ButtonCustom /> renders correctly 1`] = `
<View
accessibilityState={
{
"busy": undefined,
"checked": undefined,
"disabled": undefined,
"expanded": undefined,
"selected": undefined,
}
}
accessibilityValue={
{
"max": undefined,
"min": undefined,
"now": undefined,
"text": undefined,
}
}
accessible={true}
collapsable={false}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
[
{
"backgroundColor": "#152B5D",
"borderColor": "black",
"borderRadius": 10,
"borderWidth": 1,
"marginTop": 10,
"padding": 10,
},
undefined,
]
}
>
<Text
style={
{
"color": "white",
"fontSize": 15,
"textAlign": "center",
}
}
>
Awesome Button!
</Text>
</View>
`;

View File

@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ExternalLink /> renders correctly 1`] = `
<Text
href="https://www.unistra.fr/"
onPress={[Function]}
role="link"
>
Awesome Link!
</Text>
`;

View File

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<MonoText /> renders correctly 1`] = `
<Text
style={
[
undefined,
{
"fontFamily": "SpaceMono",
},
]
}
>
Awesome text!
</Text>
`;

View File

@ -0,0 +1,61 @@
import { act, renderHook } from "@testing-library/react-native"
import { useBoolean } from "@/hooks/useBoolean"
describe("hooks/useBoolean", () => {
beforeEach(() => {
jest.clearAllMocks()
})
const initialValues = [true, false]
for (const initialValue of initialValues) {
it(`should set the initial value to ${initialValue}`, () => {
const { result } = renderHook(() => {
return useBoolean({ initialValue })
})
expect(result.current.value).toBe(initialValue)
})
}
it("should by default set the initial value to false", () => {
const { result } = renderHook(() => {
return useBoolean()
})
expect(result.current.value).toBe(false)
})
it("should toggle the value", async () => {
const { result } = renderHook(() => {
return useBoolean({ initialValue: false })
})
await act(() => {
return result.current.toggle()
})
expect(result.current.value).toBe(true)
await act(() => {
return result.current.toggle()
})
expect(result.current.value).toBe(false)
})
it("should set the value to true", async () => {
const { result } = renderHook(() => {
return useBoolean({ initialValue: false })
})
await act(() => {
return result.current.setTrue()
})
expect(result.current.value).toBe(true)
})
it("should set the value to false", async () => {
const { result } = renderHook(() => {
return useBoolean({ initialValue: true })
})
await act(() => {
return result.current.setFalse()
})
expect(result.current.value).toBe(false)
})
})

View File

@ -0,0 +1,93 @@
import { act, renderHook, waitFor } from "@testing-library/react-native"
import AsyncStorage from "@react-native-async-storage/async-storage"
import { useLocalStorage } from "@/hooks/useLocalStorage"
describe("hooks/useLocalStorage", () => {
beforeEach(async () => {
jest.clearAllMocks()
await AsyncStorage.clear()
})
it("should get the default value", () => {
const key = "key"
const givenDefaultValue = { key: "value" }
const { result } = renderHook(() => {
return useLocalStorage(key, givenDefaultValue)
})
const [actualValue] = result.current
expect(actualValue).toEqual(givenDefaultValue)
})
it("should get the value from the storage", async () => {
const key = "key"
const givenDefaultValue = { someValue: "value" }
const storedValue = { someValue: "value" }
const { result } = renderHook(() => {
return useLocalStorage(key, givenDefaultValue)
})
await waitFor(() => {
expect(AsyncStorage.getItem).toHaveBeenCalledWith(key)
})
const [actualValue] = result.current
expect(actualValue).toEqual(storedValue)
})
it("should set the value to the storage", async () => {
const key = "key"
const givenDefaultValue = { someValue: "value" }
const storedValue = { someValue: "value" }
const { result } = renderHook(() => {
return useLocalStorage(key, givenDefaultValue)
})
const [, setValue] = result.current
await act(() => {
setValue(storedValue)
})
await waitFor(() => {
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
key,
JSON.stringify(storedValue),
)
})
})
it("should get default value if storage value is not valid JSON", async () => {
console.error = jest.fn()
const key = "key"
const givenDefaultValue = { someValue: "value" }
const storedValue = "{not valid JSON"
await AsyncStorage.setItem(key, storedValue)
const { result } = renderHook(() => {
return useLocalStorage(key, givenDefaultValue)
})
await waitFor(() => {
expect(AsyncStorage.getItem).toHaveBeenCalledWith(key)
})
const [actualValue] = result.current
expect(actualValue).toEqual(givenDefaultValue)
})
it("should catch the error when setting the value to the storage", async () => {
console.error = jest.fn()
const key = "key"
const givenDefaultValue = { someValue: "value" }
const storedValue = { someValue: "value" }
const error = new Error("error")
;(AsyncStorage.setItem as jest.Mock).mockRejectedValue(error)
const { result } = renderHook(() => {
return useLocalStorage(key, givenDefaultValue)
})
const [, setValue] = result.current
await act(() => {
setValue(storedValue)
})
await waitFor(() => {
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
key,
JSON.stringify(storedValue),
)
})
expect(console.error).toHaveBeenCalledWith(error)
})
})

50
hooks/useBoolean.ts Normal file
View File

@ -0,0 +1,50 @@
import { useState } from "react"
export interface UseBooleanResult {
value: boolean
toggle: () => void
setTrue: () => void
setFalse: () => void
}
export interface UseBooleanOptions {
/**
* The initial value of the boolean.
* @default false
*/
initialValue?: boolean
}
/**
* Hook to manage a boolean state.
* @param options
* @returns
*/
export const useBoolean = (
options: UseBooleanOptions = {},
): UseBooleanResult => {
const { initialValue = false } = options
const [value, setValue] = useState(initialValue)
const toggle = (): void => {
setValue((old) => {
return !old
})
}
const setTrue = (): void => {
setValue(true)
}
const setFalse = (): void => {
setValue(false)
}
return {
value,
toggle,
setTrue,
setFalse,
}
}

43
hooks/useLocalStorage.ts Normal file
View File

@ -0,0 +1,43 @@
import { useCallback, useEffect, useRef, useState } from "react"
import AsyncStorage from "@react-native-async-storage/async-storage"
type UseLocalStorageResult<T> = [T, React.Dispatch<React.SetStateAction<T>>]
export const useLocalStorage = <T extends unknown>(
key: string,
defaultValue: T,
): UseLocalStorageResult<T> => {
const hasSaved = useRef(false)
const [value, setValue] = useState<T>(defaultValue)
const getFromLocalStorage = useCallback(async (): Promise<T> => {
const value = await AsyncStorage.getItem(key)
hasSaved.current = true
if (value == null) {
return defaultValue
}
return JSON.parse(value) as T
}, [key, defaultValue])
useEffect(() => {
if (!hasSaved.current) {
return
}
AsyncStorage.setItem(key, JSON.stringify(value)).catch((error) => {
console.error(error)
})
}, [key, value])
useEffect(() => {
getFromLocalStorage()
.then((value) => {
setValue(value)
})
.catch((error) => {
setValue(defaultValue)
console.error(error)
})
}, [defaultValue, getFromLocalStorage])
return [value, setValue]
}

19
jest.config.json Normal file
View File

@ -0,0 +1,19 @@
{
"preset": "jest-expo",
"roots": ["./"],
"setupFilesAfterEnv": ["<rootDir>/tests/jest.setup.ts"],
"fakeTimers": {
"enableGlobally": true
},
"collectCoverage": true,
"coverageDirectory": "coverage",
"coverageReporters": ["text", "text-summary", "cobertura"],
"collectCoverageFrom": [
"<rootDir>/**/*.{ts,tsx}",
"!<rootDir>/components/ExternalLink.tsx",
"!<rootDir>/.expo",
"!<rootDir>/app/+html.tsx",
"!<rootDir>/app/**/_layout.tsx",
"!**/*.d.ts"
]
}

20466
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "p61-project",
"private": true,
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"lint:prettier": "prettier . --check",
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
"lint:typescript": "tsc --noEmit",
"test": "jest"
},
"dependencies": {
"@expo/vector-icons": "14.0.0",
"@react-native-async-storage/async-storage": "1.21.0",
"@react-navigation/native": "6.1.10",
"expo": "50.0.6",
"expo-font": "11.10.2",
"expo-linking": "6.2.2",
"expo-router": "3.4.7",
"expo-splash-screen": "0.26.4",
"expo-status-bar": "1.11.1",
"expo-system-ui": "2.9.3",
"expo-web-browser": "12.8.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.73.4",
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "3.29.0",
"react-native-web": "0.19.10"
},
"devDependencies": {
"@babel/core": "7.23.9",
"@testing-library/react-native": "12.4.3",
"@total-typescript/ts-reset": "0.5.1",
"@tsconfig/strictest": "2.0.3",
"@types/jest": "29.5.12",
"@types/react": "18.2.55",
"@types/react-test-renderer": "18.0.7",
"@typescript-eslint/eslint-plugin": "7.0.1",
"@typescript-eslint/parser": "7.0.1",
"eslint": "8.56.0",
"eslint-config-conventions": "14.1.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-react-hooks": "4.6.0",
"eslint-plugin-react-native": "4.1.0",
"eslint-plugin-unicorn": "51.0.1",
"jest": "29.7.0",
"jest-expo": "50.0.2",
"react-test-renderer": "18.2.0",
"typescript": "5.3.3"
}
}

5
react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module "*.png"
declare module "*.svg"
declare module "*.jpeg"
declare module "*.jpg"
declare module "*.ttf"

5
tests/jest.setup.ts Normal file
View File

@ -0,0 +1,5 @@
import "@testing-library/react-native/extend-expect"
jest.mock("@react-native-async-storage/async-storage", () => {
return require("@react-native-async-storage/async-storage/jest/async-storage-mock")
})

18
tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": ["@tsconfig/strictest/tsconfig.json", "expo/tsconfig.base"],
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": false,
"allowJs": false,
"checkJs": false,
"types": [
"@total-typescript/ts-reset",
"jest",
"@testing-library/react-native/extend-expect"
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}