Skip to content

Commit 6377f5a

Browse files
Merged PR 40481: Create login flow for parking module
Create login flow for parking module Related work items: #138232
2 parents 6a4d020 + b00b179 commit 6377f5a

24 files changed

+624
-26
lines changed

src/components/ui/forms/TextInput.tsx

+1-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
StyleSheet,
55
TextInput as TextInputRN,
66
TextInputProps,
7-
TextInputProps as TextInputRNProps,
87
View,
98
} from 'react-native'
109
import {IconButton} from '@/components/ui/buttons/IconButton'
@@ -21,7 +20,7 @@ type Props = {
2120
onFocus?: () => void
2221
placeholder?: string
2322
warning?: boolean
24-
} & TextInputRNProps
23+
} & TextInputProps
2524

2625
export const TextInput = forwardRef<TextInputRN, Props>(
2726
(

src/components/ui/forms/TextInputField.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type Props = {
1111
label: string
1212
maxCharacters?: number
1313
numberOfLines?: number
14-
placeholder: string
14+
placeholder?: string
1515
} & TestProps &
1616
UseControllerProps &
1717
Pick<TextInputProps, 'autoFocus'>
@@ -45,7 +45,7 @@ export const TextInputField = ({
4545
autoFocus={autoFocus}
4646
label={label}
4747
multiline={!!numberOfLines}
48-
numberOfLines={numberOfLines ?? 1}
48+
numberOfLines={numberOfLines}
4949
onChangeText={onChange}
5050
placeholder={placeholder}
5151
testID={`${testID}Input`}

src/modules/access-code/screens/AccessCode.screen.tsx

+9-5
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,24 @@ import {ModuleSlug} from '@/modules/slugs'
1515

1616
type Props = NavigationProps<AccessCodeRouteName.accessCode>
1717

18-
export const AccessCodeScreen = ({navigation: {navigate}}: Props) => {
18+
export const AccessCodeScreen = ({navigation}: Props) => {
1919
const {isCodeValid, setIsForgotCode} = useEnterAccessCode()
2020
const {isEnrolled, useBiometrics} = useAccessCodeBiometrics()
21+
const currentModule =
22+
(navigation.getParent()?.getState().routes.at(-1)?.name as ModuleSlug) ??
23+
ModuleSlug.home
2124

2225
useEffect(() => {
2326
if (isCodeValid && isEnrolled && useBiometrics === undefined) {
24-
navigate(AccessCodeRouteName.biometricsPermission)
27+
navigation.navigate(AccessCodeRouteName.biometricsPermission)
2528
}
26-
}, [isCodeValid, isEnrolled, navigate, useBiometrics])
29+
}, [isCodeValid, isEnrolled, navigation, useBiometrics])
2730

2831
const onForgotCode = useCallback(() => {
2932
setIsForgotCode(true)
30-
navigate(ModuleSlug['city-pass'])
31-
}, [navigate, setIsForgotCode])
33+
// The module's stack automatically redirects user to forgot code screen.
34+
navigation.navigate(currentModule)
35+
}, [currentModule, navigation, setIsForgotCode])
3236

3337
return (
3438
<Screen

src/modules/access-code/screens/ConfirmAccessCode.screen.tsx

+8-6
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,27 @@ import {useConfirmAccessCode} from '@/modules/access-code/hooks/useConfirmAccess
1111
import {useUnsetCode} from '@/modules/access-code/hooks/useUnsetCode'
1212
import {AccessCodeRouteName} from '@/modules/access-code/routes'
1313
import {AccessCodeType} from '@/modules/access-code/types'
14-
import {useLoginSteps} from '@/modules/city-pass/hooks/useLoginSteps'
14+
import {ModuleSlug} from '@/modules/slugs'
1515

1616
type Props = NavigationProps<AccessCodeRouteName.confirmAccessCode>
1717

1818
export const ConfirmAccessCodeScreen = ({navigation}: Props) => {
1919
const {isCodeConfirmed} = useConfirmAccessCode()
20-
const {isLoginStepsActive} = useLoginSteps()
2120
const unsetCode = useUnsetCode()
21+
const isUserRoute =
22+
(navigation.getParent()?.getState().routes.at(-2)?.name as ModuleSlug) ===
23+
ModuleSlug.user
2224

2325
useEffect(() => {
2426
if (!isCodeConfirmed) {
2527
return
26-
} else if (isLoginStepsActive) {
28+
} else if (isUserRoute) {
29+
navigation.navigate(AccessCodeRouteName.validAccessCode)
30+
} else {
2731
unsetCode()
2832
navigation.pop(2)
29-
} else {
30-
navigation.navigate(AccessCodeRouteName.validAccessCode)
3133
}
32-
}, [isCodeConfirmed, isLoginStepsActive, navigation, unsetCode])
34+
}, [isCodeConfirmed, isUserRoute, navigation, unsetCode])
3335

3436
const onResetAccessCode = useCallback(() => {
3537
unsetCode()

src/modules/parking/Stack.tsx

+109-8
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,123 @@
1+
import {TransitionPresets} from '@react-navigation/stack'
12
import {createStackNavigator} from '@/app/navigation/createStackNavigator'
23
import {RootStackParams} from '@/app/navigation/types'
34
import {useScreenOptions} from '@/app/navigation/useScreenOptions'
5+
import {useGetSecureItem} from '@/hooks/secureStorage/useGetSecureItem'
6+
import {useAccessCodeBiometrics} from '@/modules/access-code/hooks/useAccessCodeBiometrics'
7+
import {useEnterAccessCode} from '@/modules/access-code/hooks/useEnterAccessCode'
8+
import {useGetSecureAccessCode} from '@/modules/access-code/hooks/useGetSecureAccessCode'
9+
import {AccessCodeRouteName} from '@/modules/access-code/routes'
10+
import {AccessCodeScreen} from '@/modules/access-code/screens/AccessCode.screen'
11+
import {AccessCodeInvalidScreen} from '@/modules/access-code/screens/AccessCodeInvalid.screen'
12+
import {BiometricsPermissionScreen} from '@/modules/access-code/screens/BiometricsPermission.screen'
13+
import {ConfirmAccessCodeScreen} from '@/modules/access-code/screens/ConfirmAccessCode.screen'
14+
import {SetAccessCodeScreen} from '@/modules/access-code/screens/SetAccessCode.screen'
15+
import {useLoginSteps} from '@/modules/parking/hooks/useLoginSteps'
16+
import {useShouldShowIntroScreen} from '@/modules/parking/hooks/useShouldShowIntroScreen'
417
import {ParkingRouteName} from '@/modules/parking/routes'
5-
import {ParkingHomeScreen} from '@/modules/parking/screens/ParkingHome.screen'
18+
import {LoginStepsScreen} from '@/modules/parking/screens/LoginSteps.screen'
19+
import {ParkingDashboardScreen} from '@/modules/parking/screens/ParkingDashBoard.screen'
20+
import {ParkingIntroScreen} from '@/modules/parking/screens/ParkingIntro.screen'
21+
import {ParkingLoginScreen} from '@/modules/parking/screens/ParkingLogin.screen'
22+
import {RestartLoginScreen} from '@/modules/parking/screens/RestartLogin.screen'
23+
import {SecureItemKey} from '@/utils/secureStorage'
624

725
const Stack = createStackNavigator<RootStackParams>()
826

927
export const ParkingStack = () => {
28+
const {accessCode, isLoading} = useGetSecureAccessCode()
29+
const {attemptsLeft, isCodeValid, isForgotCode} = useEnterAccessCode()
30+
const {isEnrolled, useBiometrics} = useAccessCodeBiometrics()
31+
const {isLoginStepsActive} = useLoginSteps()
32+
const {shouldShowIntroScreen} = useShouldShowIntroScreen()
33+
const {item: securePermitHolder} = useGetSecureItem(
34+
SecureItemKey.parkingPermitHolder,
35+
)
36+
const {item: secureVisitor} = useGetSecureItem(SecureItemKey.parkingVisitor)
1037
const screenOptions = useScreenOptions()
1138

39+
if (isLoading) {
40+
return null
41+
}
42+
1243
return (
13-
<Stack.Navigator
14-
initialRouteName={ParkingRouteName.parkingHome}
15-
screenOptions={screenOptions}>
16-
<Stack.Screen
17-
component={ParkingHomeScreen}
18-
name={ParkingRouteName.parkingHome}
19-
/>
44+
<Stack.Navigator screenOptions={screenOptions}>
45+
{attemptsLeft <= 0 || !!isForgotCode ? (
46+
<Stack.Screen
47+
component={RestartLoginScreen}
48+
name={ParkingRouteName.restartLogin}
49+
options={{headerTitle: 'Toegangscode vergeten'}}
50+
/>
51+
) : useBiometrics === undefined && isEnrolled && isCodeValid ? (
52+
<Stack.Screen
53+
component={BiometricsPermissionScreen}
54+
name={AccessCodeRouteName.biometricsPermission}
55+
options={{
56+
headerTitle: 'Sneller toegang',
57+
}}
58+
/>
59+
) : securePermitHolder || secureVisitor ? (
60+
accessCode && !isLoginStepsActive ? (
61+
isCodeValid ? (
62+
<Stack.Screen
63+
component={ParkingDashboardScreen}
64+
name={ParkingRouteName.dashboard}
65+
/>
66+
) : attemptsLeft > 0 ? (
67+
<Stack.Screen
68+
component={AccessCodeScreen}
69+
name={AccessCodeRouteName.accessCode}
70+
options={{
71+
headerTitle: 'Toegangscode invoeren',
72+
...TransitionPresets.ModalFadeTransition,
73+
}}
74+
/>
75+
) : (
76+
<Stack.Screen
77+
component={AccessCodeInvalidScreen}
78+
name={AccessCodeRouteName.accessCodeInvalid}
79+
/>
80+
)
81+
) : (
82+
<>
83+
<Stack.Screen
84+
component={LoginStepsScreen}
85+
name={ParkingRouteName.loginSteps}
86+
options={{headerTitle: 'Inloggen'}}
87+
/>
88+
<Stack.Screen
89+
component={SetAccessCodeScreen}
90+
name={AccessCodeRouteName.setAccessCode}
91+
options={{headerTitle: 'Toegangscode kiezen'}}
92+
/>
93+
<Stack.Screen
94+
component={ConfirmAccessCodeScreen}
95+
name={AccessCodeRouteName.confirmAccessCode}
96+
options={{headerTitle: 'Toegangscode herhalen'}}
97+
/>
98+
</>
99+
)
100+
) : (
101+
<>
102+
{shouldShowIntroScreen ? (
103+
<>
104+
<Stack.Screen
105+
component={ParkingIntroScreen}
106+
name={ParkingRouteName.intro}
107+
/>
108+
<Stack.Screen
109+
component={ParkingLoginScreen}
110+
name={ParkingRouteName.login}
111+
/>
112+
</>
113+
) : (
114+
<Stack.Screen
115+
component={ParkingLoginScreen}
116+
name={ParkingRouteName.login}
117+
/>
118+
)}
119+
</>
120+
)}
20121
</Stack.Navigator>
21122
)
22123
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {TextInputField} from '@/components/ui/forms/TextInputField'
2+
import {Column} from '@/components/ui/layout/Column'
3+
4+
export const ParkingLoginForm = () => (
5+
<Column gutter="md">
6+
<TextInputField
7+
autoFocus
8+
label="Meldcode"
9+
name="reportCode"
10+
rules={{
11+
required: 'Vul een meldcode in',
12+
}}
13+
testID="ParkingLoginFormReportCodeInputField"
14+
/>
15+
<TextInputField
16+
label="Pincode"
17+
name="pin"
18+
rules={{
19+
required: 'Vul een pincode in',
20+
}}
21+
testID="ParkingLoginFormPinCodeInputField"
22+
/>
23+
</Column>
24+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {ReactNode} from 'react'
2+
import {useForm, FormProvider} from 'react-hook-form'
3+
import {ParkingFormLoginFormData} from '@/modules/parking/types'
4+
5+
type Props = {
6+
children: ReactNode
7+
}
8+
9+
export const ParkingLoginFormProvider = ({children}: Props) => {
10+
const form = useForm<ParkingFormLoginFormData>()
11+
12+
return <FormProvider {...form}>{children}</FormProvider>
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {useFormContext} from 'react-hook-form'
2+
import {Button} from '@/components/ui/buttons/Button'
3+
import {Box} from '@/components/ui/containers/Box'
4+
import {SomethingWentWrong} from '@/components/ui/feedback/SomethingWentWrong'
5+
import {Gutter} from '@/components/ui/layout/Gutter'
6+
import {useSetSecureParkingAccount} from '@/modules/parking/hooks/useSetSecureParkingAccount'
7+
import {useLoginParkingMutation} from '@/modules/parking/service'
8+
import {
9+
ParkingFormLoginFormData,
10+
ParkingLoginEndpointRequest,
11+
} from '@/modules/parking/types'
12+
13+
export const ParkingLoginFormSubmitButton = () => {
14+
const {handleSubmit} = useFormContext<ParkingFormLoginFormData>()
15+
const [loginParking, {error, isError, isLoading}] = useLoginParkingMutation()
16+
const isForbiddenError = error && 'status' in error && error.status === 403
17+
const setSecureParkingAccount = useSetSecureParkingAccount()
18+
19+
const errorSentence = isForbiddenError
20+
? 'Controleer uw meldcode en pincode en probeer het opnieuw.'
21+
: 'Er is iets misgegaan. Probeer het opnieuw.'
22+
23+
const onSubmit = handleSubmit(({pin, reportCode}) => {
24+
void loginParking({
25+
pin,
26+
report_code: reportCode,
27+
} as ParkingLoginEndpointRequest)
28+
.unwrap()
29+
.then(({access_token, scope}) => {
30+
void setSecureParkingAccount({accessToken: access_token, scope})
31+
})
32+
})
33+
34+
return (
35+
<Box>
36+
{!!isError && !isLoading && (
37+
<>
38+
<SomethingWentWrong
39+
testID="ParkingLoginFormSomethingWentWrong"
40+
text={errorSentence}
41+
title="Inloggen mislukt"
42+
/>
43+
<Gutter height="lg" />
44+
</>
45+
)}
46+
<Button
47+
label="Inloggen"
48+
onPress={onSubmit}
49+
testID="ParkingLoginFormSubmitButton"
50+
/>
51+
</Box>
52+
)
53+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {useCallback} from 'react'
2+
import {useDispatch} from '@/hooks/redux/useDispatch'
3+
import {useSelector} from '@/hooks/redux/useSelector'
4+
import {
5+
selectIsLoginStepsActive,
6+
setLoginStepsActive,
7+
} from '@/modules/parking/slice'
8+
9+
export const useLoginSteps = () => {
10+
const dispatch = useDispatch()
11+
const isLoginStepsActive = useSelector(selectIsLoginStepsActive)
12+
13+
const setIsLoginStepsActive = useCallback(
14+
(isActive: boolean) => {
15+
dispatch(setLoginStepsActive(isActive))
16+
},
17+
[dispatch],
18+
)
19+
20+
return {isLoginStepsActive, setIsLoginStepsActive}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import {useCallback} from 'react'
2+
import {useSetSecureItem} from '@/hooks/secureStorage/useSetSecureItem'
3+
import {ParkingPermitScope} from '@/modules/parking/types'
4+
import {SecureItemKey} from '@/utils/secureStorage'
5+
6+
type ParkingAccount = {
7+
accessToken: string
8+
scope: ParkingPermitScope
9+
}
10+
11+
export const useSetSecureParkingAccount = () => {
12+
const setSecureItem = useSetSecureItem()
13+
14+
return useCallback(
15+
(data: ParkingAccount) => {
16+
void setSecureItem(
17+
data.scope === ParkingPermitScope.permitHolder
18+
? SecureItemKey.parkingPermitHolder
19+
: SecureItemKey.parkingVisitor,
20+
JSON.stringify(data),
21+
)
22+
},
23+
[setSecureItem],
24+
)
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {useCallback} from 'react'
2+
import {useDispatch} from '@/hooks/redux/useDispatch'
3+
import {useSelector} from '@/hooks/redux/useSelector'
4+
import {
5+
selectShouldShowIntroScreen,
6+
setShouldShowIntroScreenAction,
7+
} from '@/modules/parking/slice'
8+
9+
export const useShouldShowIntroScreen = () => {
10+
const dispatch = useDispatch()
11+
const shouldShowIntroScreen = useSelector(selectShouldShowIntroScreen)
12+
13+
const setShouldShowIntroScreen = useCallback(
14+
(shouldShow: boolean) => {
15+
dispatch(setShouldShowIntroScreenAction(shouldShow))
16+
},
17+
[dispatch],
18+
)
19+
20+
return {shouldShowIntroScreen, setShouldShowIntroScreen}
21+
}

0 commit comments

Comments
 (0)