Skip to content
Draft
10 changes: 9 additions & 1 deletion src/app/navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ export type HeaderContentOptions = {
preventInitialFocus?: boolean
}

export type AppStackNavigationOptions = StackNavigationOptions &
HeaderContentOptions & {
/**
* Indicates whether the screen should be protected by the access-code gate, on a per-screen basis.
*/
accessCodeGate?: boolean
}

export type StackNavigationRouteConfig<R> = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: ComponentType<any>
name: keyof R
options?: StackNavigationOptions & HeaderContentOptions
options?: AppStackNavigationOptions
screenType?: 'default' | 'settings'
title?: string
}
Expand Down
123 changes: 123 additions & 0 deletions src/modules/access-code/components/AccessCodeGateProxyScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import Animated, {FadeIn} from 'react-native-reanimated'
import type {StackElement} from '@/modules/access-code/types'
import type {Route} from '@react-navigation/native'
import type {StackNavigationOptions} from '@react-navigation/stack'
import type {ComponentType} from 'react'
import type {ViewProps} from 'react-native'
import {createStackNavigator} from '@/app/navigation/createStackNavigator'
import {
type NavigationProps,
type RootStackParams,
type StackNavigationRoutes,
} from '@/app/navigation/types'
import {PleaseWait} from '@/components/ui/feedback/PleaseWait'
import {SomethingWentWrong} from '@/components/ui/feedback/SomethingWentWrong'
import {Center} from '@/components/ui/layout/Center'
import {ACCESS_CODE_SCREEN_MAP} from '@/modules/access-code/constants/accessCodeScreenMap'
import {FORGOT_CODE_SCREEN} from '@/modules/access-code/constants/forgotAccessCodeScreenConfig'
import {
AccessCodeGateStateName,
useAccessCodeGateState,
} from '@/modules/access-code/hooks/useAccessCodeGateState'
Comment thread
Copilot marked this conversation as resolved.
import {layoutStyles} from '@/styles/layoutStyles'

type AccessCodeGateProxyScreenProps<RouteName extends keyof RootStackParams> = {
ForgotCodeScreen: ComponentType
ProtectedScreenComponent:
| ComponentType<{
navigation: unknown
route: Route<Extract<RouteName, string>, RootStackParams[RouteName]>
}>
| ComponentType
loginSteps?: StackNavigationRoutes<RootStackParams>
navigatorScreenOptions?: StackNavigationOptions
state: ReturnType<typeof useAccessCodeGateState>
} & NavigationProps<RouteName> &
StackElement<'Screen'>['props']

const AccessCodeGateStack = createStackNavigator<RootStackParams>()

const AnimatedView = (layoutProps: ViewProps) => (
<Animated.View
entering={FadeIn}
style={layoutStyles.grow}
{...layoutProps}
/>
)

