diff --git a/package-lock.json b/package-lock.json index 32e757a..d27bd94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "@testing-library/react-native": "^12.3.0", "@types/jest": "^29.5.12", "@types/react": "18.2", + "@types/react-native-confetti": "^0.1.4", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", @@ -62,7 +63,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-unused-imports": "^3.2.0", "prettier": "^3.3.2", - "typescript": "^5.5.3" + "typescript": "~5.3.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -8097,6 +8098,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-native-confetti": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/react-native-confetti/-/react-native-confetti-0.1.4.tgz", + "integrity": "sha512-StgW1o0dw8ZLCVZ7v6ZVOBu+RYT+PFjtQAhF9k7uLn/avor4UtKhB9cDrF377+JfRw0aHeo3gg9dawZNzM/jOg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -19617,9 +19627,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index e69c5c5..7633496 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@testing-library/react-native": "^12.3.0", "@types/jest": "^29.5.12", "@types/react": "18.2", + "@types/react-native-confetti": "^0.1.4", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", @@ -66,7 +67,7 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-unused-imports": "^3.2.0", "prettier": "^3.3.2", - "typescript": "^5.5.3" + "typescript": "~5.3.3" }, "private": true } diff --git a/src/App.tsx b/src/App.tsx index 2065aad..5422c8b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,7 +31,7 @@ export default function App() { {/* FIXME: Read urlScheme and merchantIdentifier from .Config */} diff --git a/src/Dusken.tsx b/src/Dusken.tsx index cd766f9..79ee5cf 100644 --- a/src/Dusken.tsx +++ b/src/Dusken.tsx @@ -1,22 +1,20 @@ import { NativeBaseProvider } from 'native-base'; import React, { useEffect } from 'react'; -import { connect } from 'react-redux'; import Navigation from './Navigation'; -import { logout as logoutAction, registerUserSuccess as registerUserSuccessAction } from './actions'; -import { migrateReduxPersistFourToFive } from './utils'; +import { logout } from './actions'; +import { useAppDispatch, useAppSelector } from './hooks'; + +const Dusken = () => { + const user = useAppSelector((store) => store.user); + const userToken = useAppSelector((store) => store.userToken); + const dispatch = useAppDispatch(); -const DuskenContainer = ({ registerUserSuccess, userToken, user, logout }) => { useEffect(() => { - async function inner() { - await migrateReduxPersistFourToFive(registerUserSuccess); - - // logout authenticated users without token - if (user && !userToken) { - logout(); - } + // logout authenticated users without token + if (user && !userToken) { + dispatch(logout()); } - inner(); }); return ( @@ -26,13 +24,4 @@ const DuskenContainer = ({ registerUserSuccess, userToken, user, logout }) => { ); }; -const mapStateToProps = (state) => ({ - userToken: state.userToken, - user: state.user, -}); -const mapDispatchToProps = { - registerUserSuccess: registerUserSuccessAction, - logout: logoutAction, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DuskenContainer); +export default Dusken; diff --git a/src/Navigation.tsx b/src/Navigation.tsx index e9a989d..3453db2 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1,5 +1,5 @@ import { SimpleLineIcons as Icon } from '@expo/vector-icons'; -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { BottomTabNavigationOptions, createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import React from 'react'; @@ -57,10 +57,18 @@ const UserStackScreen = () => ( ); +type TabBarIconType = BottomTabNavigationOptions['tabBarIcon']; + const HomeTabs = createBottomTabNavigator(); -const EventIcon = ({ color, size }) => ; -const UserIcon = ({ color, size }) => ; -const AboutIcon = ({ color, size }) => ; +const EventIcon: TabBarIconType = ({ color, size }) => ( + +); +const UserIcon: TabBarIconType = ({ color, size }) => ( + +); +const AboutIcon: TabBarIconType = ({ color, size }) => ( + +); const Navigation = () => ( diff --git a/src/actions.ts b/src/actions.ts index 6e75a52..e543434 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -18,7 +18,7 @@ function loginRequest() { }; } -function loginSuccess(username, token) { +function loginSuccess(username: string, token: string) { return { type: LOGIN_SUCCESS, username, diff --git a/src/api.ts b/src/api.ts index 0d39da6..0adf7a7 100644 --- a/src/api.ts +++ b/src/api.ts @@ -13,10 +13,11 @@ import { membershipChargeSuccess, } from './actions'; import type { UserRegisterFormValues } from './modules/register/useUserRegister'; +import { AppDispatch } from './reduxStore'; import { fetchWithTimeout } from './utils'; -export function requestLogin(username, password) { - return (dispatch) => { +export function requestLogin(username: string, password: string) { + return (dispatch: AppDispatch) => { // We are now logging in dispatch(loginRequest()); @@ -50,8 +51,8 @@ export function requestLogin(username, password) { }; } -export function requestUserData(authToken) { - return (dispatch) => { +export function requestUserData(authToken: string) { + return (dispatch: AppDispatch) => { // We are now fetching user data dispatch(userDataRequest()); @@ -82,7 +83,7 @@ export function requestUserData(authToken) { } export function requestRegisterUser({ firstName, lastName, email, phoneNumber, password }: UserRegisterFormValues) { - return (dispatch) => { + return (dispatch: AppDispatch) => { dispatch(registerUserRequest()); return fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/user/register/`, { @@ -117,8 +118,8 @@ export function requestRegisterUser({ firstName, lastName, email, phoneNumber, p }; } -export function requestMembershipCharge(authToken, stripeToken, membershipType) { - return (dispatch) => { +export function requestMembershipCharge(authToken: string, stripeToken: string, membershipType: string) { + return (dispatch: AppDispatch) => { dispatch(membershipChargeRequest()); return fetchWithTimeout(`${process.env.EXPO_PUBLIC_DUSKEN_API_URL}/api/membership/charge/`, { @@ -153,7 +154,7 @@ export function requestMembershipCharge(authToken, stripeToken, membershipType) } /** Create a stripe payment sheet. Note that this is not using redux */ -export async function requestStripePaymentSheet(authToken, membershipType) { +export async function requestStripePaymentSheet(authToken: string, membershipType: string) { const body = JSON.stringify({ membership_type: membershipType, }); diff --git a/src/modules/events/EventDetail.tsx b/src/modules/events/EventDetail.tsx index cf78ba9..db5f8e6 100644 --- a/src/modules/events/EventDetail.tsx +++ b/src/modules/events/EventDetail.tsx @@ -15,6 +15,7 @@ import { import RenderHtml from 'react-native-render-html'; import theme from 'src/theme'; +import type { EventType } from 'src/types/data'; import type { EventDetailProps } from 'src/types/navigation'; moment.locale('nb'); @@ -27,11 +28,11 @@ const FORMAT_DAY = 'DD.'; const FORMAT_LOCAL_DAY = 'dddd'; const FORMAT_YEAR = 'YYYY'; -const formatRelative = (time) => moment(time).fromNow(); +const formatRelative = (time: string) => moment(time).fromNow(); -const format = (time, formatString = FORMAT_TIME) => moment(time).format(formatString); +const format = (time: string, formatString = FORMAT_TIME) => moment(time).format(formatString); -const formatTicketText = (event) => { +const formatTicketText = (event: EventType) => { /* FIXME: This should be handled by API */ let regularPrice = ''; let memberPrice = ''; @@ -61,7 +62,7 @@ const formatTicketText = (event) => { return text; }; -const onFacebookUrlPress = (link) => { +const onFacebookUrlPress = (link: string) => { const fbEventPattern = /https?:\/\/www\.facebook\.com\/events\/(\d+)\/?/i; const matches = fbEventPattern.exec(link); let iosURL = link; @@ -82,15 +83,16 @@ const onFacebookUrlPress = (link) => { .catch((err) => console.error('An error occurred', err)); }; -const onLocationPress = (venue) => { +const onLocationPress = (venue: string) => { if (venue !== 'Annetsteds') { Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${CHATEAU_NEUF_ADDRESS}`); } }; -const VenueLocation = ({ venue }) => (venue !== 'Annetsteds' ? CHATEAU_NEUF_ADDRESS : 'Ukjent adresse'); +const VenueLocation = ({ venue }: Pick) => + venue !== 'Annetsteds' ? CHATEAU_NEUF_ADDRESS : 'Ukjent adresse'; -const PriceAndTicket = ({ event }) => { +const PriceAndTicket = ({ event }: { event: EventType }) => { const text = formatTicketText(event); if (text === '') { @@ -124,7 +126,7 @@ const PriceAndTicket = ({ event }) => { ); }; -const FaceButton = ({ event }) => { +const FaceButton = ({ event }: { event: EventType }) => { if (!event.facebook_url) { return null; } @@ -141,14 +143,14 @@ const FaceButton = ({ event }) => { ); }; -const Year = ({ startTime }) => { +const Year = ({ startTime }: { startTime: string }) => { if (moment().year() === moment(startTime).year()) { return null; } return {format(startTime, FORMAT_YEAR)}; }; -const formatTimeFull = (event) => { +const formatTimeFull = (event: EventType) => { const endTime = event.end_time ? ` - ${format(event.end_time, FORMAT_TIME)}` : ''; const startTime = `${format(event.start_time, FORMAT_LOCAL_DAY)} kl. ${format(event.start_time, FORMAT_TIME)}`; return `${startTime}${endTime}`; diff --git a/src/modules/login/Login.tsx b/src/modules/login/Login.tsx index 01a0003..e3bafa6 100644 --- a/src/modules/login/Login.tsx +++ b/src/modules/login/Login.tsx @@ -41,7 +41,7 @@ const Login = () => { small light onPress={() => { - Linking.openURL(process.env.EXPO_PUBLIC_DUSKEN_FORGOT_PASSWORD_URL); + Linking.openURL(process.env.EXPO_PUBLIC_DUSKEN_FORGOT_PASSWORD_URL || ''); }} style={styles.forgotPasswordButton} > diff --git a/src/modules/membership/Membership.tsx b/src/modules/membership/Membership.tsx index 98ff525..2851941 100644 --- a/src/modules/membership/Membership.tsx +++ b/src/modules/membership/Membership.tsx @@ -8,7 +8,7 @@ import Proof from './Proof'; import useMembership from './useMembership'; const Membership = () => { - const { isFetchingUserData, onRegisterPress, onLoginPress, onLogoutPress, user, isAuthenticated, fetchUser } = + const { isFetchingUserData, onRegisterPress, onLoginPress, onLogoutPress, isAuthenticated, fetchUser } = useMembership(); if (isFetchingUserData) { @@ -20,9 +20,7 @@ const Membership = () => { } if (isAuthenticated) { - return ( - - ); + return ; } return ; diff --git a/src/modules/membership/MembershipInfo.tsx b/src/modules/membership/MembershipInfo.tsx index af54ab6..3c2b348 100644 --- a/src/modules/membership/MembershipInfo.tsx +++ b/src/modules/membership/MembershipInfo.tsx @@ -4,7 +4,11 @@ import { Button } from 'react-native-ui-lib'; import theme from 'src/theme'; -const MembershipInfo = ({ onRegister, onLogin }) => ( +interface MembershipInfoProps { + onRegister: () => void; + onLogin: () => void; +} +const MembershipInfo = ({ onRegister, onLogin }: MembershipInfoProps) => ( Hei 👋 diff --git a/src/modules/membership/Proof.tsx b/src/modules/membership/Proof.tsx index df5bfb9..0fc615c 100644 --- a/src/modules/membership/Proof.tsx +++ b/src/modules/membership/Proof.tsx @@ -9,18 +9,20 @@ import { RefreshControl, ActivityIndicator, Dimensions, + GestureResponderEvent, } from 'react-native'; import Confetti from 'react-native-confetti'; import { Button, Incubator } from 'react-native-ui-lib'; import theme from 'src/theme'; +import type { UserType } from 'src/types/data'; import useProof from './useProof'; import logo from '../../../assets/DNS-logo-svart.png'; const { Toast } = Incubator; -const MembershipValidTo = ({ user }) => { +const MembershipValidTo = ({ user }: { user: UserType }) => { if (!user.last_membership) { return null; } @@ -38,7 +40,13 @@ const MembershipValidTo = ({ user }) => { ); }; -const MembershipStatus = ({ user, onMembershipPress }) => { +const MembershipStatus = ({ + user, + onMembershipPress, +}: { + user: UserType; + onMembershipPress: (event: GestureResponderEvent) => void | undefined; +}) => { if (!user.is_member) { if (!user.last_membership) { return null; @@ -62,7 +70,21 @@ const MembershipStatus = ({ user, onMembershipPress }) => { ); }; -const PurchaseButton = ({ user, paymentLoading, isLoadingMembershipType, onChargePress, membershipPrice }) => { +interface PurchaseButtonProps { + user: UserType; + paymentLoading: boolean; + isLoadingMembershipType: boolean; + membershipPrice: number; + onChargePress: () => void; +} + +const PurchaseButton = ({ + user, + paymentLoading, + isLoadingMembershipType, + onChargePress, + membershipPrice, +}: PurchaseButtonProps) => { if (user.is_member) { return null; } @@ -85,7 +107,8 @@ const PurchaseButton = ({ user, paymentLoading, isLoadingMembershipType, onCharg ); }; -const UserConfetti = ({ user, confettiRef }) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const UserConfetti = ({ user, confettiRef }: { user: UserType; confettiRef: any }) => { if (!user.is_member) { return null; } @@ -93,7 +116,7 @@ const UserConfetti = ({ user, confettiRef }) => { // Confetti!!!! return ; }; -const Logo = ({ user }) => { +const Logo = ({ user }: { user: UserType }) => { if (!user.is_member) { return null; } @@ -117,7 +140,7 @@ const ChargeError = ({ chargeError }) => { ); }; -const MembershipName = ({ user }) => { +const MembershipName = ({ user }: { user: UserType }) => { let name = `${user.first_name} ${user.last_name}`; if (name === ' ') { name = user.email; @@ -129,14 +152,26 @@ const MembershipName = ({ user }) => { ); }; -const Proof = ({ onLogoutPress, user, fetchUser, isFetchingUserData }) => { +interface ProofProps { + onLogoutPress: () => void; + fetchUser: () => void; + isFetchingUserData: boolean; +} +const Proof = ({ onLogoutPress, fetchUser, isFetchingUserData }: ProofProps) => { const CONFETTI_TIMEOUT = 15000; - const confettiRef = useRef(null); - const { chargeError, paymentLoading, isLoadingMembershipType, onChargePress, membershipPrice, toast, setToast } = - useProof({ - user, - fetchUser, - }); + const confettiRef = useRef(null); + const { + chargeError, + paymentLoading, + isLoadingMembershipType, + onChargePress, + membershipPrice, + toast, + setToast, + user, + } = useProof({ + fetchUser, + }); useEffect(() => { const currentRef = confettiRef.current; diff --git a/src/modules/membership/useMembership.tsx b/src/modules/membership/useMembership.tsx index 850a6ff..1497ca2 100644 --- a/src/modules/membership/useMembership.tsx +++ b/src/modules/membership/useMembership.tsx @@ -11,7 +11,6 @@ const useMembership = () => { const isAuthenticated = useAppSelector((store) => store.isAuthenticated); const isFetchingUserData = useAppSelector((store) => store.isFetchingUserData); const lastOrder = useAppSelector((store) => store.lastOrder); - const user = useAppSelector((store) => store.user); const userToken = useAppSelector((store) => store.userToken); const prevUserToken = usePrevious(userToken); const prevLastOrder = usePrevious(lastOrder); @@ -39,7 +38,6 @@ const useMembership = () => { isAuthenticated, isFetchingUserData, userToken, - user, lastOrder, onLogoutPress, onRegisterPress, diff --git a/src/modules/membership/useProof.tsx b/src/modules/membership/useProof.tsx index d3376d0..45f49a5 100644 --- a/src/modules/membership/useProof.tsx +++ b/src/modules/membership/useProof.tsx @@ -2,10 +2,9 @@ import { gql, useQuery } from '@apollo/client'; import { useStripe } from '@stripe/stripe-react-native'; import Constants from 'expo-constants'; import { useEffect, useState } from 'react'; -import { useSelector } from 'react-redux'; import { requestStripePaymentSheet } from 'src/api'; -import { RootState } from 'src/reduxStore'; +import { useAppSelector } from 'src/hooks'; const PROOF_QUERY = gql` { @@ -16,22 +15,21 @@ const PROOF_QUERY = gql` } `; -function getPrice(membershipTypes) { - if (!membershipTypes) { - return 0; - } - - return membershipTypes[0].price / 100; +interface ToastProps { + message: string; + visible: boolean; + preset: 'failure' | 'success'; } -const useProof = ({ user, fetchUser }) => { - const userToken = useSelector((state: RootState) => state.userToken); - const chargeError = useSelector((state: RootState) => state.chargeError); +const useProof = ({ fetchUser }) => { + const user = useAppSelector((state) => state.user); + const userToken = useAppSelector((state) => state.userToken); + const chargeError = useAppSelector((state) => state.chargeError); const { data, loading } = useQuery(PROOF_QUERY); const { initPaymentSheet, presentPaymentSheet } = useStripe(); const [paymentLoading, setPaymentLoading] = useState(false); const membershipTypeSlug = data?.membershipTypes?.[0]?.slug ?? 'standard'; - const [toast, setToast] = useState(null); + const [toast, setToast] = useState(null); const openPaymentSheet = async () => { try { @@ -48,7 +46,10 @@ const useProof = ({ user, fetchUser }) => { const initializePaymentSheet = async () => { setPaymentLoading(true); - const { paymentIntent, ephemeralKey, customer } = await requestStripePaymentSheet(userToken, membershipTypeSlug); + const { paymentIntent, ephemeralKey, customer } = await requestStripePaymentSheet( + userToken as string, + membershipTypeSlug, + ); const { error } = await initPaymentSheet({ merchantDisplayName: 'Det Norske Studentersamfund', @@ -56,13 +57,13 @@ const useProof = ({ user, fetchUser }) => { customerEphemeralKeySecret: ephemeralKey, paymentIntentClientSecret: paymentIntent, defaultBillingDetails: { - email: user.email, - name: `${user.first_name} ${user.last_name}`, + email: user?.email, + name: `${user?.first_name} ${user?.last_name}`, address: { country: 'NO', }, }, - returnURL: `${Constants.expoConfig.scheme}://stripe-redirect`, + returnURL: `${Constants.expoConfig?.scheme}://stripe-redirect`, }); if (!error) { setPaymentLoading(true); @@ -78,14 +79,16 @@ const useProof = ({ user, fetchUser }) => { } }, [fetchUser, user?.last_membership]); + const membershipPrice = data?.membershipTypes?.[0]?.price; return { chargeError: chargeError || '', isLoadingMembershipType: loading, - membershipPrice: getPrice(data?.membershipTypes), + membershipPrice: membershipPrice ? membershipPrice / 100 : 0, onChargePress: openPaymentSheet, paymentLoading, toast, setToast, + user, }; }; diff --git a/src/reducers.ts b/src/reducers.ts index be0f77f..e7674b4 100644 --- a/src/reducers.ts +++ b/src/reducers.ts @@ -13,10 +13,26 @@ import { MEMBERSHIP_CHARGE_FAILURE, MEMBERSHIP_CHARGE_SUCCESS, } from './actions'; +import type { UserType } from './types/data'; import { snakeToCamelCase } from './utils'; -// FIXME: type this -const initialState = { +interface RootState { + isAuthenticated: boolean; + isLoggingIn: boolean; + isFetchingUserData: boolean; + isRegisteringUser: boolean; + isChargingMembership: boolean; + userToken: string | null; + user: UserType | null; + // FIXME: Type rest + loginError: unknown | null; + userError: unknown | null; + registerError: unknown | null; + chargeError: unknown | null; + lastOrder: unknown | null; +} + +const initialState: RootState = { isAuthenticated: false, isLoggingIn: false, isFetchingUserData: false, diff --git a/src/types/data.ts b/src/types/data.ts index c2cb71f..1183ff0 100644 --- a/src/types/data.ts +++ b/src/types/data.ts @@ -1,5 +1,6 @@ export interface EventType { start_time: string; + end_time: string; content: { rendered: string; }; @@ -10,4 +11,22 @@ export interface EventType { thumbnail: { medium_large: string; }; + price_regular: string; + price_member: string; + ticket_url: string; + facebook_url: string; +} + +export interface MembershipType { + membership_type: string; + end_date: string; +} + +export interface UserType { + first_name: string; + last_name: string; + email: string; + last_membership: MembershipType | null; + is_volunteer: boolean; + is_member: boolean; } diff --git a/src/utils.ts b/src/utils.ts index 15d8e82..728fba3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,4 @@ -import AsyncStorage from '@react-native-async-storage/async-storage'; - -export function snakeToCamelCase(val) { +export function snakeToCamelCase(val: string) { return val.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); } @@ -16,30 +14,3 @@ export function fetchWithTimeout(url: string, options?: RequestInit, timeout = 1 }), ]); } - -export async function migrateReduxPersistFourToFive(registerUserSuccess) { - const authedKey = 'reduxPersist:isAuthenticated'; - const userKey = 'reduxPersist:user'; - const userTokenKey = 'reduxPersist:userToken'; - const v4Keys = [authedKey, userKey, userTokenKey]; - - const asyncStorageKeys = await AsyncStorage.getAllKeys(); - if (v4Keys.some((key) => !asyncStorageKeys.includes(key))) { - return; - } - - const isAuthenticated = await AsyncStorage.getItem(authedKey); - console.log(authedKey, isAuthenticated); - if (isAuthenticated !== 'true') { - await AsyncStorage.multiRemove(v4Keys); - return; - } - - const userToken = await AsyncStorage.getItem(userTokenKey); - console.log(userTokenKey, userToken); - const user = await AsyncStorage.getItem(userKey); - console.log(userKey, user, JSON.parse(user)); - registerUserSuccess({ auth_token: JSON.parse(userToken), ...JSON.parse(user) }); - await AsyncStorage.multiRemove(v4Keys); - console.log('successfully migrated to new redux-persist'); -} diff --git a/tsconfig.json b/tsconfig.json index 65c69d3..a9f72b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "baseUrl": "./" + "baseUrl": "./", }, "extends": "expo/tsconfig.base" }