diff --git a/app/(app)/(authorized)/(tabs)/example/user-session.tsx b/app/(app)/(authorized)/(tabs)/example/user-session.tsx new file mode 100644 index 00000000..efc8b2df --- /dev/null +++ b/app/(app)/(authorized)/(tabs)/example/user-session.tsx @@ -0,0 +1,3 @@ +import { UserSessionScreen } from '@baca/screens' + +export default UserSessionScreen diff --git a/assets/logo/logo-sygnet-dark.png b/assets/logo/logo-sygnet-dark.png index a64f209b..09ce2d5d 100644 Binary files a/assets/logo/logo-sygnet-dark.png and b/assets/logo/logo-sygnet-dark.png differ diff --git a/package.json b/package.json index 193ee4a8..73ddd681 100644 --- a/package.json +++ b/package.json @@ -133,6 +133,7 @@ "expo-web-browser": "~12.8.2", "i18next": "^23.7.20", "jotai": "^2.4.3", + "jwt-decode": "^4.0.0", "moti": "^0.25.3", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/src/api/axios/custom-instance.ts b/src/api/axios/custom-instance.ts index e289f077..5ffc03a9 100644 --- a/src/api/axios/custom-instance.ts +++ b/src/api/axios/custom-instance.ts @@ -6,7 +6,7 @@ import Axios, { AxiosError, AxiosRequestConfig } from 'axios' import i18n from 'i18next' import qs from 'qs' -import { injectTokenToRequest } from './interceptors' +import { injectTokenToRequest, signOutWhenNotAuthorized } from './interceptors' type ApiErrorType = { error: string @@ -63,6 +63,8 @@ AXIOS_INSTANCE.interceptors.response.use( return } + await signOutWhenNotAuthorized(error) + // TODO: we should handle certain error type if (errorMessage) { showErrorToast({ description: errorMessage }) diff --git a/src/api/axios/interceptors/index.ts b/src/api/axios/interceptors/index.ts index e5f78b47..93640fdf 100644 --- a/src/api/axios/interceptors/index.ts +++ b/src/api/axios/interceptors/index.ts @@ -1 +1,2 @@ export * from './injectToken' +export * from './signOutWhenNotAuthorized' diff --git a/src/api/axios/interceptors/injectToken.ts b/src/api/axios/interceptors/injectToken.ts index c30485c2..19a49fa6 100644 --- a/src/api/axios/interceptors/injectToken.ts +++ b/src/api/axios/interceptors/injectToken.ts @@ -6,8 +6,8 @@ export const injectTokenToRequest = async ( ): Promise> => { const token = await getToken() - if (token) { - config.headers['Authorization'] = `Bearer ${token}` + if (token?.accessToken) { + config.headers['Authorization'] = `Bearer ${token.accessToken}` } return config diff --git a/src/api/axios/interceptors/signOutWhenNotAuthorized.ts b/src/api/axios/interceptors/signOutWhenNotAuthorized.ts new file mode 100644 index 00000000..0a24d653 --- /dev/null +++ b/src/api/axios/interceptors/signOutWhenNotAuthorized.ts @@ -0,0 +1,45 @@ +import { + isForceUpdateNeededAtom, + isSignedInAtom, + logoutMessageShownAtom, + signOut, + store, +} from '@baca/store' +import { alert } from '@baca/utils' +import { AxiosError } from 'axios' +import { router } from 'expo-router' +import i18n from 'i18next' + +export const signOutWhenNotAuthorized = async (error: AxiosError) => { + const isSignedIn = store.get(isSignedInAtom) + + if (!isSignedIn) { + return + } + + if (!error?.config?.headers?.Authorization && error.response?.status !== 401) { + return + } + + const isTokenInvalid = error?.config?.baseURL && error.response?.status === 401 + + const isForceUpdateNeeded = store.get(isForceUpdateNeededAtom) + + const logoutMessageShown = store.get(logoutMessageShownAtom) + + if (!isForceUpdateNeeded && isTokenInvalid && !logoutMessageShown) { + store.set(logoutMessageShownAtom, true) + alert(i18n.t('alert.session_expired.title'), i18n.t('alert.session_expired.description'), [ + { + text: 'Ok', + onPress: async () => { + await signOut() + + router.replace('/sign-in') + + store.set(logoutMessageShownAtom, false) + }, + }, + ]) + } +} diff --git a/src/components/LandingHeader.tsx b/src/components/LandingHeader.tsx index f001e4ea..e28eb5af 100644 --- a/src/components/LandingHeader.tsx +++ b/src/components/LandingHeader.tsx @@ -1,8 +1,7 @@ import { lightBinarLogo, darkBinarLogo } from '@baca/constants' import { useColorScheme } from '@baca/contexts' import { Box, Button, Icon, Pressable, Spacer } from '@baca/design-system' -import { useCallback, useTranslation } from '@baca/hooks' -import { TabColorsStrings } from '@baca/navigation/tabNavigator/navigation-config' +import { useCallback, useTheme, useTranslation } from '@baca/hooks' import { isSignedInAtom } from '@baca/store/auth' import { useRouter } from 'expo-router' import { useAtomValue } from 'jotai' @@ -11,6 +10,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' export function LandingHeader() { const { colorScheme } = useColorScheme() + const { colors } = useTheme() const { top } = useSafeAreaInsets() const { t } = useTranslation() const { push, canGoBack, back } = useRouter() @@ -25,7 +25,7 @@ export function LandingHeader() { return ( { hapticImpact() }, onSuccess: async (response) => { - await setToken(response.accessToken) + const { user, ...token } = response + if (token) { + await setToken(token) + } + setIsSignedIn(true) // Send push token to backend diff --git a/src/hooks/navigation/usePreventGoBack.ts b/src/hooks/navigation/usePreventGoBack.ts index fd4415a1..00adbc86 100644 --- a/src/hooks/navigation/usePreventGoBack.ts +++ b/src/hooks/navigation/usePreventGoBack.ts @@ -40,22 +40,18 @@ export const usePreventGoBack = (preventRemove = true) => { e.preventDefault() - alert( - t('navigation.prevent_go_back_alert.title'), - t('navigation.prevent_go_back_alert.description'), - [ - { - text: t('navigation.prevent_go_back_alert.do_not_leave'), - style: 'cancel', - onPress: () => undefined, - }, - { - text: t('navigation.prevent_go_back_alert.discard'), - style: 'destructive', - onPress: () => navigation.dispatch(e?.data?.action), - }, - ] - ) + alert(t('alert.prevent_go_back.title'), t('alert.prevent_go_back.description'), [ + { + text: t('alert.prevent_go_back.do_not_leave'), + style: 'cancel', + onPress: () => undefined, + }, + { + text: t('alert.prevent_go_back.discard'), + style: 'destructive', + onPress: () => navigation.dispatch(e?.data?.action), + }, + ]) }) React.useEffect( diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index bd4cd7b4..b93855e7 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -1,4 +1,16 @@ { + "alert": { + "prevent_go_back": { + "description": "You have unsaved changes. Are you sure to discard them and leave the screen?", + "discard": "Discard", + "do_not_leave": "Don't leave", + "title": "Discard changes?" + }, + "session_expired": { + "description": "Try to log in again", + "title": "Your current session expired" + } + }, "common": { "add": "Add", "back": "Back", @@ -55,12 +67,6 @@ } }, "navigation": { - "prevent_go_back_alert": { - "description": "You have unsaved changes. Are you sure to discard them and leave the screen?", - "discard": "Discard", - "do_not_leave": "Don't leave", - "title": "Discard changes?" - }, "screen_titles": { "application_info": "ApplicationInfo", "blog": "Blog", @@ -175,6 +181,7 @@ "go_to_screen_with_BEdata": "Go to screen with data from BE", "go_to_settings": "Go to Settings", "go_to_typography": "Go to Typography", + "go_to_user_session": "Go to user session", "header": "This is Example screen" }, "forgot_password_screen": { diff --git a/src/i18n/translations/pl.json b/src/i18n/translations/pl.json index 3bb7e16a..e2d9de7a 100644 --- a/src/i18n/translations/pl.json +++ b/src/i18n/translations/pl.json @@ -1,4 +1,16 @@ { + "alert": { + "prevent_go_back": { + "description": "Masz niezapisane zmiany. Jesteś pewny, że chcesz je porzucić i wyjść?", + "discard": "Porzuć", + "do_not_leave": "Nie wychodź", + "title": "Anulować zmiany?" + }, + "session_expired": { + "description": "Spróbuj zalogować się ponownie", + "title": "Twoja sesja wygasła" + } + }, "common": { "add": "Dodaj", "back": "Cofnij", @@ -55,12 +67,6 @@ } }, "navigation": { - "prevent_go_back_alert": { - "description": "Masz niezapisane zmiany. Jesteś pewny, że chcesz je porzucić i wyjść?", - "discard": "Porzuć", - "do_not_leave": "Nie wychodź", - "title": "Anulować zmiany?" - }, "screen_titles": { "application_info": "ApplicationInfo", "blog": "Blog", @@ -166,15 +172,16 @@ "awesome": "Wspaniale 🎉" }, "examples_screen": { - "header": "To jest przykładowy widok", "go_to_application_info": "Idź do ApplicationInfo", "go_to_colors": "Idź do Kolorów", "go_to_components": "Idź do Komponentów", - "go_to_typography": "Idź do Typografii", "go_to_home_stack_details": "Idź do HomeStackDetails", - "go_to_settings": "Idź do Ustawień", + "go_to_screen_test_form": "Idź do formularza testowego", "go_to_screen_with_BEdata": "Idź do widoku z danymi z backend-u", - "go_to_screen_test_form": "Idź do formularza testowego" + "go_to_settings": "Idź do Ustawień", + "go_to_typography": "Idź do Typografii", + "go_to_user_session": "Idź do sesji użytkowania", + "header": "To jest przykładowy widok" }, "forgot_password_screen": { "back_to_login": "Wróć do logowania", diff --git a/src/navigation/tabNavigator/components/AppHeader.tsx b/src/navigation/tabNavigator/components/AppHeader.tsx index eb030c2f..ce31c2c2 100644 --- a/src/navigation/tabNavigator/components/AppHeader.tsx +++ b/src/navigation/tabNavigator/components/AppHeader.tsx @@ -1,11 +1,11 @@ import { CompanyLogo } from '@baca/components' import { isWeb } from '@baca/constants' +import { useTheme } from '@baca/hooks' import { StyleSheet, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import { TabColorsStrings } from '../navigation-config' - export function AppHeader() { + const { colors } = useTheme() const { top } = useSafeAreaInsets() if (!isWeb) return null @@ -13,7 +13,12 @@ export function AppHeader() { const height = 60 + top return ( - + ) @@ -22,7 +27,6 @@ export function AppHeader() { const jsStyles = StyleSheet.create({ appHeader: { alignItems: 'center', - borderBottomColor: TabColorsStrings.lightGray, borderBottomWidth: 1, display: 'flex', flexDirection: 'row', diff --git a/src/navigation/tabNavigator/components/BottomBar.tsx b/src/navigation/tabNavigator/components/BottomBar.tsx index 18b95580..66ed2d21 100644 --- a/src/navigation/tabNavigator/components/BottomBar.tsx +++ b/src/navigation/tabNavigator/components/BottomBar.tsx @@ -1,15 +1,15 @@ -import { useColorScheme } from '@baca/contexts' import { Icon } from '@baca/design-system' +import { useTheme } from '@baca/hooks' import cssStyles from '@baca/styles' import { Platform, StyleSheet, View } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { TabBarItemWrapper } from './TabBarItemWrapper' -import { bottomTabs, TabColors, TabColorsStrings } from '../navigation-config' +import { bottomTabs } from '../navigation-config' import { cns } from '../utils' export function BottomBar({ visible }: { visible: boolean }) { - const { colorScheme } = useColorScheme() + const { colors } = useTheme() return ( - + {bottomTabs.map((tab, i) => ( {({ focused, pressed, hovered }) => ( diff --git a/src/navigation/tabNavigator/components/SideBar.tsx b/src/navigation/tabNavigator/components/SideBar.tsx index ea94fa78..704649d7 100644 --- a/src/navigation/tabNavigator/components/SideBar.tsx +++ b/src/navigation/tabNavigator/components/SideBar.tsx @@ -1,3 +1,4 @@ +import { useTheme } from '@baca/hooks' import { signOut } from '@baca/store/auth' import cssStyles from '@baca/styles' import { Platform, StyleSheet, View } from 'react-native' @@ -5,12 +6,13 @@ import { Platform, StyleSheet, View } from 'react-native' import { HeaderLogo } from './HeaderLogo' import { SideBarTabItem } from './SideBarTabItem' import { useWidth } from '../hooks' -import { TabColorsStrings, upperSideTabs } from '../navigation-config' +import { upperSideTabs } from '../navigation-config' import { cns } from '../utils' const NAV_MEDIUM_WIDTH = 244 export function SideBar({ visible }: { visible: boolean }) { + const { colors } = useTheme() const isLarge = useWidth(1264) return ( @@ -36,6 +38,7 @@ export function SideBar({ visible }: { visible: boolean }) { }) { const isLarge = useWidth(1264) - const { colorScheme } = useColorScheme() + const { colors } = useTheme() return ( @@ -35,7 +31,7 @@ export function SideBarTabItem({ style={[ jsStyles.sidebarItemContainer, hovered && { - backgroundColor: TabColorsStrings.lightGray50, + backgroundColor: colors.bg.tertiary, }, ]} > @@ -47,30 +43,13 @@ export function SideBarTabItem({ }, ]} > - + - {children} @@ -81,7 +60,6 @@ export function SideBarTabItem({ } const jsStyles = StyleSheet.create({ - fontBold: { fontWeight: 'bold' }, sidebarIconContainer: Platform.select({ default: { padding: 0 }, web: { @@ -103,10 +81,9 @@ const jsStyles = StyleSheet.create({ }), }, sidebarItemText: { - fontSize: 16, - lineHeight: 24, marginLeft: 16, marginRight: 16, + userSelect: 'none', }, sidebarTabItem: { paddingVertical: 4, diff --git a/src/navigation/tabNavigator/navigation-config.ts b/src/navigation/tabNavigator/navigation-config.ts index 061f97d3..81ba8419 100644 --- a/src/navigation/tabNavigator/navigation-config.ts +++ b/src/navigation/tabNavigator/navigation-config.ts @@ -1,6 +1,4 @@ -import { palette } from '@baca/design-system' import { IconNames } from '@baca/types/icon' -import { hex2rgba } from '@baca/utils' type Tab = { displayedName: string @@ -64,16 +62,3 @@ export const upperSideTabs: Tabs = [ export const bottomSideTabs: Tabs = [] export const bottomTabs: Tabs = [...upperSideTabs] - -export const TabColors: Record = { - tabIconDark: 'text.brand.tertiary', - tabIconLight: 'text.success.primary', -} as const - -export const TabColorsStrings = { - lightGray: palette.gray['300'], - lightGray50: hex2rgba(palette.gray['50'], 0.5), - tabTextDark: palette.gray['700'], - tabTextLight: palette.gray['300'], - transparent: 'transparent', -} as const diff --git a/src/screens/ExamplesScreen.tsx b/src/screens/ExamplesScreen.tsx index 1434b48c..8b493f66 100644 --- a/src/screens/ExamplesScreen.tsx +++ b/src/screens/ExamplesScreen.tsx @@ -16,6 +16,7 @@ export const ExamplesScreen = () => { const goToTypography = useCallback(() => push('/example/typography'), [push]) const goToCityListScreen_EXAMPLE = useCallback(() => push('/example/data-from-be'), [push]) const goToTestForm = useCallback(() => push('/example/test-form'), [push]) + const goToUserSession = useCallback(() => push('/example/user-session'), [push]) const goToHomeStackDetails = useCallback(() => push('/home/details'), [push]) @@ -42,6 +43,9 @@ export const ExamplesScreen = () => { + ) } diff --git a/src/screens/UserSessionScreen.tsx b/src/screens/UserSessionScreen.tsx new file mode 100644 index 00000000..72aec0a8 --- /dev/null +++ b/src/screens/UserSessionScreen.tsx @@ -0,0 +1,61 @@ +import { useAuthControllerMe } from '@baca/api/query/auth/auth' +import { Box, Button, ScrollView, Text } from '@baca/design-system' +import { Token, getToken } from '@baca/services' +import { isRefreshingTokenAtom } from '@baca/store' +import { wait } from '@baca/utils' +import { useAtomValue } from 'jotai' +import { useCallback, useEffect, useState } from 'react' + +export const UserSessionScreen = () => { + const isRefreshing = useAtomValue(isRefreshingTokenAtom) + const { data, refetch, isInitialLoading, isRefetching } = useAuthControllerMe({ + query: { enabled: false }, + }) + + const [token, setToken] = useState(null) + + const fetchToken = useCallback(async () => { + const token = await getToken() + if (token) { + setToken(token) + } + }, []) + + const fetchUser = useCallback(async () => { + await refetch() + + // Refetch function could refresh token, so we are fetching it from store again + await wait(100) + await fetchToken() + }, [fetchToken, refetch]) + + useEffect(() => { + fetchToken() + }, [fetchToken]) + + return ( + + + User data: + + Is fetching user data: + {JSON.stringify(isInitialLoading || isRefetching, null, 10)} + + {JSON.stringify(data, null, 10)} + +