diff --git a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/_layout.tsx b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/_layout.tsx index 44d564b29..210a280c7 100644 --- a/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/_layout.tsx +++ b/apps/expo/app/(app)/(drawer)/(tabs)/(stack)/_layout.tsx @@ -4,12 +4,13 @@ import { View, Text, SafeAreaView, TouchableOpacity } from 'react-native'; import useCustomStyles from 'app/hooks/useCustomStyles'; import { useIsMobileView } from 'app/hooks/common'; import { useNavigate } from 'app/hooks/navigation'; -import { useAuthUser } from 'app/hooks/user/useAuthUser'; +import { useAuthUser } from 'app/auth/hooks'; import { Button } from 'tamagui'; import { EvilIcons } from '@expo/vector-icons'; import SVGLogoComponent from 'app/components/logo'; import { AuthStateListener } from 'app/auth/AuthStateListener'; import useTheme from 'app/hooks/useTheme'; +import { DrawerActions } from '@react-navigation/native'; export default function StackLayout() { const user = useAuthUser(); @@ -41,7 +42,7 @@ export default function StackLayout() { { - navigation.toggleDrawer(); + navigation.dispatch(DrawerActions.toggleDrawer()); }} > state.auth.user); + const user = useAuthUser(); const mutualStyles = { backgroundColor: currentTheme.colors.background, diff --git a/apps/expo/app/(app)/_layout.tsx b/apps/expo/app/(app)/_layout.tsx index 8d07dcb27..ea12efc78 100644 --- a/apps/expo/app/(app)/_layout.tsx +++ b/apps/expo/app/(app)/_layout.tsx @@ -1,10 +1,10 @@ -import { useSession } from 'app/context/Auth/SessionProvider'; -import { useProtectedRoute } from 'app/hooks/auth/useProtectedRoute'; -import { Navigation } from 'app/components/navigation'; -import { Slot } from 'expo-router' +import { Slot } from 'expo-router'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; export default function AppLayout() { - const { session } = useSession(); - useProtectedRoute(session); - return ; + return ( + + + + ); } diff --git a/apps/expo/app/(auth)/_layout.tsx b/apps/expo/app/(auth)/_layout.tsx index 445435f90..c7314d16d 100644 --- a/apps/expo/app/(auth)/_layout.tsx +++ b/apps/expo/app/(auth)/_layout.tsx @@ -1,9 +1,5 @@ import { Stack } from 'expo-router'; -import { useSession } from 'app/context/Auth/SessionProvider'; -import { useProtectedRoute } from 'app/hooks/auth/useProtectedRoute'; export default function AppLayout() { - const { session } = useSession(); - useProtectedRoute(session); return ; } diff --git a/apps/next/auth/authWrapper.tsx b/apps/next/auth/authWrapper.tsx deleted file mode 100644 index ce3db5745..000000000 --- a/apps/next/auth/authWrapper.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useSession } from 'app/context/Auth/SessionProvider'; -import { useRouter } from 'next/router'; -import { useDispatch, useSelector } from 'react-redux'; -import useProtectedRoute from 'app/hooks/auth/useProtectedRoute/useProtectedRoute.web'; -import { useEffect, useState } from 'react'; - -type Props = { - children?: React.ReactNode; -}; - -export const AuthWrapper = ({ children }: Props) => { - const dispatch = useDispatch(); - const { push } = useRouter(); - const [isLoading, setIsLoading] = useState(true); - const loading = useProtectedRoute(); - useEffect(() => { - if (!loading) { - setIsLoading(false); - } - }, [loading]); - if (isLoading) { - return
Loading...
; - } - return children; -}; diff --git a/apps/next/pages/appearance/index.tsx b/apps/next/pages/appearance/index.tsx index f87666105..f412ccaef 100644 --- a/apps/next/pages/appearance/index.tsx +++ b/apps/next/pages/appearance/index.tsx @@ -1,5 +1,5 @@ import AppearanceContainer from 'app/screens/appearance/AppearanceContainer'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; diff --git a/apps/next/pages/dashboard/index.tsx b/apps/next/pages/dashboard/index.tsx index d148b4da4..2ca8da120 100644 --- a/apps/next/pages/dashboard/index.tsx +++ b/apps/next/pages/dashboard/index.tsx @@ -1,6 +1,6 @@ import Dashboard from 'app/screens/dashboard'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; diff --git a/apps/next/pages/destination/[destinationId].tsx b/apps/next/pages/destination/[destinationId].tsx index 2fd40d496..da0e0d660 100644 --- a/apps/next/pages/destination/[destinationId].tsx +++ b/apps/next/pages/destination/[destinationId].tsx @@ -1,5 +1,5 @@ import { DestinationPage } from 'app/components/destination'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // import DestinationPage from "../../components/destination"; // export const runtime = 'experimental-edge'; diff --git a/apps/next/pages/feed/index.tsx b/apps/next/pages/feed/index.tsx index 6f7e5e4a2..98ca89bd5 100644 --- a/apps/next/pages/feed/index.tsx +++ b/apps/next/pages/feed/index.tsx @@ -1,5 +1,5 @@ import Feed from 'app/screens/feed/Feed'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; diff --git a/apps/next/pages/index.tsx b/apps/next/pages/index.tsx index 8e9fded2c..6093200a7 100644 --- a/apps/next/pages/index.tsx +++ b/apps/next/pages/index.tsx @@ -1,10 +1,11 @@ import Dashboard from './dashboard'; -import { useSelector } from 'react-redux'; import LandingPage from 'app/components/landing_page'; +import { useAuthUser } from 'app/auth/hooks'; // export const runtime = 'experimental-edge' export default function Home() { - const user = useSelector((state: any) => state.auth.user); + const user = useAuthUser(); + return <>{!user ? : }; } diff --git a/apps/next/pages/items/index.tsx b/apps/next/pages/items/index.tsx index d44f34a37..19ef06e3f 100644 --- a/apps/next/pages/items/index.tsx +++ b/apps/next/pages/items/index.tsx @@ -1,5 +1,5 @@ import Items from 'app/screens/items'; - +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; export default function ItemsPage() { @@ -11,5 +11,5 @@ export default function ItemsPage() { } ItemsPage.getLayout = function getLayout(page: any) { - return <>{page}; + return {page}; }; diff --git a/apps/next/pages/pack/[id].tsx b/apps/next/pages/pack/[id].tsx index 8766051a9..1841bbb31 100644 --- a/apps/next/pages/pack/[id].tsx +++ b/apps/next/pages/pack/[id].tsx @@ -1,6 +1,6 @@ import React from 'react'; import { PackDetails } from 'app/components/pack/PackDetails'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/pack/create.tsx b/apps/next/pages/pack/create.tsx index 528fb611e..94a6bfd9e 100644 --- a/apps/next/pages/pack/create.tsx +++ b/apps/next/pages/pack/create.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { AddPack } from 'app/components/pack/AddPack'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/packs/index.tsx b/apps/next/pages/packs/index.tsx index 25e3663fd..4c9fadbc3 100644 --- a/apps/next/pages/packs/index.tsx +++ b/apps/next/pages/packs/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Feed from 'app/screens/feed/Feed'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/password-reset/index.tsx b/apps/next/pages/password-reset/index.tsx index 1e60082a7..b715379cf 100644 --- a/apps/next/pages/password-reset/index.tsx +++ b/apps/next/pages/password-reset/index.tsx @@ -1,5 +1,5 @@ import { RequestPasswordReset } from 'app/components/password-reset'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/profile/[id].tsx b/apps/next/pages/profile/[id].tsx index 5bcdb7017..10c4b5806 100644 --- a/apps/next/pages/profile/[id].tsx +++ b/apps/next/pages/profile/[id].tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { StyleSheet, Text, View, Platform } from 'react-native'; import ProfileContainer from 'app/screens/user/ProfileContainer'; import { createParam } from 'solito'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; const { useParam } = createParam(); diff --git a/apps/next/pages/profile/index.tsx b/apps/next/pages/profile/index.tsx index 0ddea9303..d4ab506df 100644 --- a/apps/next/pages/profile/index.tsx +++ b/apps/next/pages/profile/index.tsx @@ -1,5 +1,5 @@ import ProfileContainer from 'app/screens/user/ProfileContainer'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/profile/settings/index.tsx b/apps/next/pages/profile/settings/index.tsx index 6d6c1160e..9e3ad650f 100644 --- a/apps/next/pages/profile/settings/index.tsx +++ b/apps/next/pages/profile/settings/index.tsx @@ -1,5 +1,5 @@ import Settings from 'app/screens/user/Settings'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/sign-in/index.tsx b/apps/next/pages/sign-in/index.tsx index 3cbc94189..07bca7b04 100644 --- a/apps/next/pages/sign-in/index.tsx +++ b/apps/next/pages/sign-in/index.tsx @@ -1,8 +1,6 @@ import React from 'react'; import LoginScreen from 'app/screens/LoginScreen'; -// export const runtime = 'experimental-edge' - function Login() { return (
diff --git a/apps/next/pages/trip/[tripId].tsx b/apps/next/pages/trip/[tripId].tsx index bf11d9dc4..5080d4fac 100644 --- a/apps/next/pages/trip/[tripId].tsx +++ b/apps/next/pages/trip/[tripId].tsx @@ -1,5 +1,5 @@ import { TripDetails } from 'app/screens/trip/TripDetails'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/trip/create.tsx b/apps/next/pages/trip/create.tsx index 134e95164..406814c03 100644 --- a/apps/next/pages/trip/create.tsx +++ b/apps/next/pages/trip/create.tsx @@ -1,5 +1,5 @@ import CreateTrip from 'app/screens/trip/createTrip'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/next/pages/trips/index.tsx b/apps/next/pages/trips/index.tsx index 23d1b6812..dd9b94034 100644 --- a/apps/next/pages/trips/index.tsx +++ b/apps/next/pages/trips/index.tsx @@ -1,5 +1,5 @@ import Feed from 'app/screens/feed/Feed'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/auth/authWrapper.tsx b/apps/tauri/auth/authWrapper.tsx deleted file mode 100644 index ce3db5745..000000000 --- a/apps/tauri/auth/authWrapper.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useSession } from 'app/context/Auth/SessionProvider'; -import { useRouter } from 'next/router'; -import { useDispatch, useSelector } from 'react-redux'; -import useProtectedRoute from 'app/hooks/auth/useProtectedRoute/useProtectedRoute.web'; -import { useEffect, useState } from 'react'; - -type Props = { - children?: React.ReactNode; -}; - -export const AuthWrapper = ({ children }: Props) => { - const dispatch = useDispatch(); - const { push } = useRouter(); - const [isLoading, setIsLoading] = useState(true); - const loading = useProtectedRoute(); - useEffect(() => { - if (!loading) { - setIsLoading(false); - } - }, [loading]); - if (isLoading) { - return
Loading...
; - } - return children; -}; diff --git a/apps/tauri/pages/appearance/index.tsx b/apps/tauri/pages/appearance/index.tsx index f87666105..f412ccaef 100644 --- a/apps/tauri/pages/appearance/index.tsx +++ b/apps/tauri/pages/appearance/index.tsx @@ -1,5 +1,5 @@ import AppearanceContainer from 'app/screens/appearance/AppearanceContainer'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; diff --git a/apps/tauri/pages/dashboard/index.tsx b/apps/tauri/pages/dashboard/index.tsx index d148b4da4..2ca8da120 100644 --- a/apps/tauri/pages/dashboard/index.tsx +++ b/apps/tauri/pages/dashboard/index.tsx @@ -1,6 +1,6 @@ import Dashboard from 'app/screens/dashboard'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; diff --git a/apps/tauri/pages/destination/[destinationId].tsx b/apps/tauri/pages/destination/[destinationId].tsx index 2fd40d496..da0e0d660 100644 --- a/apps/tauri/pages/destination/[destinationId].tsx +++ b/apps/tauri/pages/destination/[destinationId].tsx @@ -1,5 +1,5 @@ import { DestinationPage } from 'app/components/destination'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // import DestinationPage from "../../components/destination"; // export const runtime = 'experimental-edge'; diff --git a/apps/tauri/pages/feed/index.tsx b/apps/tauri/pages/feed/index.tsx index 6f7e5e4a2..98ca89bd5 100644 --- a/apps/tauri/pages/feed/index.tsx +++ b/apps/tauri/pages/feed/index.tsx @@ -1,5 +1,5 @@ import Feed from 'app/screens/feed/Feed'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge'; diff --git a/apps/tauri/pages/index.tsx b/apps/tauri/pages/index.tsx index 8e9fded2c..eb754ad50 100644 --- a/apps/tauri/pages/index.tsx +++ b/apps/tauri/pages/index.tsx @@ -1,10 +1,10 @@ import Dashboard from './dashboard'; -import { useSelector } from 'react-redux'; import LandingPage from 'app/components/landing_page'; +import { useAuthUser } from 'app/auth/hooks'; // export const runtime = 'experimental-edge' export default function Home() { - const user = useSelector((state: any) => state.auth.user); + const user = useAuthUser(); return <>{!user ? : }; } diff --git a/apps/tauri/pages/pack/[id].tsx b/apps/tauri/pages/pack/[id].tsx index 8766051a9..1841bbb31 100644 --- a/apps/tauri/pages/pack/[id].tsx +++ b/apps/tauri/pages/pack/[id].tsx @@ -1,6 +1,6 @@ import React from 'react'; import { PackDetails } from 'app/components/pack/PackDetails'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/pack/create.tsx b/apps/tauri/pages/pack/create.tsx index 528fb611e..94a6bfd9e 100644 --- a/apps/tauri/pages/pack/create.tsx +++ b/apps/tauri/pages/pack/create.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { AddPack } from 'app/components/pack/AddPack'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/packs/index.tsx b/apps/tauri/pages/packs/index.tsx index 25e3663fd..4c9fadbc3 100644 --- a/apps/tauri/pages/packs/index.tsx +++ b/apps/tauri/pages/packs/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Feed from 'app/screens/feed/Feed'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/password-reset/index.tsx b/apps/tauri/pages/password-reset/index.tsx index 1e60082a7..b715379cf 100644 --- a/apps/tauri/pages/password-reset/index.tsx +++ b/apps/tauri/pages/password-reset/index.tsx @@ -1,5 +1,5 @@ import { RequestPasswordReset } from 'app/components/password-reset'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/profile/[id].tsx b/apps/tauri/pages/profile/[id].tsx index 5bcdb7017..10c4b5806 100644 --- a/apps/tauri/pages/profile/[id].tsx +++ b/apps/tauri/pages/profile/[id].tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { StyleSheet, Text, View, Platform } from 'react-native'; import ProfileContainer from 'app/screens/user/ProfileContainer'; import { createParam } from 'solito'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; const { useParam } = createParam(); diff --git a/apps/tauri/pages/profile/index.tsx b/apps/tauri/pages/profile/index.tsx index 0ddea9303..d4ab506df 100644 --- a/apps/tauri/pages/profile/index.tsx +++ b/apps/tauri/pages/profile/index.tsx @@ -1,5 +1,5 @@ import ProfileContainer from 'app/screens/user/ProfileContainer'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/profile/settings/index.tsx b/apps/tauri/pages/profile/settings/index.tsx index 6d6c1160e..9e3ad650f 100644 --- a/apps/tauri/pages/profile/settings/index.tsx +++ b/apps/tauri/pages/profile/settings/index.tsx @@ -1,5 +1,5 @@ import Settings from 'app/screens/user/Settings'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/trip/[tripId].tsx b/apps/tauri/pages/trip/[tripId].tsx index bf11d9dc4..5080d4fac 100644 --- a/apps/tauri/pages/trip/[tripId].tsx +++ b/apps/tauri/pages/trip/[tripId].tsx @@ -1,5 +1,5 @@ import { TripDetails } from 'app/screens/trip/TripDetails'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/trip/create.tsx b/apps/tauri/pages/trip/create.tsx index 134e95164..406814c03 100644 --- a/apps/tauri/pages/trip/create.tsx +++ b/apps/tauri/pages/trip/create.tsx @@ -1,5 +1,5 @@ import CreateTrip from 'app/screens/trip/createTrip'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/apps/tauri/pages/trips/index.tsx b/apps/tauri/pages/trips/index.tsx index 23d1b6812..dd9b94034 100644 --- a/apps/tauri/pages/trips/index.tsx +++ b/apps/tauri/pages/trips/index.tsx @@ -1,5 +1,5 @@ import Feed from 'app/screens/feed/Feed'; -import { AuthWrapper } from 'auth/authWrapper'; +import { AuthWrapper } from 'app/auth/AuthWrapper'; // export const runtime = 'experimental-edge' diff --git a/packages/app/.eslintrc.js b/packages/app/.eslintrc.js index e56de141e..70b471919 100644 --- a/packages/app/.eslintrc.js +++ b/packages/app/.eslintrc.js @@ -5,6 +5,8 @@ module.exports = { }, extends: [ 'standard-with-typescript', + 'eslint:recommended', + 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@tanstack/eslint-plugin-query/recommended', 'prettier', diff --git a/packages/app/auth/AuthLoader.tsx b/packages/app/auth/AuthLoader.tsx new file mode 100644 index 000000000..905069772 --- /dev/null +++ b/packages/app/auth/AuthLoader.tsx @@ -0,0 +1,19 @@ +import { useAuthUserToken } from './hooks'; + +export const AuthLoader = ({ + children, + loadingElement, + unauthorizedElement, +}) => { + const { token, isLoading } = useAuthUserToken(); + + if (isLoading) { + return loadingElement; + } + + if (!token) { + return unauthorizedElement; + } + + return children; +}; diff --git a/packages/app/auth/AuthWrapper.tsx b/packages/app/auth/AuthWrapper.tsx new file mode 100644 index 000000000..b497e1d22 --- /dev/null +++ b/packages/app/auth/AuthWrapper.tsx @@ -0,0 +1,18 @@ +import { AuthLoader } from 'app/auth/AuthLoader'; +import { Redirect } from 'app/components/Redirect'; +import {Text} from '@packrat/ui'; + +type Props = { + children?: React.ReactNode; +}; + +export const AuthWrapper = ({ children }: Props) => { + return ( + Loading...} + unauthorizedElement={} + > + {children} + + ); +}; diff --git a/packages/app/hooks/login/index.ts b/packages/app/auth/hooks/index.ts similarity index 61% rename from packages/app/hooks/login/index.ts rename to packages/app/auth/hooks/index.ts index fdbe03cf4..90ddb9286 100644 --- a/packages/app/hooks/login/index.ts +++ b/packages/app/auth/hooks/index.ts @@ -1,3 +1,5 @@ +export * from './useUser'; export { useLogin } from './useLogin'; +export { useRegisterUser } from './useRegisterUser'; export { useGoogleAuth } from './useGoogleAuth'; export { useLogout } from './useLogout'; diff --git a/packages/app/auth/hooks/useGetMe.ts b/packages/app/auth/hooks/useGetMe.ts new file mode 100644 index 000000000..e06f6d0a0 --- /dev/null +++ b/packages/app/auth/hooks/useGetMe.ts @@ -0,0 +1,10 @@ +import { queryTrpc } from '../../trpc'; +import { useStorage } from 'app/hooks/storage/useStorage'; + +export const useGetMe = () => { + const [[_, token]] = useStorage('token'); // TODO add enabled based on token to avoid unneeded requests + + const { data, isLoading } = queryTrpc.getMe.useQuery(); + + return { data, isLoading }; +}; diff --git a/packages/app/auth/hooks/useGoogleAuth.ts b/packages/app/auth/hooks/useGoogleAuth.ts new file mode 100644 index 000000000..02044856a --- /dev/null +++ b/packages/app/auth/hooks/useGoogleAuth.ts @@ -0,0 +1,38 @@ +import { useState, useEffect } from 'react'; +import * as Google from 'expo-auth-session/providers/google'; + +import { queryTrpc } from 'app/trpc'; +import { useSessionSignIn } from './useSessionSignIn'; + +const webClientId = String(process.env.NEXT_PUBLIC_GOOGLE_ID); +const iosClientId = String(process.env.IOS_CLIENT_ID); +const androidClientId = String(process.env.ANDROID_CLIENT_ID); + +export const useGoogleAuth = () => { + const [token, setToken] = useState(''); + const sessionSignIn = useSessionSignIn(); + + queryTrpc.googleSignin.useQuery( + { + idToken: token, + }, + { enabled: !!token, onSuccess: (user) => sessionSignIn(user) }, + ); + + const [request, response, promptAsync] = Google.useIdTokenAuthRequest({ + webClientId, + iosClientId, + androidClientId, + }); + + const enableGoogleLogin = webClientId && webClientId !== ''; + + useEffect(() => { + if (response?.type === 'success') { + const { id_token } = response.params; + setToken(id_token); + } + }, [response]); + + return { enableGoogleLogin, isGoogleSignInReady: !!request, promptAsync }; +}; diff --git a/packages/app/auth/hooks/useLogin.ts b/packages/app/auth/hooks/useLogin.ts new file mode 100644 index 000000000..5c7e20615 --- /dev/null +++ b/packages/app/auth/hooks/useLogin.ts @@ -0,0 +1,34 @@ +import { useForm, type UseFormReturn } from 'react-hook-form'; +import { queryTrpc } from 'app/trpc'; +import { useSessionSignIn } from './useSessionSignIn'; + +interface UserForm { + email: string; + password: string; +} + +interface UseLoginReturn { + form: UseFormReturn; + handleLogin: (data: UserForm) => void; + hasError: boolean; +} + +export const useLogin = (): UseLoginReturn => { + const { mutateAsync: signIn, error } = queryTrpc.signIn.useMutation(); + const sessionSignIn = useSessionSignIn(); + const form = useForm({ + defaultValues: { + email: '', + password: '', + }, + }); + + const handleLogin: UseLoginReturn['handleLogin'] = (data) => { + const { email, password } = data; + signIn({ email, password }).then((user) => { + sessionSignIn(user); + }); + }; + + return { form, handleLogin, hasError: !!error }; +}; diff --git a/packages/app/auth/hooks/useLogout.ts b/packages/app/auth/hooks/useLogout.ts new file mode 100644 index 000000000..d97bd4db4 --- /dev/null +++ b/packages/app/auth/hooks/useLogout.ts @@ -0,0 +1,13 @@ +import { Storage } from 'app/utils/storage'; +import { useUserSetter } from './useUserSetter'; + +export const useLogout = () => { + const setUser = useUserSetter(); + + const logout = () => { + setUser(null); + Storage.removeItem('token'); + }; + + return logout; +}; diff --git a/packages/app/hooks/user/useRegisterUser.ts b/packages/app/auth/hooks/useRegisterUser.ts similarity index 57% rename from packages/app/hooks/user/useRegisterUser.ts rename to packages/app/auth/hooks/useRegisterUser.ts index 327e0ae99..8c0d9b12d 100644 --- a/packages/app/hooks/user/useRegisterUser.ts +++ b/packages/app/auth/hooks/useRegisterUser.ts @@ -1,11 +1,6 @@ import { useForm, type UseFormReturn } from 'react-hook-form'; -import { useDispatch } from 'react-redux'; -import * as WebBrowser from 'expo-web-browser'; -import { signUp } from '../../store/authStore'; -import { useSession } from '../../context/Auth/SessionProvider'; -import { useRouter } from 'app/hooks/router'; - -WebBrowser.maybeCompleteAuthSession(); +import { queryTrpc } from 'app/trpc'; +import { useSessionSignIn } from './useSessionSignIn'; interface RegisterForm { name: string; @@ -20,9 +15,8 @@ interface UseRegisterUserReturn { } export const useRegisterUser = (): UseRegisterUserReturn => { - const dispatch = useDispatch(); - const { sessionSignIn } = useSession(); - const router = useRouter(); + const { mutateAsync: signUp } = queryTrpc.signUp.useMutation(); + const sessionSignIn = useSessionSignIn(); const form = useForm({ defaultValues: { name: '', @@ -40,15 +34,9 @@ export const useRegisterUser = (): UseRegisterUserReturn => { alert('Username should be alphanumeric'); return; } - dispatch(signUp({ name, username, email, password })).then( - ({ payload }) => { - if (!payload) return; - if (payload.token) { - sessionSignIn(payload.token); - router.push('/'); - } - }, - ); + signUp({ name, username, email, password }).then((user) => { + sessionSignIn(user); + }); } catch (e) { console.log('Error', e); } diff --git a/packages/app/auth/hooks/useSessionSignIn.ts b/packages/app/auth/hooks/useSessionSignIn.ts new file mode 100644 index 000000000..2d622db21 --- /dev/null +++ b/packages/app/auth/hooks/useSessionSignIn.ts @@ -0,0 +1,21 @@ +import { useCallback } from 'react'; +import { useUserSetter } from './useUserSetter'; +import { Storage } from 'app/utils/storage'; +import { useRouter } from 'app/hooks/router'; + +export const useSessionSignIn = () => { + const setUser = useUserSetter(); + const router = useRouter(); + + const sessionSignIn = useCallback((user) => { + if (user?.token) { + (async () => { + setUser(user); + await Storage.setItem('token', user.token); + router.push('/'); + })(); + } + }, []); + + return sessionSignIn; +}; diff --git a/packages/app/auth/hooks/useUser.ts b/packages/app/auth/hooks/useUser.ts new file mode 100644 index 000000000..ac2c18167 --- /dev/null +++ b/packages/app/auth/hooks/useUser.ts @@ -0,0 +1,20 @@ +import { useStorage } from 'app/hooks/storage/useStorage'; +import { useGetMe } from './useGetMe'; + +export const useUserQuery = () => { + const { data: user, isLoading } = useGetMe(); + + return { isLoading, user }; +}; + +export const useAuthUserToken = () => { + const [[isLoading, token]] = useStorage('token'); + + return { token, isLoading }; +}; + +export const useAuthUser = () => { + const { user } = useUserQuery(); + + return user; +}; diff --git a/packages/app/auth/hooks/useUserSetter.ts b/packages/app/auth/hooks/useUserSetter.ts new file mode 100644 index 000000000..304203577 --- /dev/null +++ b/packages/app/auth/hooks/useUserSetter.ts @@ -0,0 +1,17 @@ +import { useCallback } from 'react'; +import { getQueryKey } from '@trpc/react-query'; +import { queryTrpc } from 'app/trpc'; +import { useQueryClient } from '@tanstack/react-query'; +import { User } from '../ts'; + +export const useUserSetter = () => { + const queryClient = useQueryClient(); + const userKey = getQueryKey(queryTrpc.getMe, undefined, 'query'); + + const setUser = useCallback( + (data: User | null) => queryClient.setQueryData(userKey, data), + [queryClient, userKey], + ); + + return setUser; +}; diff --git a/packages/app/auth/ts/index.ts b/packages/app/auth/ts/index.ts new file mode 100644 index 000000000..957860982 --- /dev/null +++ b/packages/app/auth/ts/index.ts @@ -0,0 +1 @@ +export * from './interfaces'; diff --git a/packages/app/auth/ts/interfaces.ts b/packages/app/auth/ts/interfaces.ts new file mode 100644 index 000000000..e1ec51337 --- /dev/null +++ b/packages/app/auth/ts/interfaces.ts @@ -0,0 +1,15 @@ +import { UserRole } from './types'; + +export interface User { + createdAt: string; + email: string; + favorites: any[]; + id: string; + name: string; + profileImage: string; + role: UserRole; + token: string; + updatedAt: string; + username: string; + _id: string; +} diff --git a/packages/app/auth/ts/types.ts b/packages/app/auth/ts/types.ts new file mode 100644 index 000000000..317cef73a --- /dev/null +++ b/packages/app/auth/ts/types.ts @@ -0,0 +1 @@ +export type UserRole = 'user' | 'admin'; diff --git a/packages/app/components/Redirect/Redirect.tsx b/packages/app/components/Redirect/Redirect.tsx new file mode 100644 index 000000000..b41139cc4 --- /dev/null +++ b/packages/app/components/Redirect/Redirect.tsx @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; +import { useRouter } from 'app/hooks/router'; + +type RedirectProps = { + to: string; +}; + +export const Redirect = ({ to }: RedirectProps) => { + const router = useRouter(); + useEffect(() => { + if (to) { + router.replace(to); + } + }, [to]); + + return null; +}; diff --git a/packages/app/components/Redirect/index.ts b/packages/app/components/Redirect/index.ts new file mode 100644 index 000000000..bb0a6b847 --- /dev/null +++ b/packages/app/components/Redirect/index.ts @@ -0,0 +1 @@ +export { Redirect } from './Redirect'; diff --git a/packages/app/components/card/CustomCardHeader.tsx b/packages/app/components/card/CustomCardHeader.tsx index 1df6e8b8c..5dbdf669c 100644 --- a/packages/app/components/card/CustomCardHeader.tsx +++ b/packages/app/components/card/CustomCardHeader.tsx @@ -4,7 +4,7 @@ import { View } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { Link } from 'solito/link'; import { useCopyClipboard } from 'app/hooks/common'; -import { useAuthUser } from 'app/hooks/user/useAuthUser'; +import { useAuthUser } from 'app/auth/hooks'; export const CustomCardHeader = ({ data, title, link, actionsComponent }) => { const { isCopied, handleCopyLink } = useCopyClipboard(link); diff --git a/packages/app/components/card/PackCardHeader/PackCardHeader.tsx b/packages/app/components/card/PackCardHeader/PackCardHeader.tsx index 1f13faaf8..b18a3d22f 100644 --- a/packages/app/components/card/PackCardHeader/PackCardHeader.tsx +++ b/packages/app/components/card/PackCardHeader/PackCardHeader.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { CustomCardHeader } from '../CustomCardHeader'; -import { useAuthUser } from 'app/hooks/user/useAuthUser'; +import { useAuthUser } from 'app/auth/hooks'; import { ThreeDotsMenu, YStack, Button, EditableText } from '@packrat/ui'; import { useDeletePack, useFetchSinglePack } from 'app/hooks/packs'; import { usePackTitleInput } from './usePackTitleInput'; diff --git a/packages/app/components/feed/FeedCard.tsx b/packages/app/components/feed/FeedCard.tsx index 8d549e290..f2e0d50df 100644 --- a/packages/app/components/feed/FeedCard.tsx +++ b/packages/app/components/feed/FeedCard.tsx @@ -15,6 +15,7 @@ import { truncateString } from '../../utils/truncateString'; import { RText, RStack, RHeading } from '@packrat/ui'; import { formatNumber } from 'app/utils/formatNumber'; import { useAddFavorite } from 'app/hooks/favorites'; +import { useAuthUser } from 'app/auth/hooks'; export default function Card({ type, @@ -31,7 +32,7 @@ export default function Card({ owners, duration, }) { - const user = useSelector((state) => state.auth.user); + const user = useAuthUser(); const { enableDarkMode, enableLightMode, isDark, isLight, currentTheme } = useTheme(); diff --git a/packages/app/components/item/SearchItem/useSearchItem.ts b/packages/app/components/item/SearchItem/useSearchItem.ts index 68598a036..a4e62117d 100644 --- a/packages/app/components/item/SearchItem/useSearchItem.ts +++ b/packages/app/components/item/SearchItem/useSearchItem.ts @@ -1,10 +1,10 @@ -import { useSelector } from 'react-redux'; import { useMemo, useState } from 'react'; import { useItems } from 'app/hooks/items'; import { trpc } from 'app/trpc'; +import { useAuthUser } from 'app/auth/hooks'; export const useSearchItem = () => { - const user = useSelector((state: any) => state.auth.user); + const user = useAuthUser() const [searchString, setSearchString] = useState(''); const itemFilters = useMemo(() => { diff --git a/packages/app/components/navigation/Navigation.tsx b/packages/app/components/navigation/Navigation.tsx index 0a9bfbb9c..557e5484b 100644 --- a/packages/app/components/navigation/Navigation.tsx +++ b/packages/app/components/navigation/Navigation.tsx @@ -10,7 +10,7 @@ import { AuthStateListener } from '../../auth/AuthStateListener'; import useCustomStyles from 'app/hooks/useCustomStyles'; import { useIsMobileView } from 'app/hooks/common'; import { useNavigate } from 'app/hooks/navigation'; -import { useAuthUser } from 'app/hooks/user/useAuthUser'; +import { useAuthUser } from 'app/auth/hooks'; import { NavigationList } from './NavigationList'; import { Button } from 'tamagui'; import SVGLogoComponent from '../../components/logo'; diff --git a/packages/app/components/pack/PackContainer.tsx b/packages/app/components/pack/PackContainer.tsx index ff6513238..3e8b0daf8 100644 --- a/packages/app/components/pack/PackContainer.tsx +++ b/packages/app/components/pack/PackContainer.tsx @@ -4,7 +4,6 @@ import DropdownComponent from '../Dropdown'; import { AddItem } from '../item/AddItem'; import { TableContainer } from '../pack_table/Table'; // import { useAuth } from "../../auth/provider"; -import { useSelector } from 'react-redux'; import { useUserPacks } from '../../hooks/packs/useUserPacks'; import { fetchUserPacks, @@ -17,16 +16,18 @@ import { View } from 'react-native'; import { AddItemModal } from './AddItemModal'; import useCustomStyles from 'app/hooks/useCustomStyles'; import { useSearchParams } from 'app/hooks/common'; +import { useAuthUser } from 'app/auth/hooks'; export default function PackContainer({ isCreatingTrip = false }) { const dispatch = useDispatch(); const [isAddItemModalOpen, setIsAddItemModalOpen] = useState(false); - const user = useSelector((state) => state.auth.user); const searchParams = useSearchParams(); const [currentPackId, setCurrentPackId] = useState( searchParams.get('packId'), ); + const user = useAuthUser(); + const [refetch, setRefetch] = useState(false); const styles = useCustomStyles(loadStyles); diff --git a/packages/app/components/pack/PackDetails.tsx b/packages/app/components/pack/PackDetails.tsx index 86536121a..77a6bb9c5 100644 --- a/packages/app/components/pack/PackDetails.tsx +++ b/packages/app/components/pack/PackDetails.tsx @@ -18,7 +18,7 @@ import { AddItemModal } from './AddItemModal'; import useCustomStyles from 'app/hooks/useCustomStyles'; import { useUserPacks } from 'app/hooks/packs/useUserPacks'; import { useFetchSinglePack } from '../../hooks/packs'; -import { RootState } from 'store/store'; +import { useAuthUser } from 'app/auth/hooks'; const { useParam } = createParam(); @@ -29,7 +29,7 @@ export function PackDetails() { console.log(packId, 'packId'); const link = `${CLIENT_URL}/packs/${packId}`; const [firstLoad, setFirstLoad] = useState(true); - const user = useSelector((state: RootState) => state.auth.user); + const user = useAuthUser(); const userId = user?._id; const [isAddItemModalOpen, setIsAddItemModalOpen] = useState(false); const [refetch, setRefetch] = useState(false); diff --git a/packages/app/components/trip/createTripModal.tsx b/packages/app/components/trip/createTripModal.tsx index 13d647beb..0a47d7d16 100644 --- a/packages/app/components/trip/createTripModal.tsx +++ b/packages/app/components/trip/createTripModal.tsx @@ -1,6 +1,5 @@ import React, { useState } from 'react'; import { BaseModal, RInput, RStack, RText } from '@packrat/ui'; -import { useDispatch, useSelector } from 'react-redux'; import { useRouter } from 'app/hooks/router'; import { format, intervalToDuration } from 'date-fns'; // import { addTrip } from '../../store/tripsStore'; @@ -10,6 +9,7 @@ import { useGetPhotonDetails } from 'app/hooks/destination'; // import { Picker } from '@react-native-picker/picker'; import { DropdownComponent } from '../Dropdown'; import { useSearchParams } from 'app/hooks/common'; +import { useAuthUser } from 'app/auth/hooks'; const options = [ { label: 'Public', value: 'true' }, { label: 'For me only', value: 'false' }, @@ -72,7 +72,7 @@ export const SaveTripContainer = ({ search, form, }) => { - const user = useSelector((state) => state.auth.user); + const user = useAuthUser(); const searchParams = useSearchParams(); const packId = searchParams.get('packId'); diff --git a/packages/app/components/user/UserDataContainer.tsx b/packages/app/components/user/UserDataContainer.tsx index a933dc117..6d7f27c72 100644 --- a/packages/app/components/user/UserDataContainer.tsx +++ b/packages/app/components/user/UserDataContainer.tsx @@ -10,7 +10,7 @@ import { theme } from '../../theme'; import useTheme from '../../hooks/useTheme'; import { hexToRGBA } from 'app/utils/colorFunctions'; import { View, FlatList } from 'react-native'; -import { RootState } from 'store/store'; +import { useAuthUser } from 'app/auth/hooks'; // Skeleton version of the UserDataCard component const SkeletonUserDataCard = () => { @@ -41,7 +41,7 @@ export default function UserDataContainer({ useEffect(() => { setDataState(Array(data.length).fill(false)); }, [data]); - const currentUser = useSelector((state: RootState) => state.auth.user); + const currentUser = useAuthUser(); const typeUppercase = type.charAt(0).toUpperCase() + type.slice(1); diff --git a/packages/app/hooks/chat/useChat.ts b/packages/app/hooks/chat/useChat.ts index 219d834f7..5adf7d0a2 100644 --- a/packages/app/hooks/chat/useChat.ts +++ b/packages/app/hooks/chat/useChat.ts @@ -3,10 +3,11 @@ import { useSelector } from '../redux/useSelector'; import { useState } from 'react'; import { useGetUserChats } from './useGetUserChats'; import { useGetAIResponse } from './useGetAIResponse'; +import { useAuthUser } from 'app/auth/hooks'; export const useChat = (defaultChatId = null) => { const dispatch = useDispatch(); - const user = useSelector((state) => state.auth.user); + const user = useAuthUser(); const [conversationId, setConversationId] = useState(defaultChatId); // const conversation = useSelector((state) => // selectConversationById(state, conversationId), diff --git a/packages/app/hooks/login/useGoogleAuth.ts b/packages/app/hooks/login/useGoogleAuth.ts deleted file mode 100644 index 05995a436..000000000 --- a/packages/app/hooks/login/useGoogleAuth.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import * as Google from 'expo-auth-session/providers/google'; -import { useSession } from '../../context/Auth/SessionProvider'; -import { WEB_CLIENT_ID } from '@env'; -import { signInWithGoogle } from '../../store/authStore'; - -export const useGoogleAuth = () => { - const dispatch = useDispatch(); - const { sessionSignIn } = useSession(); - // Add Google auth-related variables - const [request, response, promptAsync] = Google.useIdTokenAuthRequest({ - clientId: WEB_CLIENT_ID || 'default', - }); - - const enableGoogleLogin = WEB_CLIENT_ID && WEB_CLIENT_ID !== ''; - - useEffect(() => { - console.log({ response }); - if (response?.type === 'success') { - const { id_token } = response.params; - dispatch(signInWithGoogle({ idToken: id_token })).then(({ payload }) => { - if (!payload) return; - if (payload.token) { - sessionSignIn(payload.token); - } - }); - } - }, [response]); - - return { enableGoogleLogin, isGoogleSignInReady: !!request, promptAsync }; -}; diff --git a/packages/app/hooks/login/useLogin.ts b/packages/app/hooks/login/useLogin.ts deleted file mode 100644 index 6e1cee1a2..000000000 --- a/packages/app/hooks/login/useLogin.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useForm, type UseFormReturn } from 'react-hook-form'; -import { useDispatch, useSelector } from 'react-redux'; -import * as WebBrowser from 'expo-web-browser'; -import { signIn } from '../../store/authStore'; -import { useSession } from '../../context/Auth/SessionProvider'; -import { useRouter } from 'app/hooks/router'; - -WebBrowser.maybeCompleteAuthSession(); - -interface UserForm { - email: string; - password: string; -} - -interface UseLoginReturn { - form: UseFormReturn; - handleLogin: (data: UserForm) => void; - hasError: boolean; -} - -export const useLogin = (): UseLoginReturn => { - const router = useRouter(); - const dispatch = useDispatch(); - const { sessionSignIn } = useSession(); - const form = useForm({ - defaultValues: { - email: '', - password: '', - }, - }); - const error = useSelector((state) => state.auth.error); - - const handleLogin: UseLoginReturn['handleLogin'] = (data) => { - const { email, password } = data; - dispatch(signIn({ email, password })).then(({ payload }) => { - if (!payload) return; - if (payload.token) { - router.push('/'); - sessionSignIn(payload.token); - } - }); - }; - - return { form, handleLogin, hasError: !!error }; -}; diff --git a/packages/app/hooks/login/useLogout.ts b/packages/app/hooks/login/useLogout.ts deleted file mode 100644 index c0fdd5d87..000000000 --- a/packages/app/hooks/login/useLogout.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useDispatch } from 'react-redux'; -import { useSession } from '../../context/Auth/SessionProvider'; -import { signOut } from '../../store/authStore'; - -export const useLogout = () => { - const dispatch = useDispatch(); - const { sessionSignOut } = useSession(); - - const logout = () => { - dispatch(signOut()); - sessionSignOut(); - }; - - return logout; -}; diff --git a/packages/app/hooks/navigation/useNavigationItem.ts b/packages/app/hooks/navigation/useNavigationItem.ts index 4bde8b3cd..1979853bc 100644 --- a/packages/app/hooks/navigation/useNavigationItem.ts +++ b/packages/app/hooks/navigation/useNavigationItem.ts @@ -1,6 +1,6 @@ import { useState } from 'react'; import { EvilIcons } from '@expo/vector-icons'; -import { useLogout } from '../login'; +import { useLogout } from '../../auth/hooks'; import { useNavigate } from './useNavigate'; export const useNavigationItem = (item, onSelect) => { diff --git a/packages/app/hooks/navigation/useNavigationList.ts b/packages/app/hooks/navigation/useNavigationList.ts index 46f9bcb6d..75a7890f1 100644 --- a/packages/app/hooks/navigation/useNavigationList.ts +++ b/packages/app/hooks/navigation/useNavigationList.ts @@ -6,7 +6,7 @@ import { Entypo, Fontisto, } from '@expo/vector-icons'; -import { useAuthUser } from '../user/useAuthUser'; +import { useAuthUser } from '../../auth/hooks'; import { Platform } from 'react-native'; export const useNavigationList = () => { diff --git a/packages/app/hooks/navigation/useTabItem.ts b/packages/app/hooks/navigation/useTabItem.ts index 24f4ec2ef..71ea678f0 100644 --- a/packages/app/hooks/navigation/useTabItem.ts +++ b/packages/app/hooks/navigation/useTabItem.ts @@ -1,5 +1,5 @@ import { EvilIcons } from '@expo/vector-icons'; -import { useLogout } from '../login'; +import { useLogout } from 'app/auth/hooks'; import { useTab } from './useTab'; export const useTabItem = (item, onSelect) => { const logout = useLogout(); diff --git a/packages/app/hooks/navigation/useTabList.ts b/packages/app/hooks/navigation/useTabList.ts index 25a24d899..75b20a1b8 100644 --- a/packages/app/hooks/navigation/useTabList.ts +++ b/packages/app/hooks/navigation/useTabList.ts @@ -5,7 +5,7 @@ import { MaterialIcons, AntDesign, } from '@expo/vector-icons'; -import { useAuthUser } from '../user/useAuthUser'; +import { useAuthUser } from 'app/auth/hooks'; export const useTabList = () => { const user = useAuthUser(); diff --git a/packages/app/hooks/packs/useAddNewPack.ts b/packages/app/hooks/packs/useAddNewPack.ts index ca36a4447..19a9f344f 100644 --- a/packages/app/hooks/packs/useAddNewPack.ts +++ b/packages/app/hooks/packs/useAddNewPack.ts @@ -1,9 +1,9 @@ import { useState } from 'react'; import { queryTrpc } from '../../trpc'; -import { useSelector } from 'react-redux'; +import { useAuthUser } from 'app/auth/hooks'; export const useAddNewPack = () => { - const user = useSelector((state) => state.auth.user); + const user = useAuthUser(); const [name, setName] = useState(''); const [isPublic, setIsPublic] = useState(false); const utils = queryTrpc.useContext(); diff --git a/packages/app/hooks/packs/usePackTable.tsx b/packages/app/hooks/packs/usePackTable.tsx index 38ac5ae48..a128f8597 100644 --- a/packages/app/hooks/packs/usePackTable.tsx +++ b/packages/app/hooks/packs/usePackTable.tsx @@ -7,6 +7,7 @@ import { convertWeight } from 'app/utils/convertWeight'; import { RootState } from 'store/store'; type WeightUnit = 'g' | 'kg' | 'oz' | 'lb' | 'lbs'; +import { useAuthUser } from 'app/auth/hooks'; export const usePackTable = ({ currentPack, @@ -15,7 +16,7 @@ export const usePackTable = ({ setRefetch, copy, }) => { - const user = useSelector((state) => state.auth.user); + const user = useAuthUser(); const dispatch = useDispatch(); let ids = []; if (currentPack?.items) { diff --git a/packages/app/hooks/storage/useStorage.ts b/packages/app/hooks/storage/useStorage.ts new file mode 100644 index 000000000..9dde58690 --- /dev/null +++ b/packages/app/hooks/storage/useStorage.ts @@ -0,0 +1,51 @@ +import { useCallback, useEffect, useReducer } from 'react'; +import { Storage, storageEvents } from 'app/utils/storage'; + +type UseStateHook = [[boolean, T | null], (value?: T | null) => void]; + +const useAsyncState = (initialValue) => { + return useReducer((state = null, action) => [false, action], initialValue); +}; + +export function useStorage(key: string): UseStateHook { + const [state, setState] = useAsyncState(key); + + useEffect(() => { + (async () => { + try { + const value = await Storage.getItem(key); + setState(value); + } catch (e) { + console.error('Local storage is unavailable:', e); + } + })(); + + const handleChange = (evt) => { + if (evt.key !== key) return; + setState(evt.value); + }; + + const handleRemove = (evt) => { + if (evt.key !== key) return; + setState(null); + }; + + storageEvents.addListener('change', handleChange); + storageEvents.addListener('remove', handleRemove); + + return () => { + storageEvents.removeListener('change', handleChange); + storageEvents.removeListener('remove', handleRemove); + }; + }, [key]); + + // Set + const setValue = useCallback( + (value: string | null) => { + Storage.setItem(key, value); + }, + [key], + ); + + return [state, setValue]; +} diff --git a/packages/app/hooks/useMatchesCurrentUser.ts b/packages/app/hooks/useMatchesCurrentUser.ts index 5b53911f1..331e71286 100644 --- a/packages/app/hooks/useMatchesCurrentUser.ts +++ b/packages/app/hooks/useMatchesCurrentUser.ts @@ -1,9 +1,10 @@ // useMatchesCurrentUser.ts import { useSelector } from 'react-redux'; import { type RootState } from '../store/store'; +import { useAuthUser } from 'app/auth/hooks'; export const useMatchesCurrentUser = (ownerId: string): boolean => { - const currentUser = useSelector((state: RootState) => state.auth.user); + const currentUser = useAuthUser(); console.log('currentUse---r', currentUser); const currentUserId = currentUser?._id; diff --git a/packages/app/hooks/user/index.ts b/packages/app/hooks/user/index.ts index 5cc3ac37a..080d29853 100644 --- a/packages/app/hooks/user/index.ts +++ b/packages/app/hooks/user/index.ts @@ -1,4 +1,3 @@ export { useGetUser } from './useGetUser'; export { useProfile } from './useProfile'; export { useProfileSettings } from './useProfileSettings'; -export { useRegisterUser } from './useRegisterUser'; diff --git a/packages/app/hooks/user/useAuthUser.ts b/packages/app/hooks/user/useAuthUser.ts deleted file mode 100644 index 50b408754..000000000 --- a/packages/app/hooks/user/useAuthUser.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useSelector } from 'react-redux'; - -export const useAuthUser = () => { - const authUser = useSelector((state) => state.auth.user); - - return authUser; -}; diff --git a/packages/app/hooks/user/useProfile.ts b/packages/app/hooks/user/useProfile.ts index b6362abc4..07d020e9c 100644 --- a/packages/app/hooks/user/useProfile.ts +++ b/packages/app/hooks/user/useProfile.ts @@ -2,7 +2,7 @@ import { useFetchUserFavorites } from '../favorites'; import { useUserPacks } from '../packs'; import { useUserTrips } from '../singletrips'; import { useMatchesCurrentUser } from '../useMatchesCurrentUser'; -import { useAuthUser } from './useAuthUser'; +import { useAuthUser } from '../../auth/hooks'; import { useGetUser } from './useGetUser'; export const useProfile = (id = null) => { diff --git a/packages/app/hooks/user/useProfileSettings.ts b/packages/app/hooks/user/useProfileSettings.ts index 5bf796ae6..67b8c396e 100644 --- a/packages/app/hooks/user/useProfileSettings.ts +++ b/packages/app/hooks/user/useProfileSettings.ts @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import * as ImagePicker from 'expo-image-picker'; import { editUser, updatePassword } from '../../store/authStore'; -import { useAuthUser } from './useAuthUser'; +import { useAuthUser } from '../../auth/hooks'; const PROFILE_SETTINGS_DEFAULTS = { profileImage: '', diff --git a/packages/app/provider/TrpcTanstackProvider/index.tsx b/packages/app/provider/TrpcTanstackProvider/index.tsx index 8031e267a..7ccd1f9b3 100644 --- a/packages/app/provider/TrpcTanstackProvider/index.tsx +++ b/packages/app/provider/TrpcTanstackProvider/index.tsx @@ -33,7 +33,7 @@ export const TrpcTanstackProvider: React.FC<{ children: React.ReactNode }> = ({ httpBatchLink({ url: `${api}/trpc`, async headers() { - const token = await getToken('session'); + const token = await getToken(); return { authorization: token ? `Bearer ${token}` : '', }; diff --git a/packages/app/screens/LoginScreen.tsx b/packages/app/screens/LoginScreen.tsx index b87468c8d..ecd2aaed4 100644 --- a/packages/app/screens/LoginScreen.tsx +++ b/packages/app/screens/LoginScreen.tsx @@ -13,7 +13,7 @@ import { Link } from 'solito/link'; import { InformUser } from '../utils/ToastUtils'; import useTheme from '../hooks/useTheme'; import { InputText, InputTextRules } from '@packrat/ui'; -import { useGoogleAuth, useLogin } from 'app/hooks/login'; +import { useGoogleAuth, useLogin } from 'app/auth/hooks'; const demoUser = { email: 'zoot3@email.com', @@ -46,7 +46,13 @@ export default function Login() { return ( - + Welcome @@ -186,7 +192,7 @@ export default function Login() { {/* Demo Login for Development end */} - + ); diff --git a/packages/app/screens/RegisterScreen.tsx b/packages/app/screens/RegisterScreen.tsx index 00686fdab..d8ac9efa7 100644 --- a/packages/app/screens/RegisterScreen.tsx +++ b/packages/app/screens/RegisterScreen.tsx @@ -19,8 +19,7 @@ import { InformUser } from '../utils/ToastUtils'; import useTheme from '../hooks/useTheme'; import { useForm } from 'react-hook-form'; import { useSession } from '../context/Auth/SessionProvider'; -import { useGoogleAuth } from 'app/hooks/login'; -import { useRegisterUser } from 'app/hooks/user'; +import { useRegisterUser, useGoogleAuth } from 'app/auth/hooks'; export default function Register() { const { currentTheme } = useTheme(); diff --git a/packages/app/screens/feed/Feed.tsx b/packages/app/screens/feed/Feed.tsx index a1e0e42ad..b9eed42c3 100644 --- a/packages/app/screens/feed/Feed.tsx +++ b/packages/app/screens/feed/Feed.tsx @@ -22,7 +22,7 @@ import FeedSearchFilter from 'app/components/feed/FeedSearchFilter'; import { useFeed } from 'app/hooks/feed'; import { RefreshControl } from 'react-native'; import { RText } from '@packrat/ui'; -import { RootState } from 'store/store'; +import { useAuthUser } from 'app/auth/hooks'; const URL_PATHS = { userPacks: '/pack/', @@ -51,7 +51,8 @@ const Feed = ({ feedType = 'public' }) => { const [refreshing, setRefreshing] = useState(false); const dispatch = useDispatch(); - const ownerId = useSelector((state: RootState) => state.auth.user?._id); + const user = useAuthUser(); + const ownerId = user?._id; // const publicPacksData = useSelector((state) => state.feed.publicPacks); // const userPacksData = useSelector(selectAllPacks); // const publicTripsData = useSelector((state) => state.feed.publicTrips); diff --git a/packages/app/screens/trip/TripDetails.tsx b/packages/app/screens/trip/TripDetails.tsx index 7006a852c..c5f82a57e 100644 --- a/packages/app/screens/trip/TripDetails.tsx +++ b/packages/app/screens/trip/TripDetails.tsx @@ -48,7 +48,6 @@ export function TripDetails() { (state: RootState) => state.singleTrip.singleTrip, ); - // const user = useSelector((state) => state.auth.user); // check if user is owner of pack, and that pack and user exists // const isOwner = currentTrip && user && currentTrip.owner_id === user._id; diff --git a/packages/app/trpc.ts b/packages/app/trpc.ts index e25ee7e28..1a2da82f0 100644 --- a/packages/app/trpc.ts +++ b/packages/app/trpc.ts @@ -1,11 +1,11 @@ import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from 'server/src/routes/trpcRouter'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { api } from './constants/api'; import { createTRPCReact } from '@trpc/react-query'; +import { Storage } from 'app/utils/storage'; -export const getToken = async (key: string) => { - const token = await AsyncStorage.getItem(key); +export const getToken = async () => { + const token = await Storage.getItem('token'); if (!token) return ''; return token; }; @@ -15,7 +15,7 @@ export const trpc = createTRPCProxyClient({ httpBatchLink({ url: `${api}/trpc`, async headers() { - const token = await getToken('session'); + const token = await getToken(); return { authorization: token ? `Bearer ${token}` : '', }; diff --git a/packages/app/utils/storage/index.ts b/packages/app/utils/storage/index.ts new file mode 100644 index 000000000..85674ee7c --- /dev/null +++ b/packages/app/utils/storage/index.ts @@ -0,0 +1 @@ +export * from './storage'; diff --git a/packages/app/utils/storage/storage.native.ts b/packages/app/utils/storage/storage.native.ts new file mode 100644 index 000000000..2d344ea2e --- /dev/null +++ b/packages/app/utils/storage/storage.native.ts @@ -0,0 +1,29 @@ +import EventEmitter from 'events'; +import * as SecureStore from 'expo-secure-store'; + +class StorageEventEmitter extends EventEmitter {} +export const storageEvents = new StorageEventEmitter(); + +export const Storage = { + getItem: async (key) => { + try { + return await SecureStore.getItemAsync(key); + } catch (e) {} + }, + setItem: async (key, value) => { + try { + await SecureStore.setItemAsync(key, value); + storageEvents.emit('change', { key, value }); + + return true; + } catch (e) {} + }, + removeItem: async (key) => { + try { + await SecureStore.deleteItemAsync(key); + storageEvents.emit('remove', { key }); + + return true; + } catch (e) {} + }, +}; diff --git a/packages/app/utils/storage/storage.web.ts b/packages/app/utils/storage/storage.web.ts new file mode 100644 index 000000000..af19974c0 --- /dev/null +++ b/packages/app/utils/storage/storage.web.ts @@ -0,0 +1,37 @@ +import EventEmitter from 'events'; + +class StorageEventEmitter extends EventEmitter {} +export const storageEvents = new StorageEventEmitter(); + +export const Storage = { + getItem: async (key) => { + try { + return JSON.parse(window.localStorage.getItem(key)); + } catch { + console.error('Failed to load data from storage'); + } + return null; + }, + setItem: async (key, value) => { + try { + window.localStorage.setItem(key, JSON.stringify(value)); + storageEvents.emit('change', { key, value }); + + return true; + } catch { + console.error('Failed to change data from storage'); + } + return false; + }, + removeItem: async (key) => { + try { + window.localStorage.removeItem(key); + storageEvents.emit('remove', { key }); + + return true; + } catch { + console.error('Failed to remove data from storage'); + } + return false; + }, +}; diff --git a/server/src/controllers/user/getMe.ts b/server/src/controllers/user/getMe.ts index fb50cfb35..5754ff84d 100644 --- a/server/src/controllers/user/getMe.ts +++ b/server/src/controllers/user/getMe.ts @@ -1,4 +1,5 @@ -import { publicProcedure } from '../../trpc'; +import { getUserByTokenService } from '../../services/user/getUserByToken'; +import { protectedProcedure } from '../../trpc'; /** * Retrieves the user information and sends it as a response. @@ -8,14 +9,17 @@ import { publicProcedure } from '../../trpc'; */ export const getMe = async (req, res) => { try { - res.status(200).send(req.user); + const authHeader = req.headers.authorization; + const token = authHeader.split(' ')[1]; + const user = await getUserByTokenService(token); + res.status(200).send(user); } catch (err) { res.status(401).send({ message: err.message }); } }; export function getMeRoute() { - return publicProcedure.query(async (opts) => { - return opts.input; + return protectedProcedure.query((opts) => { + return opts.ctx.user; }); } diff --git a/server/src/index.ts b/server/src/index.ts index ff1997dcf..114c77047 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -14,6 +14,7 @@ import { limiter } from './helpers/limiter'; import * as trpcExpress from '@trpc/server/adapters/express'; import { type inferAsyncReturnType, initTRPC } from '@trpc/server'; import { appRouter } from './routes/trpcRouter'; +import { createContext } from './trpc'; const app = express(); @@ -57,12 +58,6 @@ app.use( next(err); }, ); -const createContext = ({ - req, - res, -}: trpcExpress.CreateExpressContextOptions) => ({ req, res }); - -export type Context = inferAsyncReturnType; app.use( '/api/trpc', diff --git a/server/src/services/user/getUserByToken.ts b/server/src/services/user/getUserByToken.ts new file mode 100644 index 000000000..d03a1b1c6 --- /dev/null +++ b/server/src/services/user/getUserByToken.ts @@ -0,0 +1,16 @@ +import User from '../../models/userModel'; + +/** + * Retrieves a user by their ID from the database. + * @param {string} token - The ID of the user. + * @return {Promise} The user object. + */ +export const getUserByTokenService = async (token: string): Promise => { + try { + const user: any = await User.findOne({ token }).lean(); + + return user; + } catch (error) { + throw new Error('User cannot be found'); + } +}; diff --git a/server/src/trpc.ts b/server/src/trpc.ts index cb00119cb..dc04f942d 100644 --- a/server/src/trpc.ts +++ b/server/src/trpc.ts @@ -1,6 +1,48 @@ import { TRPCError, initTRPC } from '@trpc/server'; +import type * as trpcExpress from '@trpc/server/adapters/express'; +import { getUserByTokenService } from './services/user/getUserByToken'; -const t = initTRPC.create(); +const t = initTRPC.context().create(); + +/** + * Create a context object that will be passed to all resolvers + */ +export const createContext = async ({ + req, + res, +}: trpcExpress.CreateExpressContextOptions) => { + // Create context based on the request object + // Will be available as `ctx` in all your resolvers + + // Extract the token from the request headers, verify it, and retrieve the user. Add the user to the context object. + const getUserFromHeader = async () => { + let user = null; + + // Extract the token from the request headers + const authHeader = req.headers.authorization || ''; + + // If the token is present, verify it + if (authHeader.startsWith('Bearer ')) { + const token = authHeader.split(' ')[1]; + + // Try to verify the token and retrieve the user + user = await getUserByTokenService(token); + } + + return user; + }; + + const user = await getUserFromHeader(); + console.log('user', user); + + return { + req, + res, + user, + }; +}; + +export type Context = Awaited>; /** * Export reusable router and procedure helpers @@ -9,3 +51,21 @@ const t = initTRPC.create(); export const router = t.router; export const middleware = t.middleware; export const publicProcedure = t.procedure; + +/** + * Authentication middleware + */ +const isAuthenticated = t.middleware(async (opts) => { + const { ctx, next } = opts; + + if (!ctx.user) { + // If the user is invalid, throw an error + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Invalid token', + }); + } + return next({ ctx: { ...ctx, user: ctx.user } }); +}); + +export const protectedProcedure = t.procedure.use(isAuthenticated);