export const AccessCodeGateProxyScreen = <
RouteName extends keyof RootStackParams,
>({
ProtectedScreenComponent,
ForgotCodeScreen,
state,
loginSteps,
navigatorScreenOptions,
...props
}: AccessCodeGateProxyScreenProps<RouteName>) => {
if (state === AccessCodeGateStateName.loading) {
return (
<Center grow>
<PleaseWait testID="AccessCodeGateProxyScreenPleaseWait" />
</Center>
)
}

if (state === AccessCodeGateStateName.fallback) {
return (
<Center grow>
<SomethingWentWrong testID="AccessCodeGateProxyScreenSomethingWentWrong" />
</Center>
)
}

if (state === AccessCodeGateStateName.allowed) {
return (
<AnimatedView>
<ProtectedScreenComponent {...props} />
</AnimatedView>
)
}

return (
<AccessCodeGateStack.Navigator
layout={AnimatedView}
screenOptions={{...navigatorScreenOptions, headerShown: true}}>
{state === AccessCodeGateStateName.biometricsPermission && (
<AccessCodeGateStack.Screen
{...ACCESS_CODE_SCREEN_MAP.biometricsPermission}
/>
)}

{state === AccessCodeGateStateName.forgotCode && (
<AccessCodeGateStack.Screen
{...FORGOT_CODE_SCREEN}
component={ForgotCodeScreen}
/>
)}

{state === AccessCodeGateStateName.invalid && (
<AccessCodeGateStack.Screen {...ACCESS_CODE_SCREEN_MAP.invalid} />
)}

{state === AccessCodeGateStateName.accessCode && (
<AccessCodeGateStack.Screen {...ACCESS_CODE_SCREEN_MAP.accessCode} />
)}

{state === AccessCodeGateStateName.setup && (
<AccessCodeGateStack.Group screenOptions={navigatorScreenOptions}>
{!!loginSteps &&
Object.entries(loginSteps).map(([key, route]) => (
<AccessCodeGateStack.Screen
key={key}
{...route}
/>
))}

<AccessCodeGateStack.Screen {...ACCESS_CODE_SCREEN_MAP.setup} />
<AccessCodeGateStack.Screen {...ACCESS_CODE_SCREEN_MAP.confirm} />
</AccessCodeGateStack.Group>
)}
</AccessCodeGateStack.Navigator>
)
}
74 changes: 74 additions & 0 deletions src/modules/access-code/constants/accessCodeScreenMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {TransitionPresets} from '@react-navigation/stack'
import type {StackFactory} from '@/modules/access-code/types'
import type {ComponentProps} from 'react'
import {FORGOT_CODE_SCREEN} from '@/modules/access-code/constants/forgotAccessCodeScreenConfig'
import {AccessCodeGateStateName} from '@/modules/access-code/hooks/useAccessCodeGateState'
import {AccessCodeRouteName} from '@/modules/access-code/routes'
import {AccessCodeScreen} from '@/modules/access-code/screens/AccessCode.screen'
import {AccessCodeInvalidScreen} from '@/modules/access-code/screens/AccessCodeInvalid.screen'
import {BiometricsPermissionScreen} from '@/modules/access-code/screens/BiometricsPermission.screen'
import {ConfirmAccessCodeScreen} from '@/modules/access-code/screens/ConfirmAccessCode.screen'
import {SetAccessCodeScreen} from '@/modules/access-code/screens/SetAccessCode.screen'

enum AccessCodeGateRouteName {
fallback = 'AccessCodeGateFallback',
loading = 'AccessCodeGateLoading',
}

export const ACCESS_CODE_SCREEN_MAP: Record<
Exclude<AccessCodeGateStateName, 'allowed'>,
ComponentProps<StackFactory['Screen']>
> = {
[AccessCodeGateStateName.biometricsPermission]: {
component: BiometricsPermissionScreen,
name: AccessCodeRouteName.biometricsPermission,
options: {
headerTitle: 'Sneller toegang',
},
},
[AccessCodeGateStateName.forgotCode]: {
component: FORGOT_CODE_SCREEN.component,
name: AccessCodeRouteName.forgotAccessCode,
options: {headerTitle: 'Toegangscode vergeten'},
},
[AccessCodeGateStateName.invalid]: {
component: AccessCodeInvalidScreen,
name: AccessCodeRouteName.accessCodeInvalid,
},
[AccessCodeGateStateName.accessCode]: {
component: AccessCodeScreen as never,
name: AccessCodeRouteName.accessCode,
options: {
headerTitle: 'Toegangscode invoeren',
...TransitionPresets.ModalFadeTransition,
},
},
[AccessCodeGateStateName.setup]: {
component: SetAccessCodeScreen as never,
name: AccessCodeRouteName.setAccessCode,
options: {
headerTitle: 'Toegangscode kiezen',
},
},
[AccessCodeGateStateName.confirm]: {
component: ConfirmAccessCodeScreen as never,
name: AccessCodeRouteName.confirmAccessCode,
options: {
headerTitle: 'Toegangscode herhalen',
},
},
[AccessCodeGateStateName.loading]: {
children: () => null,
name: AccessCodeGateRouteName.loading,
options: {
...TransitionPresets.ModalFadeTransition,
},
},
[AccessCodeGateStateName.fallback]: {
children: () => null,
name: AccessCodeGateRouteName.fallback,
options: {
...TransitionPresets.ModalFadeTransition,
},
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {AccessCodeRouteName} from '@/modules/access-code/routes'
import {ForgotAccessCodeScreen} from '@/modules/access-code/screens/ForgotAccessCode.screen'

export const FORGOT_CODE_SCREEN = {
component: ForgotAccessCodeScreen,
name: AccessCodeRouteName.forgotAccessCode,
options: {headerTitle: 'Toegangscode vergeten'},
}
Loading
Loading