diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 70ddd3bc00..354a5baf15 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -30,6 +30,7 @@ import {logger as notyLogger} from '#/lib/notifications/util' import {buildStateObject} from '#/lib/routes/helpers' import { type AllNavigatorParams, + type AuthNavigatorParams, type BottomTabNavigatorParams, type CoreNavigatorParams, type FlatNavigatorParams, @@ -149,10 +150,12 @@ import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback' import {useLoggedOutViewControls} from './state/shell/logged-out' import {useLoggedOutView} from './state/shell/logged-out' import {useCloseAllActiveElements} from './state/util' +import {AuthModal} from './view/shell/desktop/AuthLayout' const navigationRef = createNavigationContainerRef() const Core = createNativeStackNavigator() +const Auth = createNativeStackNavigator() const HomeTab = createNativeStackNavigatorWithAuth() const SearchTab = createNativeStackNavigatorWithAuth() const NotificationsTab = @@ -778,10 +781,74 @@ const FlatNavigator = () => { options={{title: title(msg`Home`)}} /> {commonScreens(Flat, numUnread)} + AuthNavigator} + options={{ + title: title(msg`Login or Sign Up`), + presentation: 'modal', + }} + /> ) } +function AuthNavigator() { + const t = useTheme() + return ( + + {authScreens({navigator: Auth})} + + ) +} + +function authScreens({navigator: Navigator}: {navigator: typeof Auth}) { + return ( + <> + LandingScreen} + options={{animation: 'fade', animationDuration: 200}} + /> + StarterPackLandingScreen} + /> + + SelectAccountScreen} + /> + SignInScreen} /> + ForgotPasswordScreen} + /> + SetNewPasswordScreen} + /> + PasswordUpdatedScreen} + /> + SignUpInfoScreen} + /> + SignUpHandleScreen} + /> + SignUpCaptchaScreen} + /> + + + ) +} + /** * The core navigator on native handles authentication, switching between * the main tab navigator when logged in and the auth screen when logged out. @@ -800,48 +867,7 @@ export const NativeNavigator = () => { options={{animation: 'fade', animationDuration: 200}} /> ) : ( - <> - LandingScreen} - options={{animation: 'fade', animationDuration: 200}} - /> - StarterPackLandingScreen} - /> - - SelectAccountScreen} - /> - SignInScreen} /> - ForgotPasswordScreen} - /> - SetNewPasswordScreen} - /> - PasswordUpdatedScreen} - /> - SignUpInfoScreen} - /> - SignUpHandleScreen} - /> - SignUpCaptchaScreen} - /> - - + authScreens({navigator: Core as typeof Auth}) )} ) diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 5891ca863a..4be9fdc65d 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -13,6 +13,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {isWeb} from '#/platform/detection' import {useShellLayout} from '#/state/shell/shell-layout' +import {useIsWithinAuthLayout} from '#/view/shell/desktop/AuthLayout' import { atoms as a, useBreakpoints, @@ -38,16 +39,24 @@ export type ScreenProps = React.ComponentProps & { export const Screen = memo(function Screen({ style, noInsetTop, + children, ...props }: ScreenProps) { const {top} = useSafeAreaInsets() + const withinAuthLayout = useIsWithinAuthLayout() + + if (withinAuthLayout) { + return children + } + return ( <> {isWeb && } + {...props}> + {children} + ) }) diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index c1a63c48d2..252cf3cc9e 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -119,6 +119,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Feeds: undefined Notifications: undefined Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} + Auth: undefined } export type AllNavigatorParams = CommonNavigatorParams & { diff --git a/src/routes.ts b/src/routes.ts index 7fc673e2be..296b532ff5 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -3,7 +3,7 @@ import {type FlatNavigatorParams} from './lib/routes/types' type AllNavigatableRoutes = Omit< FlatNavigatorParams, - 'NotFound' | 'SharedPreferencesTester' + 'Auth' | 'NotFound' | 'SharedPreferencesTester' > export const router = new Router({ diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx index 284dfea90e..323afa239f 100644 --- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx +++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import {useCallback, useEffect} from 'react' import {View} from 'react-native' // Based on @react-navigation/native-stack/src/navigators/createNativeStackNavigator.ts // MIT License @@ -6,6 +6,7 @@ import {View} from 'react-native' import { createNavigatorFactory, type EventArg, + type NavigationProp, type NavigatorTypeBagBase, type ParamListBase, type StackActionHelpers, @@ -15,6 +16,8 @@ import { type StackRouterOptions, type StaticConfig, type TypedNavigator, + useFocusEffect, + useNavigation, useNavigationBuilder, } from '@react-navigation/native' import {NativeStackView} from '@react-navigation/native-stack' @@ -27,14 +30,11 @@ import { import {PWI_ENABLED} from '#/lib/build-flags' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' +import {type FlatNavigatorParams} from '#/lib/routes/types' import {isWeb} from '#/platform/detection' import {useSession} from '#/state/session' import {useOnboardingState} from '#/state/shell' -import { - useLoggedOutView, - useLoggedOutViewControls, -} from '#/state/shell/logged-out' -import {LoggedOut} from '#/view/com/auth/LoggedOut' +import {useLoggedOutView} from '#/state/shell/logged-out' import {Deactivated} from '#/screens/Deactivated' import {Onboarding} from '#/screens/Onboarding' import {SignupQueued} from '#/screens/SignupQueued' @@ -76,7 +76,7 @@ function NativeStackNavigator({ screenLayout, }) - React.useEffect( + useEffect( () => // @ts-expect-error: there may not be a tab navigator in parent navigation?.addListener?.('tabPress', (e: any) => { @@ -108,23 +108,40 @@ function NativeStackNavigator({ const activeDescriptor = descriptors[activeRoute.key] const activeRouteRequiresAuth = activeDescriptor.options.requireAuth ?? false const onboardingState = useOnboardingState() - const {showLoggedOut} = useLoggedOutView() - const {setShowLoggedOut} = useLoggedOutViewControls() const {isMobile} = useWebMediaQueries() const {leftNavMinimal} = useLayoutBreakpoints() - // Temp: use old system for web. TODO: unify - if (isWeb && !hasSession && (!PWI_ENABLED || activeRouteRequiresAuth)) { - return - } + const {showLoggedOut} = useLoggedOutView() + + const shouldRedirectToAuth = + isWeb && activeRoute.name !== 'Auth' && (showLoggedOut || !PWI_ENABLED) + useEffect(() => { + if (shouldRedirectToAuth) { + navigation.navigate('Auth') + } + }, [navigation, shouldRedirectToAuth]) + + const shouldRedirectAwayFromAuth = + isWeb && + activeRoute.name === 'Auth' && + !showLoggedOut && + PWI_ENABLED && + !activeRouteRequiresAuth + useEffect(() => { + if (shouldRedirectAwayFromAuth) { + if (navigation.canGoBack()) { + navigation.goBack() + } else { + navigation.replace('Home') + } + } + }, [navigation, shouldRedirectAwayFromAuth]) + if (hasSession && currentAccount?.signupQueued) { return } if (hasSession && currentAccount?.status === 'takendown') { return } - if (isWeb && showLoggedOut) { - return setShowLoggedOut(false)} /> - } if (currentAccount?.status === 'deactivated') { return } @@ -139,7 +156,7 @@ function NativeStackNavigator({ ...descriptor, render() { if (requireAuth && !hasSession) { - return + return } else { return descriptor.render() } @@ -158,7 +175,7 @@ function NativeStackNavigator({ {...rest} state={state} navigation={navigation} - descriptors={descriptors} + descriptors={newDescriptors} describe={describe} /> @@ -172,6 +189,25 @@ function NativeStackNavigator({ ) } +/** + * Redirects to the auth screen on web. On native, this is handled by swapping + * out the screens instead - see `NativeNavigator` + */ +function RedirectToAuth() { + const navigation = useNavigation>() + useFocusEffect( + useCallback(() => { + if (isWeb) { + const timeout = setTimeout(() => { + navigation.navigate('Auth') + }, 500) + return () => clearTimeout(timeout) + } + }, [navigation]), + ) + return +} + export function createNativeStackNavigatorWithAuth< const ParamList extends ParamListBase, const NavigatorID extends string | undefined = undefined, diff --git a/src/view/shell/desktop/AuthLayout.native.tsx b/src/view/shell/desktop/AuthLayout.native.tsx new file mode 100644 index 0000000000..727c2a7406 --- /dev/null +++ b/src/view/shell/desktop/AuthLayout.native.tsx @@ -0,0 +1,7 @@ +export function AuthLayout({children}: {children: React.ReactNode}) { + return children +} + +export function useIsWithinAuthLayout() { + return false +} diff --git a/src/view/shell/desktop/AuthLayout.tsx b/src/view/shell/desktop/AuthLayout.tsx new file mode 100644 index 0000000000..90ea576ab7 --- /dev/null +++ b/src/view/shell/desktop/AuthLayout.tsx @@ -0,0 +1,119 @@ +import {createContext, useContext} from 'react' +import {Pressable, View} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {DismissableLayer, FocusGuards, FocusScope} from 'radix-ui/internal' +import {createPortal} from 'react-dom' +import {RemoveScrollBar} from 'react-remove-scroll-bar' + +import {useA11y} from '#/state/a11y' +import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {atoms as a, flatten, useTheme, web} from '#/alf' + +const stopPropagation = (e: any) => e.stopPropagation() +const preventDefault = (e: any) => e.preventDefault() + +const WithinAuthLayoutContext = createContext(false) + +export function AuthModal({children}: {children: React.ReactNode}) { + const t = useTheme() + const {_} = useLingui() + const {reduceMotionEnabled} = useA11y() + const {setShowLoggedOut} = useLoggedOutViewControls() + FocusGuards.useFocusGuards() + + console.log('auth modal') + + return createPortal( + setShowLoggedOut(false)}> + + + + + + true} + onTouchEnd={stopPropagation} + style={flatten([ + a.relative, + a.rounded_md, + a.w_full, + a.border, + t.atoms.bg, + { + minHeight: '100%', + maxWidth: 600, + borderColor: t.palette.contrast_200, + shadowColor: t.palette.black, + shadowOpacity: t.name === 'light' ? 0.1 : 0.4, + shadowRadius: 30, + }, + !reduceMotionEnabled && a.zoom_fade_in, + a.p_xl, + ])}> + + + {children} + + + + + + + , + document.querySelector('#root')!, + ) +} + +function Backdrop() { + const t = useTheme() + const {reduceMotionEnabled} = useA11y() + return ( + + + + ) +} + +export function useIsWithinAuthLayout() { + return useContext(WithinAuthLayoutContext) +}