refactor(app): transfer last components to typescript

This commit is contained in:
Rickard Natt och Dag 2021-03-26 10:34:11 +01:00
parent 203a0a302b
commit 7573e642b4
No known key found for this signature in database
GPG Key ID: C3958EFC7F24E8DF
8 changed files with 89 additions and 277 deletions

View File

@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import { NavigationProp, RouteProp } from '@react-navigation/native'
import {
Button,
CheckBox,
@ -10,7 +11,7 @@ import {
TopNavigationAction,
useTheme,
} from '@ui-kitten/components'
import { Formik } from 'formik'
import { ErrorMessage, Formik } from 'formik'
import moment from 'moment'
import Personnummer from 'personnummer'
import React from 'react'
@ -20,23 +21,32 @@ import * as Yup from 'yup'
import { studentName } from '../utils/peopleHelpers'
import { useSMS } from '../utils/SMS'
import { BackIcon } from './icon.component'
import { RootStackParamList } from './navigation.component'
interface AbsenceProps {
navigation: NavigationProp<RootStackParamList, 'Absence'>
route: RouteProp<RootStackParamList, 'Absence'>
}
interface AbsenceFormValues {
displayStartTimePicker: boolean
displayEndTimePicker: boolean
socialSecurityNumber: string
isFullDay: boolean
startTime: moment.Moment
endTime: moment.Moment
}
const AbsenceSchema = Yup.object().shape({
socialSecurityNumber: Yup.string()
.required('Personnummer saknas')
.test('is-valid', 'Personnumret är ogiltigt', (value) =>
Personnummer.valid(value)
value ? Personnummer.valid(value) : true
),
isFullDay: Yup.bool().required(),
startTime: Yup.string().when('isFullDay', (isFullDay, schema) =>
isFullDay ? schema : schema.required('Starttid saknas')
),
endTime: Yup.string().when('isFullDay', (isFullDay, schema) =>
isFullDay ? schema : schema.required('Sluttid saknas')
),
})
const Absence = ({ route, navigation }) => {
const Absence = ({ route, navigation }: AbsenceProps) => {
const { sendSMS } = useSMS()
const { child } = route.params
const theme = useTheme()
@ -47,12 +57,21 @@ const Absence = ({ route, navigation }) => {
React.useEffect(() => {
const getSocialSecurityNumber = async () => {
const ssn = await AsyncStorage.getItem(`@childssn.${child.id}`)
setSocialSecurityNumber(ssn)
setSocialSecurityNumber(ssn || '')
}
getSocialSecurityNumber()
}, [child])
const initialValues: AbsenceFormValues = {
displayStartTimePicker: false,
displayEndTimePicker: false,
socialSecurityNumber: socialSecurityNumber || '',
isFullDay: true,
startTime: moment().hours(Math.max(8, new Date().getHours())).minute(0),
endTime: maximumDate,
}
return (
<SafeAreaView style={styles.safeArea}>
<TopNavigation
@ -72,16 +91,7 @@ const Absence = ({ route, navigation }) => {
<Formik
enableReinitialize
validationSchema={AbsenceSchema}
initialValues={{
displayStartTimePicker: false,
displayEndTimePicker: false,
socialSecurityNumber: socialSecurityNumber || '',
isFullDay: true,
startTime: moment()
.hours(Math.max(8, new Date().getHours()))
.minute(0),
endTime: maximumDate,
}}
initialValues={initialValues}
onSubmit={async (values) => {
const ssn = Personnummer.parse(values.socialSecurityNumber).format()
@ -110,7 +120,8 @@ const Absence = ({ route, navigation }) => {
touched,
errors,
}) => {
const hasError = (field) => errors[field] && touched[field]
const hasError = (field: keyof typeof values) =>
errors[field] && touched[field]
return (
<View>
@ -171,11 +182,6 @@ const Absence = ({ route, navigation }) => {
setFieldValue('displayStartTimePicker', false)
}
/>
{hasError('startTime') && (
<Text style={{ color: theme['color-danger-700'] }}>
{errors.startTime}
</Text>
)}
</View>
<View style={styles.spacer} />
<View style={styles.inputHalf}>
@ -207,11 +213,6 @@ const Absence = ({ route, navigation }) => {
setFieldValue('displayEndTimePicker', false)
}
/>
{hasError('endTime') && (
<Text style={{ color: theme['color-danger-700'] }}>
{errors.endTime}
</Text>
)}
</View>
</View>
)}

View File

@ -1,4 +1,3 @@
import { NavigationProp } from '@react-navigation/native'
import { Layout, Text } from '@ui-kitten/components'
import React, { useState } from 'react'
import {
@ -11,11 +10,6 @@ import {
View,
} from 'react-native'
import { Login } from './login.component'
import { SigninStackParamList } from './navigation.component'
interface AuthProps {
navigation: NavigationProp<SigninStackParamList, 'Login'>
}
const funArguments = [
'agila',
@ -37,7 +31,7 @@ const funArguments = [
'öppna',
]
export const Auth = ({ navigation }: AuthProps) => {
export const Auth = () => {
const [argument] = useState(() => {
const argNum = Math.floor(Math.random() * funArguments.length)
return funArguments[argNum]
@ -58,7 +52,7 @@ export const Auth = ({ navigation }: AuthProps) => {
<Text category="h6" style={styles.subtitle}>
Det {argument} alternativet
</Text>
<Login navigation={navigation} />
<Login />
</Layout>
</View>
</SafeAreaView>

View File

@ -1,151 +0,0 @@
import { useApi, useChildList } from '@skolplattformen/api-hooks'
import {
Divider,
Layout,
List,
Spinner,
Text,
TopNavigation,
TopNavigationAction,
} from '@ui-kitten/components'
import React from 'react'
import { Dimensions, Image, SafeAreaView, StyleSheet, View } from 'react-native'
import ActionSheet from 'rn-actionsheet-module'
import { ChildListItem } from './childListItem.component'
import { SettingsIcon } from './icon.component'
import AsyncStorage from '@react-native-async-storage/async-storage'
const { width } = Dimensions.get('window')
const colors = ['primary', 'success', 'info', 'warning', 'danger']
const settingsOptions = ['Logga ut', 'Avbryt']
export const Children = ({ navigation }) => {
const { api } = useApi()
const { data: childList, status } = useChildList()
const handleSettingSelection = (index) => {
switch (index) {
case 0:
api.logout()
AsyncStorage.clear()
navigation.navigate('Login')
}
}
const settings = () => {
const options = {
cancelButtonIndex: 1,
title: 'Inställningar',
optionsIOS: settingsOptions,
optionsAndroid: settingsOptions,
onCancelAndroidIndex: handleSettingSelection,
}
ActionSheet(options, handleSettingSelection)
}
return (
<SafeAreaView style={styles.topContainer}>
{status === 'loaded' ? (
<>
<TopNavigation
title="Dina barn"
alignment="center"
accessoryRight={() => (
<TopNavigationAction icon={SettingsIcon} onPress={settings} />
)}
/>
<Divider />
<List
contentContainerStyle={styles.childListContainer}
data={childList}
style={styles.childList}
ListEmptyComponent={
<View style={styles.emptyState}>
<Text category="h2">Inga barn</Text>
<Text style={styles.emptyStateDescription}>
Det finns inga barn registrerade för ditt personnummer i
Stockholms Stad
</Text>
<Image
source={require('../assets/children.png')}
style={styles.emptyStateImage}
/>
</View>
}
renderItem={({ item: child, index }) => (
<ChildListItem
child={child}
color={colors[index % colors.length]}
key={child.id}
navigation={navigation}
/>
)}
/>
</>
) : (
<Layout style={styles.loading}>
<Image
source={require('../assets/girls.png')}
style={styles.loadingImage}
/>
<View style={styles.loadingMessage}>
<Spinner size="large" status="warning" />
<Text category="h1" style={styles.loadingText}>
Laddar...
</Text>
</View>
</Layout>
)}
</SafeAreaView>
)
}
const styles = StyleSheet.create({
topContainer: {
flex: 1,
backgroundColor: '#fff',
},
loading: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingImage: {
height: (width / 16) * 9,
width: width,
},
loadingMessage: {
alignItems: 'center',
flexDirection: 'row',
marginTop: 8,
},
loadingText: {
marginLeft: 20,
},
childList: {
flex: 1,
},
childListContainer: {
padding: 20,
},
emptyState: {
backgroundColor: '#fff',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 20,
},
emptyStateDescription: {
lineHeight: 21,
marginTop: 8,
textAlign: 'center',
},
emptyStateImage: {
// 80% size and 16:9 aspect ratio
height: ((width * 0.8) / 16) * 9,
marginTop: 20,
width: width * 0.8,
},
})

View File

@ -23,6 +23,7 @@ import ActionSheet from 'rn-actionsheet-module'
import { ChildListItem } from './childListItem.component'
import { SettingsIcon } from './icon.component'
import { RootStackParamList } from './navigation.component'
import AsyncStorage from '@react-native-async-storage/async-storage'
interface ChildrenProps {
navigation: NavigationProp<RootStackParamList, 'Children'>
@ -41,7 +42,7 @@ export const Children = ({ navigation }: ChildrenProps) => {
switch (index) {
case 0:
api.logout()
navigation.navigate('Login')
AsyncStorage.clear()
}
}

View File

@ -1,3 +1,4 @@
import { Classmate } from '@skolplattformen/embedded-api'
import {
Button,
MenuGroup,
@ -15,7 +16,17 @@ import {
SMSIcon,
} from './icon.component'
export const ContactMenu = ({ contact, selected, setSelected }) => {
interface ContactMenuProps {
contact: Classmate
selected: boolean
setSelected: (value?: number | null) => void
}
export const ContactMenu = ({
contact,
selected,
setSelected,
}: ContactMenuProps) => {
const [visible, setVisible] = React.useState(selected)
const renderToggleButton = () => (
@ -32,7 +43,7 @@ export const ContactMenu = ({ contact, selected, setSelected }) => {
setSelected(null)
}
const shouldDisplay = (option) => (option ? 'flex' : 'none')
const shouldDisplay = (option?: string) => (option ? 'flex' : 'none')
return (
<OverflowMenu

View File

@ -10,14 +10,15 @@ import {
import Personnummer from 'personnummer'
import React, { useEffect, useState } from 'react'
import {
Dimensions,
Image,
Linking,
Platform,
StyleSheet,
TouchableWithoutFeedback,
View,
Dimensions,
} from 'react-native'
import ActionSheet from 'rn-actionsheet-module'
import { useAsyncStorage } from 'use-async-storage'
import { schema } from '../app.json'
import {
@ -26,15 +27,16 @@ import {
SecureIcon,
SelectIcon,
} from './icon.component'
import ActionSheet from 'rn-actionsheet-module'
const { width } = Dimensions.get('window')
export const Login = ({ navigation }) => {
const { api, isLoggedIn } = useApi()
const [cancelLoginRequest, setCancelLoginRequest] = useState(() => () => null)
export const Login = () => {
const { api } = useApi()
const [cancelLoginRequest, setCancelLoginRequest] = useState<
(() => Promise<void>) | (() => null)
>(() => () => null)
const [visible, showModal] = useState(false)
const [error, setError] = useState(null)
const [error, setError] = useState<string | null>(null)
const [cachedSsn, setCachedSsn] = useAsyncStorage('socialSecurityNumber', '')
const [socialSecurityNumber, setSocialSecurityNumber] = useState('')
const [valid, setValid] = useState(false)
@ -55,11 +57,11 @@ export const Login = ({ navigation }) => {
optionsAndroid: loginMethods,
onCancelAndroidIndex: loginMethodIndex,
}
ActionSheet(options, (index) => setLoginMethodIndex(index))
ActionSheet(options, (index: number) => setLoginMethodIndex(index))
}
useEffect(() => {
if (loginMethodIndex !== parseInt(cachedLoginMethodIndex, 10)) {
setCachedLoginMethodIndex(loginMethodIndex)
setCachedLoginMethodIndex(loginMethodIndex.toString())
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loginMethodIndex])
@ -88,18 +90,20 @@ export const Login = ({ navigation }) => {
useEffect(() => {
api.on('login', loginHandler)
return () => api.off('login', loginHandler)
return () => {
api.off('login', loginHandler)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
/* Helpers */
const handleInput = (text) => {
const handleInput = (text: string) => {
setValid(Personnummer.valid(text))
setCachedSsn(text)
setSocialSecurityNumber(text)
}
const openBankId = (token) => {
const openBankId = (token: string) => {
try {
const redirect = loginMethodIndex === 0 ? encodeURIComponent(schema) : ''
const bankIdUrl =
@ -112,7 +116,7 @@ export const Login = ({ navigation }) => {
}
}
const startLogin = async (text) => {
const startLogin = async (text: string) => {
if (loginMethodIndex < 2) {
showModal(true)
const ssn = Personnummer.parse(text).format(true)
@ -135,12 +139,6 @@ export const Login = ({ navigation }) => {
}
}
const clearInput = (props) => (
<TouchableWithoutFeedback onPress={() => handleInput('')}>
<CloseOutlineIcon {...props} />
</TouchableWithoutFeedback>
)
return (
<>
<Image source={require('../assets/boys.png')} style={styles.image} />
@ -152,10 +150,14 @@ export const Login = ({ navigation }) => {
value={socialSecurityNumber}
style={styles.pnrInput}
accessoryLeft={PersonIcon}
accessoryRight={clearInput}
accessoryRight={(props) => (
<TouchableWithoutFeedback onPress={() => handleInput('')}>
<CloseOutlineIcon {...props} />
</TouchableWithoutFeedback>
)}
keyboardType="numeric"
onSubmitEditing={(event) => startLogin(event.nativeEvent.text)}
caption={error?.message || ''}
caption={error || ''}
onChangeText={(text) => handleInput(text)}
placeholder="Ditt personnr"
/>
@ -164,20 +166,18 @@ export const Login = ({ navigation }) => {
<Button
onPress={() => startLogin(socialSecurityNumber)}
style={styles.loginButton}
appearence="ghost"
appearance="ghost"
disabled={loginMethodIndex !== 2 && !valid}
status="primary"
accessoryLeft={SecureIcon}
size="medium"
>
<Text adjustsFontSizeToFit style={styles.loginButtonText}>
{loginMethods[loginMethodIndex]}
</Text>
{loginMethods[loginMethodIndex]}
</Button>
<Button
onPress={selectLoginMethod}
style={styles.loginMethodButton}
appearence="ghost"
appearance="ghost"
status="primary"
accessoryLeft={SelectIcon}
size="medium"
@ -194,7 +194,6 @@ export const Login = ({ navigation }) => {
<Text style={styles.bankIdLoading}>Väntar BankID...</Text>
<Button
visible={!isLoggedIn}
onPress={() => {
cancelLoginRequest()
showModal(false)

View File

@ -1,43 +0,0 @@
import { useApi } from '@skolplattformen/api-hooks'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import React from 'react'
import { StatusBar } from 'react-native'
import { schema } from '../app.json'
import Absence from './absence.component'
import { Child } from './child.component'
import { Children } from './children.component'
import { Auth } from './auth.component'
import { NewsItem } from './newsItem.component'
const { Navigator, Screen } = createStackNavigator()
const linking = {
prefixes: [schema],
config: {
screens: {
Login: 'login',
},
},
}
export const AppNavigator = () => {
const { isLoggedIn } = useApi()
return (
<NavigationContainer linking={linking}>
<StatusBar />
<Navigator headerMode="none">
{isLoggedIn ? (
<>
<Screen name="Children" component={Children} />
<Screen name="Child" component={Child} />
<Screen name="NewsItem" component={NewsItem} />
<Screen name="Absence" component={Absence} />
</>
) : (
<Screen name="Login" component={Auth} />
)}
</Navigator>
</NavigationContainer>
)
}

View File

@ -1,3 +1,4 @@
import { useApi } from '@skolplattformen/api-hooks'
import { NavigationContainer } from '@react-navigation/native'
import { createStackNavigator } from '@react-navigation/stack'
import React from 'react'
@ -25,22 +26,8 @@ export type RootStackParamList = {
Absence: { child: ChildType }
}
export type SigninStackParamList = {
Login: undefined
}
const { Navigator, Screen } = createStackNavigator()
const HomeNavigator = () => (
<Navigator headerMode="none">
<Screen name="Login" component={Auth} />
<Screen name="Children" component={Children} />
<Screen name="Child" component={Child} />
<Screen name="NewsItem" component={NewsItem} />
<Screen name="Absence" component={Absence} />
</Navigator>
)
const linking = {
prefixes: [schema],
config: {
@ -50,10 +37,23 @@ const linking = {
},
}
export const AppNavigator = () => {
const { isLoggedIn } = useApi()
return (
<NavigationContainer linking={linking}>
<StatusBar />
<HomeNavigator />
<Navigator headerMode="none">
{isLoggedIn ? (
<>
<Screen name="Children" component={Children} />
<Screen name="Child" component={Child} />
<Screen name="NewsItem" component={NewsItem} />
<Screen name="Absence" component={Absence} />
</>
) : (
<Screen name="Login" component={Auth} />
)}
</Navigator>
</NavigationContainer>
)
}