diff --git a/app/(app)/(authorized)/_layout.tsx b/app/(app)/(authorized)/_layout.tsx index efe1be08..7dca3eed 100644 --- a/app/(app)/(authorized)/_layout.tsx +++ b/app/(app)/(authorized)/_layout.tsx @@ -1,13 +1,14 @@ import { Redirect, Stack } from 'expo-router' +import { useAtomValue } from 'jotai' -import { useAuth } from '~hooks' +import { isSignedInAtom } from '~store/auth' export const unstable_settings = { initialRouteName: '(tabs)', } export default function AuthorizedLayout() { - const { isSignedIn } = useAuth() + const isSignedIn = useAtomValue(isSignedInAtom) if (isSignedIn === false) { return diff --git a/app/(app)/(not-authorized)/_layout.tsx b/app/(app)/(not-authorized)/_layout.tsx index 2eca7350..f545ee2f 100644 --- a/app/(app)/(not-authorized)/_layout.tsx +++ b/app/(app)/(not-authorized)/_layout.tsx @@ -1,13 +1,13 @@ import { Redirect, Stack } from 'expo-router' +import { useAtomValue } from 'jotai' -import { useAuth } from '~hooks' - +import { isSignedInAtom } from '~store/auth' export const unstable_settings = { initialRouteName: 'sign-in', } export default function NotAuthorizedLayout() { - const { isSignedIn } = useAuth() + const isSignedIn = useAtomValue(isSignedInAtom) if (isSignedIn === true) { return diff --git a/app/_layout.tsx b/app/_layout.tsx index 0be0a5c3..8fbaf315 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,16 +1,18 @@ import { ThemeProvider } from '@react-navigation/native' import { Slot } from 'expo-router' +import { useAtomValue } from 'jotai' import { AbsoluteFullFill, Loader, StatusBar } from '~components' -import { useAuth, useNavigationTheme, useRouterNotifications } from '~hooks' +import { useNavigationTheme, useRouterNotifications } from '~hooks' import { Providers } from '~providers' +import { isSignedInAtom } from '~store/auth' export const unstable_settings = { initialRouteName: 'index', } const Layout = () => { - const { isSignedIn } = useAuth() + const isSignedIn = useAtomValue(isSignedInAtom) const { navigationTheme } = useNavigationTheme() useRouterNotifications() // TODO: check if handling notification deeplinks works correctly diff --git a/app/index.tsx b/app/index.tsx index 156571bb..1c450294 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -1,11 +1,12 @@ import { Redirect } from 'expo-router' +import { useAtomValue } from 'jotai' import { Platform } from 'react-native' -import { useAuth } from '~hooks' import { LandingScreen } from '~screens/LandingScreen' +import { isSignedInAtom } from '~store/auth' export default function Root() { - const { isSignedIn } = useAuth() + const isSignedIn = useAtomValue(isSignedInAtom) if (isSignedIn === true) { return diff --git a/package.json b/package.json index c0354eb0..4b6029f7 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "expo-updates": "~0.24.8", "expo-web-browser": "~12.8.2", "i18next": "^23.7.20", + "jotai": "^2.4.3", "moti": "^0.25.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/src/components/AppLoading.tsx b/src/components/AppLoading.tsx index 5dc39826..6c4d248b 100644 --- a/src/components/AppLoading.tsx +++ b/src/components/AppLoading.tsx @@ -1,10 +1,12 @@ import * as SplashScreen from 'expo-splash-screen' +import { useAtomValue } from 'jotai' import { FC, PropsWithChildren, useCallback, useEffect } from 'react' import { View, StyleSheet } from 'react-native' import { AbsoluteFullFill, Loader, Center } from './atoms' -import { useAuth, useBoolean, useCachedResources, useFonts } from '~hooks' +import { useBoolean, useCachedResources, useFonts } from '~hooks' +import { isSignedInAtom } from '~store/auth' SplashScreen.preventAutoHideAsync() @@ -21,7 +23,8 @@ export const AppLoading: FC = ({ children }) => { // Delay loading logic was made to prevent displaying empty screen after splash screen will hide const [isDelayLoading, setIsDelayLoading] = useBoolean(true) - const { isSignedIn } = useAuth() + + const isSignedIn = useAtomValue(isSignedInAtom) useEffect(() => { async function prepare() { diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a2fdde7e..f2a539b1 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,23 +1,13 @@ import { NativeStackHeaderProps } from '@react-navigation/native-stack' import { useRouter } from 'expo-router' -import { Touchable } from './atoms' - -import { Box, Column, Row, Icon, Text } from '~components/atoms' +import { Box, Column, Row, Icon, Text, Touchable } from '~components/atoms' const logoHeight = 24 -export const Header = ({ options, ...rest }: NativeStackHeaderProps) => { +export const Header = ({ options }: NativeStackHeaderProps) => { const router = useRouter() - console.log('options', { - options, - rest, - canGoBack: router.canGoBack(), - restxd: rest.navigation.getState(), - canGoBack2: rest.navigation.canGoBack(), - }) - return ( diff --git a/src/components/LandingHeader.tsx b/src/components/LandingHeader.tsx index 63adf02d..3932041e 100644 --- a/src/components/LandingHeader.tsx +++ b/src/components/LandingHeader.tsx @@ -1,20 +1,21 @@ import { useRouter } from 'expo-router' +import { useAtomValue } from 'jotai' import { Image, StyleSheet, Platform, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { Box, Button, Icon, Pressable } from '~components' import { darkLogoFull, lightLogoFull } from '~constants' import { useColorScheme } from '~contexts' -import { useAuth, useTranslation } from '~hooks' +import { useTranslation } from '~hooks' import { TabColorsStrings } from '~navigation/tabNavigator/config' - +import { isSignedInAtom } from '~store/auth' export function LandingHeader() { const { colorScheme } = useColorScheme() const { top } = useSafeAreaInsets() const { t } = useTranslation() const { push, canGoBack, back } = useRouter() - const { isSignedIn } = useAuth() + const isSignedIn = useAtomValue(isSignedInAtom) const navigateToLogin = () => push('/sign-in') diff --git a/src/constants/theme.ts b/src/constants/theme.ts index 43d7c2bf..ba143d42 100644 --- a/src/constants/theme.ts +++ b/src/constants/theme.ts @@ -356,8 +356,8 @@ export const theme = { export const lightNavigationTheme: Theme = { colors: { background: themeColors.lightMode.bg.primary, - border: themeColors.lightMode.border.primary, - card: themeColors.lightMode.button.primary.bg, + border: 'transparent', + card: themeColors.lightMode.bg.primary, text: themeColors.lightMode.alpha.black[70], notification: themeColors.lightMode.avatar.bg, primary: themeColors.lightMode.utility.purple[500], @@ -368,8 +368,8 @@ export const lightNavigationTheme: Theme = { export const darkNavigationTheme: Theme = { colors: { background: themeColors.darkMode.bg.primary, - border: themeColors.darkMode.border.primary, - card: themeColors.darkMode.button.primary.bg, + border: 'transparent', + card: themeColors.darkMode.bg.primary, text: themeColors.darkMode.alpha.black[70], notification: themeColors.darkMode.avatar.bg, primary: themeColors.darkMode.utility.purple[500], diff --git a/src/contexts/AuthContext.ts b/src/contexts/AuthContext.ts deleted file mode 100644 index 0aa12d8f..00000000 --- a/src/contexts/AuthContext.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createRef, MutableRefObject } from 'react' - -import { SignInFormValues, SignUpFormValues } from '~types/authForms' -import createGenericContext from '~utils/createGenericContext' - -// TODO: modify return options from signIn, signOut, signUp and add sendPasswordResetEmail and confirmPasswordReset functions -export type AuthContextType = { - isSignedIn: boolean | null - signIn: (data: SignInFormValues) => void - signOut: () => void - signUp: (data: SignUpFormValues) => void -} - -export const [useAuthContext, AuthContextProvider] = - createGenericContext('AuthContext') - -export const authContextRef: MutableRefObject = createRef() diff --git a/src/contexts/index.ts b/src/contexts/index.ts index a595c424..ba71af47 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,3 +1,2 @@ -export * from './AuthContext' export * from './NotificationContext' export * from './ColorSchemeContext' diff --git a/src/hooks/forms/useSignInForm.ts b/src/hooks/forms/useSignInForm.ts index a7065f23..6e08a527 100644 --- a/src/hooks/forms/useSignInForm.ts +++ b/src/hooks/forms/useSignInForm.ts @@ -1,12 +1,13 @@ import { isError } from '@tanstack/react-query' +import { useSetAtom } from 'jotai' import { useState } from 'react' import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { useAuth } from '../useAuth' - +import { setToken } from '~services' +import { isSignedInAtom } from '~store/auth' import { SignInFormValues } from '~types/authForms' -import { hapticImpact } from '~utils' +import { hapticImpact, wait } from '~utils' const defaultValues: SignInFormValues = { // TODO: Reset this values when building production app @@ -16,11 +17,13 @@ const defaultValues: SignInFormValues = { } export const useSignInForm = () => { - const { signIn } = useAuth() const [error, setError] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) + const { t } = useTranslation() + const setIsSignedIn = useSetAtom(isSignedInAtom) + const { control, formState: { errors }, @@ -35,7 +38,18 @@ export const useSignInForm = () => { try { setIsSubmitting(true) setError('') - await signIn(data) + // Errors are handled on UI side + // if you want to stop this function with error just throw new Error. + // Remember to pass readable error message for user, because this error will be displayed for him + + // TODO: Add some backend call here, you can use react query for this + await wait(500) + + if (data.email !== 'test@example.com' || data.password !== '123456') { + throw new Error('Incorrect email or password') + } + await setToken('token_here') + setIsSignedIn(true) } catch (e) { if (isError(e)) { setError(e.message) diff --git a/src/hooks/forms/useSignUpForm.ts b/src/hooks/forms/useSignUpForm.ts index 5c2f179b..5de48f4c 100644 --- a/src/hooks/forms/useSignUpForm.ts +++ b/src/hooks/forms/useSignUpForm.ts @@ -1,12 +1,12 @@ import { isError } from '@tanstack/react-query' +import { useSetAtom } from 'jotai' import { useState } from 'react' import { useForm } from 'react-hook-form' import { useTranslation } from 'react-i18next' -import { useAuth } from '../useAuth' - +import { isSignedInAtom } from '~store/auth' import { SignUpFormValues } from '~types/authForms' -import { hapticImpact } from '~utils' +import { hapticImpact, wait } from '~utils' const defaultValues: SignUpFormValues = { user: '', @@ -17,11 +17,13 @@ const defaultValues: SignUpFormValues = { } export const useSignUpForm = () => { - const { signUp } = useAuth() const [error, setError] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) + const { t } = useTranslation() + const setIsSignedIn = useSetAtom(isSignedInAtom) + const { control, formState: { errors }, @@ -36,7 +38,9 @@ export const useSignUpForm = () => { try { setIsSubmitting(true) setError('') - await signUp(data) + await wait(500) + // TODO: Add some backend call here, you can use react query for this + setIsSignedIn(true) } catch (e) { if (isError(e)) { setError(e.message) diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c2a1c702..0635df19 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -57,7 +57,6 @@ export * from './navigation' // Custom hooks implemented in app export * from './useAppStateActive' -export * from './useAuth' export * from './useBoolean' export * from './useCachedResources' export * from './useKeyboardHeight' diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts deleted file mode 100644 index cbfe7d29..00000000 --- a/src/hooks/useAuth.ts +++ /dev/null @@ -1,2 +0,0 @@ -// This was made to prevent require cycle in the app -export { useAuthContext as useAuth } from '~contexts' diff --git a/src/logic/AuthLogic.tsx b/src/logic/AuthLogic.tsx new file mode 100644 index 00000000..692f26bc --- /dev/null +++ b/src/logic/AuthLogic.tsx @@ -0,0 +1,22 @@ +import { useSetAtom } from 'jotai' +import { FC } from 'react' + +import { useEffect } from '~hooks' +import { getToken } from '~services' +import { isSignedInAtom } from '~store/auth' + +export const AuthLogic: FC = () => { + const setIsSignedIn = useSetAtom(isSignedInAtom) + + useEffect(() => { + const bootstrap = async () => { + // TODO: This should be moved to backend calls, in this bootstrap function we should fetch user info and not token + const token = await getToken() + setIsSignedIn(!!token) + } + + bootstrap() + }, [setIsSignedIn]) + + return null +} diff --git a/src/logic/index.ts b/src/logic/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/navigation/tabNavigator/components/SideBar.tsx b/src/navigation/tabNavigator/components/SideBar.tsx index 8c480e91..1da2dff0 100644 --- a/src/navigation/tabNavigator/components/SideBar.tsx +++ b/src/navigation/tabNavigator/components/SideBar.tsx @@ -6,14 +6,13 @@ import { TabColorsStrings, upperSideTabs } from '../config' import { useWidth } from '../hooks' import { cns } from '../utils' -import { useAuth } from '~hooks' +import { signOut } from '~store/auth' import cssStyles from '~styles' const NAV_MEDIUM_WIDTH = 244 export function SideBar({ visible }: { visible: boolean }) { const isLarge = useWidth(1264) - const { signOut } = useAuth() return ( = ({ children }) => { - const [isSignedIn, setIsSignedIn] = useState(null) - - useEffect(() => { - const bootstrap = async () => { - // TODO: This should be moved to backend calls, in this bootstrap function we should fetch user info and not token - const token = await getToken() - setIsSignedIn(!!token) - } - - bootstrap() - }, []) - - const signIn: AuthContextType['signIn'] = useCallback(async (data) => { - // Errors are handled on UI side - // if you want to stop this function with error just throw new Error. - // Remember to pass readable error message for user, because this error will be displayed for him - await wait(500) - - if (data.email !== 'test@example.com' || data.password !== '123456') { - throw new Error('Incorrect email or password') - } - await setToken('token_here') - setIsSignedIn(true) - }, []) - - const signOut = useCallback(async () => { - await deleteToken() - setIsSignedIn(false) - }, []) - - const signUp = useCallback(async (data: SignUpFormValues) => { - // temporary solution - console.log(data) - await wait(500) - setIsSignedIn(true) - }, []) - - const value = useMemo(() => { - return { - isSignedIn, - signIn, - signOut, - signUp, - } - }, [isSignedIn, signIn, signOut, signUp]) - - authContextRef.current = value - - return {children} -} diff --git a/src/providers/Providers.tsx b/src/providers/Providers.tsx index d9548d3c..510d9c85 100644 --- a/src/providers/Providers.tsx +++ b/src/providers/Providers.tsx @@ -1,18 +1,20 @@ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import { PortalProvider } from '@gorhom/portal' import { QueryClientProvider, QueryClient } from '@tanstack/react-query' +import { Provider } from 'jotai' import { FC, PropsWithChildren } from 'react' import { StyleSheet } from 'react-native' import { GestureHandlerRootView } from 'react-native-gesture-handler' import { SafeAreaProvider } from 'react-native-safe-area-context' -import { AuthProvider } from './AuthProvider' import { ColorSchemeProvider } from './ColorSchemeProvider' import { NotificationsProvider } from './NotificatedProvider' import { NotificationProvider as ExpoNotificationsProvider } from './NotificationProvider' import { AppLoading } from '~components' import { useAppStateActive } from '~hooks' +import { AuthLogic } from '~logic/AuthLogic' +import { store } from '~store' import { checkForUpdates } from '~utils' const queryClient = new QueryClient({}) @@ -25,18 +27,19 @@ export const Providers: FC = ({ children }) => { - - {/* @ts-expect-error: error comes from a react-native-notificated library which doesn't have declared children in types required in react 18 */} - - - + + + {/* @ts-expect-error: error comes from a react-native-notificated library which doesn't have declared children in types required in react 18 */} + + {children} - - - - + + + + + diff --git a/src/providers/index.ts b/src/providers/index.ts index 1d89e6ff..dd3bcfff 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,5 +1,4 @@ export { SafeAreaProvider } from 'react-native-safe-area-context' -export * from './AuthProvider' export * from './NotificatedProvider' export * from './NotificationProvider' export * from './Providers' diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index a5610fc3..e88abfa6 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -1,7 +1,8 @@ import { Version, Spacer, Button, Center, Text, ScrollView } from '~components' import { colorSchemesList } from '~constants' import { useColorScheme } from '~contexts' -import { useAuth, useCallback, useScreenOptions, useTranslation } from '~hooks' +import { useCallback, useScreenOptions, useTranslation } from '~hooks' +import { signOut } from '~store/auth' import { noop } from '~utils' export const SettingsScreen = (): JSX.Element => { @@ -12,8 +13,6 @@ export const SettingsScreen = (): JSX.Element => { title: t('navigation.screen_titles.settings'), }) - const { signOut } = useAuth() - const handleColorSchemeSettingChange = useCallback( (scheme: typeof colorSchemeSetting) => () => { setColorSchemeSetting(scheme) diff --git a/src/store/auth/authActions.ts b/src/store/auth/authActions.ts new file mode 100644 index 00000000..c3c3c66c --- /dev/null +++ b/src/store/auth/authActions.ts @@ -0,0 +1,9 @@ +import { isSignedInAtom } from './authState' + +import { deleteToken } from '~services' +import { store } from '~store/store' + +export async function signOut() { + await deleteToken() + store.set(isSignedInAtom, false) +} diff --git a/src/store/auth/authState.ts b/src/store/auth/authState.ts new file mode 100644 index 00000000..2c75bb7a --- /dev/null +++ b/src/store/auth/authState.ts @@ -0,0 +1,3 @@ +import { atom } from 'jotai' + +export const isSignedInAtom = atom(null) diff --git a/src/store/auth/index.ts b/src/store/auth/index.ts new file mode 100644 index 00000000..b221e524 --- /dev/null +++ b/src/store/auth/index.ts @@ -0,0 +1,2 @@ +export * from './authState' +export * from './authActions' diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 00000000..16c86332 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1 @@ +export * from './store' diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 00000000..1ce2b50f --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,3 @@ +import { createStore } from 'jotai' + +export const store = createStore() diff --git a/src/utils/testUtils.tsx b/src/utils/testUtils.tsx index 37666204..617d7e6e 100644 --- a/src/utils/testUtils.tsx +++ b/src/utils/testUtils.tsx @@ -4,18 +4,15 @@ import { PropsWithChildren, ReactElement } from 'react' import { I18nextProvider } from 'react-i18next' import i18n from '~i18n/i18nForTests' -import { AuthProvider } from '~providers' import { ColorSchemeProvider } from '~providers/ColorSchemeProvider' type RenderOptions = Parameters[1] const ProvidersWrapper: React.FC = ({ children }) => ( - - - {children} - - + + {children} + ) diff --git a/yarn.lock b/yarn.lock index 71bd7153..8e5df435 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8373,6 +8373,11 @@ join-component@^1.1.0: resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" integrity sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ== +jotai@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.4.3.tgz#a8eff8ca6de968d6a04616329dd1335ce52e70f3" + integrity sha512-CSAHX9LqWG5WCrU8OgBoZbBJ+Bo9rQU0mPusEF4e0CZ/SNFgurG26vb3UpgvCSJZgYVcUQNiUBM5q86PA8rstQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"