feat: 🎸 add settings screen (#492)
* feat: add first version of settings
* fix: navigate after logout is now working
* Add settings store with valtio
* fix: fix failing tests
* fix: remove unused module `rn-actionsheet-module`
* fix: remove unused packages
* fix: Remove unused AppearanceProvider
* fix: upgrade to correct version of hermes engine
* fix: correct theme name in selection list
* fix: add missing translations
* fix lint errors
* fix failing tests
* fix: simplify login method logic
* simplify cached personalIdNumber storage
* fix: settings is now always pressable on login screen
* fix: app is correctly rendered when language changes
* chore: rename SettingListItemText to SettingListItem
* fix: fix trailing useEffect error message
* fix: better RTL layout in settings
* add back missing hermes reference
* fix: add useTranslation hook instead of translate
* fix separator styling (includes change for bgColor)
* Add missing translation
* fix: 🐛 useTranslation on childListItem
Co-authored-by: Andreas Eriksson <addeman@gmail.com>
Co-authored-by: Kajetan Kazimierczak <kajetan@hotmail.com>
This commit is contained in:
parent
fcb8170e24
commit
3b307d25b3
|
@ -57,3 +57,5 @@ buck-out/
|
|||
|
||||
# CocoaPods
|
||||
/ios/Pods/
|
||||
|
||||
libraries.json
|
|
@ -6,13 +6,13 @@ import init from '@skolplattformen/embedded-api'
|
|||
import { ApplicationProvider, IconRegistry } from '@ui-kitten/components'
|
||||
import { EvaIconsPack } from '@ui-kitten/eva-icons'
|
||||
import React from 'react'
|
||||
import { StatusBar } from 'react-native'
|
||||
import { AppearanceProvider, useColorScheme } from 'react-native-appearance'
|
||||
import { StatusBar, useColorScheme } from 'react-native'
|
||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||
import { AppNavigator } from './components/navigation.component'
|
||||
import { LanguageProvider } from './context/language/languageContext'
|
||||
import { default as customMapping } from './design/mapping.json'
|
||||
import { darkTheme, lightTheme } from './design/themes'
|
||||
import useSettingsStorage from './hooks/useSettingsStorage'
|
||||
import { translations } from './utils/translation'
|
||||
const api = init(fetch, CookieManager)
|
||||
|
||||
|
@ -57,28 +57,30 @@ const logAsyncStorage = async () => {
|
|||
}
|
||||
|
||||
export default () => {
|
||||
const colorScheme = useColorScheme()
|
||||
const [usingSystemTheme] = useSettingsStorage('usingSystemTheme')
|
||||
const [theme] = useSettingsStorage('theme')
|
||||
const systemTheme = useColorScheme()
|
||||
|
||||
const colorScheme = usingSystemTheme ? systemTheme : theme
|
||||
|
||||
return (
|
||||
<ApiProvider api={api} storage={AsyncStorage} reporter={reporter}>
|
||||
<SafeAreaProvider>
|
||||
<AppearanceProvider>
|
||||
<StatusBar
|
||||
backgroundColor={colorScheme === 'dark' ? '#2E3137' : '#FFF'}
|
||||
barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'}
|
||||
translucent
|
||||
/>
|
||||
<IconRegistry icons={EvaIconsPack} />
|
||||
<ApplicationProvider
|
||||
{...eva}
|
||||
customMapping={customMapping}
|
||||
theme={colorScheme === 'dark' ? darkTheme : lightTheme}
|
||||
>
|
||||
<LanguageProvider cache={true} data={translations}>
|
||||
<AppNavigator />
|
||||
</LanguageProvider>
|
||||
</ApplicationProvider>
|
||||
</AppearanceProvider>
|
||||
<StatusBar
|
||||
backgroundColor={colorScheme === 'dark' ? '#2E3137' : '#FFF'}
|
||||
barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'}
|
||||
translucent
|
||||
/>
|
||||
<IconRegistry icons={EvaIconsPack} />
|
||||
<ApplicationProvider
|
||||
{...eva}
|
||||
customMapping={customMapping}
|
||||
theme={colorScheme === 'dark' ? darkTheme : lightTheme}
|
||||
>
|
||||
<LanguageProvider cache={true} data={translations}>
|
||||
<AppNavigator />
|
||||
</LanguageProvider>
|
||||
</ApplicationProvider>
|
||||
</SafeAreaProvider>
|
||||
</ApiProvider>
|
||||
)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export * from 'react-native-appearance/src/mock'
|
|
@ -76,9 +76,9 @@ import com.android.build.OutputFile
|
|||
* extraPackagerArgs: []
|
||||
* ]
|
||||
*/
|
||||
// Crashes the app in RN 0.65.1
|
||||
|
||||
project.ext.react = [
|
||||
enableHermes: false, // clean and rebuild if changing
|
||||
enableHermes: true, // clean and rebuild if changing
|
||||
]
|
||||
|
||||
apply from: "../../node_modules/react-native/react.gradle"
|
||||
|
|
|
@ -10,23 +10,24 @@ import {
|
|||
Image,
|
||||
ImageStyle,
|
||||
Keyboard,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import { LanguageService } from '../services/languageService'
|
||||
import { useTranslation } from '../hooks/useTranslation'
|
||||
import { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
||||
import { fontSize } from '../styles/typography'
|
||||
import { KeyboardAvoidingView } from '../ui/keyboardAvoidingView.component'
|
||||
import { SafeAreaView } from '../ui/safeAreaView.component'
|
||||
import { SafeAreaViewContainer } from '../ui/safeAreaViewContainer.component'
|
||||
import { languages, translate } from '../utils/translation'
|
||||
import { GlobeIcon } from './icon.component'
|
||||
import { SettingsIcon } from './icon.component'
|
||||
import { Login } from './login.component'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
|
||||
const randomWord = () => {
|
||||
const words = translate('auth.words')
|
||||
const randomWord = (
|
||||
t: (scope: I18n.Scope, options?: I18n.TranslateOptions | undefined) => string
|
||||
) => {
|
||||
const words = t('auth.words')
|
||||
const keys = Object.keys(words)
|
||||
|
||||
const randomIndex: number = Math.floor(Math.random() * keys.length)
|
||||
|
@ -51,73 +52,64 @@ export const authRouteOptions = (): NativeStackNavigationOptions => {
|
|||
export const Auth: React.FC<AuthProps> = ({ navigation }) => {
|
||||
const styles = useStyleSheet(themeStyles)
|
||||
const colors = useTheme()
|
||||
|
||||
const currentLanguage = LanguageService.getLanguageCode()
|
||||
const currentLanguageName = languages.find(
|
||||
(language) => language.langCode === currentLanguage
|
||||
)?.languageLocalName
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<SafeAreaViewContainer>
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={LayoutStyle.flex.full}>
|
||||
<TouchableWithoutFeedback
|
||||
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
||||
onPress={() => navigation.navigate('SetLanguage')}
|
||||
accessibilityHint={translate(
|
||||
'auth.a11y_navigate_to_change_language',
|
||||
{
|
||||
defaultValue: 'Navigerar till vyn för att byta språk',
|
||||
}
|
||||
)}
|
||||
accessibilityLabel={translate('auth.a11y_change_language', {
|
||||
defaultValue: 'Byt språk',
|
||||
})}
|
||||
>
|
||||
<View style={styles.language}>
|
||||
<GlobeIcon
|
||||
height={24}
|
||||
width={24}
|
||||
fill={colors['color-primary-500']}
|
||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||
<View style={LayoutStyle.flex.full}>
|
||||
<TouchableOpacity
|
||||
style={styles.settingsLink}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
accessibilityHint={t('auth.a11y_navigate_to_settings', {
|
||||
defaultValue: 'Navigerar till vyn för inställningar',
|
||||
})}
|
||||
accessibilityLabel={t('auth.a11y_settings', {
|
||||
defaultValue: 'Inställningar',
|
||||
})}
|
||||
>
|
||||
<View style={styles.language}>
|
||||
<SettingsIcon
|
||||
height={28}
|
||||
width={28}
|
||||
fill={colors['color-primary-500']}
|
||||
/>
|
||||
<Text style={styles.languageText}>{t('general.settings')}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<KeyboardAvoidingView>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.imageWrapper}>
|
||||
<Image
|
||||
source={require('../assets/boys.png')}
|
||||
style={styles.image as ImageStyle}
|
||||
accessibilityHint={t('login.a11y_image_two_boys', {
|
||||
defaultValue: 'Bild på två personer som kollar i mobilen',
|
||||
})}
|
||||
resizeMode="contain"
|
||||
accessibilityIgnoresInvertColors={false}
|
||||
/>
|
||||
<Text style={styles.languageText}>{currentLanguageName}</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<KeyboardAvoidingView>
|
||||
<View style={styles.content}>
|
||||
<View style={styles.imageWrapper}>
|
||||
<Image
|
||||
source={require('../assets/boys.png')}
|
||||
style={styles.image as ImageStyle}
|
||||
accessibilityHint={translate('login.a11y_image_two_boys', {
|
||||
defaultValue: 'Bild på två personer som kollar i mobilen',
|
||||
})}
|
||||
resizeMode="contain"
|
||||
accessibilityIgnoresInvertColors={false}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
<Text
|
||||
category="h1"
|
||||
style={styles.header}
|
||||
adjustsFontSizeToFit
|
||||
numberOfLines={2}
|
||||
>
|
||||
Öppna skolplattformen
|
||||
</Text>
|
||||
<Login />
|
||||
<Text category="c2" style={styles.subtitle}>
|
||||
{translate('auth.subtitle', {
|
||||
word: randomWord(),
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.container}>
|
||||
<Text
|
||||
category="h1"
|
||||
style={styles.header}
|
||||
adjustsFontSizeToFit
|
||||
numberOfLines={2}
|
||||
>
|
||||
Öppna skolplattformen
|
||||
</Text>
|
||||
<Login />
|
||||
<Text category="c2" style={styles.subtitle}>
|
||||
{t('auth.subtitle', {
|
||||
word: randomWord(t),
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaViewContainer>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</SafeAreaView>
|
||||
)
|
||||
}
|
||||
|
@ -153,10 +145,15 @@ const themeStyles = StyleService.create({
|
|||
language: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: Sizing.t4,
|
||||
padding: Sizing.t3,
|
||||
paddingLeft: Sizing.t5,
|
||||
},
|
||||
languageText: {
|
||||
...fontSize.xs,
|
||||
...fontSize.sm,
|
||||
marginLeft: Sizing.t1,
|
||||
},
|
||||
settingsLink: {
|
||||
alignSelf: 'flex-start',
|
||||
zIndex: 1,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -21,7 +21,7 @@ import React from 'react'
|
|||
import { TouchableOpacity, useColorScheme, View } from 'react-native'
|
||||
import { Colors, Layout, Sizing } from '../styles'
|
||||
import { studentName } from '../utils/peopleHelpers'
|
||||
import { translate } from '../utils/translation'
|
||||
import { useTranslation } from '../hooks/useTranslation'
|
||||
import { DaySummary } from './daySummary.component'
|
||||
import { AlertIcon, RightArrowIcon } from './icon.component'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
|
@ -41,6 +41,7 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
|||
React.useEffect(() => {}, [child.id])
|
||||
|
||||
const navigation = useNavigation<ChildListItemNavigationProp>()
|
||||
const { t } = useTranslation()
|
||||
const { data: notifications } = useNotifications(child)
|
||||
const { data: news } = useNews(child)
|
||||
const { data: classmates } = useClassmates(child)
|
||||
|
@ -91,10 +92,10 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
|||
// Taken from Skolverket
|
||||
// https://www.skolverket.se/skolutveckling/anordna-och-administrera-utbildning/administrera-utbildning/skoltermer-pa-engelska
|
||||
const abbrevations = {
|
||||
G: translate('abbrevations.upperSecondarySchool'),
|
||||
GR: translate('abbrevations.compulsorySchool'),
|
||||
F: translate('abbrevations.leisureTimeCentre'),
|
||||
FS: translate('abbrevations.preSchool'),
|
||||
G: t('abbrevations.upperSecondarySchool'),
|
||||
GR: t('abbrevations.compulsorySchool'),
|
||||
F: t('abbrevations.leisureTimeCentre'),
|
||||
FS: t('abbrevations.preSchool'),
|
||||
}
|
||||
|
||||
return child.status
|
||||
|
@ -143,7 +144,7 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
|||
</Text>
|
||||
))}
|
||||
<Text category="c2" style={styles.label}>
|
||||
{translate('navigation.news')}
|
||||
{t('navigation.news')}
|
||||
</Text>
|
||||
{notificationsThisWeek.slice(0, 3).map((notification, i) => (
|
||||
<Text category="p1" key={i}>
|
||||
|
@ -159,14 +160,14 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
|||
notificationsThisWeek.length ||
|
||||
newsThisWeek.length ? null : (
|
||||
<Text category="p1" style={styles.noNewNewsItemsText}>
|
||||
{translate('news.noNewNewsItemsThisWeek')}
|
||||
{t('news.noNewNewsItemsThisWeek')}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!menu[moment().isoWeekday() - 1] ? null : (
|
||||
<>
|
||||
<Text category="c2" style={styles.label}>
|
||||
{translate('schedule.lunch')}
|
||||
{t('schedule.lunch')}
|
||||
</Text>
|
||||
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
|
||||
</>
|
||||
|
@ -175,14 +176,14 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
|||
<Button
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${child.name}, ${translate('abscense.title')}`}
|
||||
accessibilityLabel={`${child.name}, ${t('abscense.title')}`}
|
||||
appearance="ghost"
|
||||
accessoryLeft={AlertIcon}
|
||||
status="primary"
|
||||
style={styles.absenceButton}
|
||||
onPress={() => navigation.navigate('Absence', { child })}
|
||||
>
|
||||
{translate('abscense.title')}
|
||||
{t('abscense.title')}
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import AppStorage from '../services/appStorage'
|
||||
import { useNavigation } from '@react-navigation/core'
|
||||
import { useApi, useChildList } from '@skolplattformen/api-hooks'
|
||||
import { Child } from '@skolplattformen/embedded-api'
|
||||
|
@ -11,7 +10,7 @@ import {
|
|||
TopNavigationAction,
|
||||
useStyleSheet,
|
||||
} from '@ui-kitten/components'
|
||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
Image,
|
||||
ImageStyle,
|
||||
|
@ -20,12 +19,12 @@ import {
|
|||
View,
|
||||
} from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import ActionSheet from 'rn-actionsheet-module'
|
||||
import { defaultStackStyling } from '../design/navigationThemes'
|
||||
import AppStorage from '../services/appStorage'
|
||||
import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
||||
import { translate } from '../utils/translation'
|
||||
import { ChildListItem } from './childListItem.component'
|
||||
import { CloseOutlineIcon } from './icon.component'
|
||||
import { SettingsIcon } from './icon.component'
|
||||
|
||||
const colors = ['primary', 'success', 'info', 'warning', 'danger']
|
||||
|
||||
|
@ -54,56 +53,18 @@ export const Children = () => {
|
|||
AppStorage.clearTemporaryItems().then(() => api.logout())
|
||||
}, [api])
|
||||
|
||||
const logoutAndClearPersonalData = useCallback(() => {
|
||||
api
|
||||
.getUser()
|
||||
.then((user) => AppStorage.clearPersonalData(user))
|
||||
.then(() => AppStorage.clearTemporaryItems().then(() => api.logout()))
|
||||
}, [api])
|
||||
|
||||
const logoutAndClearAll = useCallback(() => {
|
||||
AppStorage.nukeAllStorage().then(() => api.logout())
|
||||
}, [api])
|
||||
|
||||
const settingsOptions = useMemo(() => {
|
||||
return [
|
||||
translate('general.logout'),
|
||||
translate('general.logoutAndClearPersonalData'),
|
||||
translate('general.logoutAndClearAllDataInclSettings'),
|
||||
translate('general.cancel'),
|
||||
]
|
||||
}, [])
|
||||
|
||||
const handleSettingSelection = useCallback(
|
||||
(index: number) => {
|
||||
if (index === 0) logout()
|
||||
if (index === 1) logoutAndClearPersonalData()
|
||||
if (index === 2) logoutAndClearAll()
|
||||
},
|
||||
[logout, logoutAndClearAll, logoutAndClearPersonalData]
|
||||
)
|
||||
|
||||
const settings = useCallback(() => {
|
||||
const options = {
|
||||
cancelButtonIndex: settingsOptions.length - 1,
|
||||
title: translate('general.settings'),
|
||||
optionsIOS: settingsOptions,
|
||||
optionsAndroid: settingsOptions,
|
||||
onCancelAndroidIndex: handleSettingSelection,
|
||||
}
|
||||
|
||||
ActionSheet(options, handleSettingSelection)
|
||||
}, [handleSettingSelection, settingsOptions])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => {
|
||||
return (
|
||||
<TopNavigationAction icon={CloseOutlineIcon} onPress={settings} />
|
||||
<TopNavigationAction
|
||||
icon={SettingsIcon}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
/>
|
||||
)
|
||||
},
|
||||
})
|
||||
}, [navigation, settings])
|
||||
}, [navigation])
|
||||
|
||||
// We need to skip safe area view here, due to the reason that it's adding a white border
|
||||
// when this view is actually lightgrey. Taking the padding top value from the use inset hook.
|
||||
|
|
|
@ -6,6 +6,7 @@ const uiIcon = (name: string) => (props: IconProps) =>
|
|||
|
||||
export const AlertIcon = uiIcon('alert-circle-outline')
|
||||
export const BackIcon = uiIcon('arrow-back')
|
||||
export const BrushIcon = uiIcon('brush')
|
||||
export const CalendarOutlineIcon = uiIcon('calendar-outline')
|
||||
export const CallIcon = uiIcon('phone-outline')
|
||||
export const CheckIcon = uiIcon('checkmark-outline')
|
||||
|
@ -29,3 +30,5 @@ export const GlobeIcon = uiIcon('globe-outline')
|
|||
export const ExternalLinkIcon = uiIcon('external-link-outline')
|
||||
export const ClipboardIcon = uiIcon('clipboard-outline')
|
||||
export const RightArrowIcon = uiIcon('arrow-ios-forward-outline')
|
||||
export const QuestionMarkIcon = uiIcon('question-mark')
|
||||
export const AwardIcon = uiIcon('award')
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { RouteProp, useRoute } from '@react-navigation/native'
|
||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
||||
import React from 'react'
|
||||
import { Linking, Platform } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import { Layout, Sizing, Typography } from '../styles'
|
||||
import { fontSize } from '../styles/typography'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
|
||||
type LibraryRouteProp = RouteProp<RootStackParamList, 'Library'>
|
||||
|
||||
export const libraryRouteOptions = (): NativeStackNavigationOptions => {
|
||||
return {
|
||||
title: '',
|
||||
headerLargeTitle: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const LibraryScreen = () => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
const route = useRoute<LibraryRouteProp>()
|
||||
const library = route.params.library
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.article}
|
||||
style={styles.scrollView}
|
||||
>
|
||||
<Text style={styles.title}>
|
||||
{library.libraryName}
|
||||
<Text style={styles.version}> (v{library.version})</Text>
|
||||
</Text>
|
||||
{library._description && (
|
||||
<Text style={styles.description}>{library._description}</Text>
|
||||
)}
|
||||
<Text style={styles.license}>
|
||||
{library._licenseContent ?? library._license?.toString()}
|
||||
</Text>
|
||||
{library.homepage && (
|
||||
<Text
|
||||
style={styles.link}
|
||||
onPress={() => Linking.openURL(library.homepage ?? '')}
|
||||
>
|
||||
{library.homepage}
|
||||
</Text>
|
||||
)}
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const themedStyles = StyleService.create({
|
||||
title: {
|
||||
...Typography.fontWeight.bold,
|
||||
fontSize: 30,
|
||||
marginBottom: Sizing.t4,
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
fontWeight: '700',
|
||||
},
|
||||
version: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
...fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: 'text-hint-color',
|
||||
},
|
||||
link: {
|
||||
fontWeight: '700',
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
textDecorationColor: 'text-hint-color',
|
||||
textDecorationLine: 'underline',
|
||||
},
|
||||
description: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
fontWeight: '700',
|
||||
marginBottom: Sizing.t4,
|
||||
},
|
||||
license: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
marginBottom: Sizing.t4,
|
||||
},
|
||||
article: {
|
||||
padding: Sizing.t5,
|
||||
},
|
||||
scrollView: {
|
||||
...Layout.flex.full,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,46 @@
|
|||
import { StyleService, useStyleSheet } from '@ui-kitten/components'
|
||||
import { Library } from 'libraries.json'
|
||||
import React, { useCallback } from 'react'
|
||||
import { FlatList, ListRenderItemInfo } from 'react-native'
|
||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
||||
import { LibraryListItem } from './libraryListItem.component'
|
||||
import { SettingListSeparator } from './settingsComponents.component'
|
||||
|
||||
export const LibraryList = ({ libraries }: { libraries: Library[] }) => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
const renderItem = useCallback(
|
||||
({ item: library }: ListRenderItemInfo<Library>) => (
|
||||
<LibraryListItem library={library} />
|
||||
),
|
||||
[]
|
||||
)
|
||||
|
||||
const keyExtractor = useCallback((library: Library) => {
|
||||
return `${library.libraryName}:${library.version}`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={libraries}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
ItemSeparatorComponent={SettingListSeparator}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.container}
|
||||
initialNumToRender={15}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const themedStyles = StyleService.create({
|
||||
list: {
|
||||
...LayoutStyle.flex.full,
|
||||
paddingHorizontal: Sizing.t4,
|
||||
marginBottom: Sizing.t5,
|
||||
},
|
||||
container: {
|
||||
borderRadius: 15,
|
||||
backgroundColor: 'background-basic-color-1',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,54 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/core'
|
||||
import { StyleService, Text, useStyleSheet } from '@ui-kitten/components'
|
||||
import { Library } from 'libraries.json'
|
||||
import React from 'react'
|
||||
import { Platform, View } from 'react-native'
|
||||
import { fontSize } from '../styles/typography'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
import { SettingListItem } from './settingsComponents.component'
|
||||
|
||||
export const LibraryListItem = ({ library }: { library: Library }) => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>()
|
||||
return (
|
||||
<SettingListItem
|
||||
onNavigate={() => navigation.navigate('Library', { library })}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.name}>{library.libraryName}</Text>
|
||||
<View style={styles.bottomRow}>
|
||||
<Text style={styles.version}>v{library.version}</Text>
|
||||
<Text style={styles.license}>
|
||||
{library._license?.toString() ?? 'Unknown'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</SettingListItem>
|
||||
)
|
||||
}
|
||||
|
||||
const themedStyles = StyleService.create({
|
||||
container: {},
|
||||
name: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
fontWeight: '700',
|
||||
},
|
||||
license: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
fontWeight: '700',
|
||||
marginLeft: 10,
|
||||
color: 'text-hint-color',
|
||||
...fontSize.sm,
|
||||
},
|
||||
version: {
|
||||
fontFamily: Platform.OS === 'ios' ? 'Courier New' : 'monospace',
|
||||
minWidth: 55,
|
||||
fontWeight: '700',
|
||||
color: 'text-hint-color',
|
||||
...fontSize.sm,
|
||||
},
|
||||
bottomRow: {
|
||||
marginTop: 4,
|
||||
flexDirection: 'row',
|
||||
},
|
||||
})
|
|
@ -21,11 +21,10 @@ import {
|
|||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||
import AppStorage from '../services/appStorage'
|
||||
import { schema } from '../app.json'
|
||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||
import { useTranslation } from '../hooks/useTranslation'
|
||||
import { Layout } from '../styles'
|
||||
import { translate } from '../utils/translation'
|
||||
import {
|
||||
CheckIcon,
|
||||
CloseOutlineIcon,
|
||||
|
@ -49,63 +48,21 @@ export const Login = () => {
|
|||
const [visible, showModal] = useState(false)
|
||||
const [showLoginMethod, setShowLoginMethod] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [personalIdNumber, setPersonalIdNumber] = useState('')
|
||||
const [valid, setValid] = useState(false)
|
||||
const [loginMethodIndex, setLoginMethodIndex] = useState(0)
|
||||
const [cachedLoginMethodIndex, setCachedLoginMethodIndex] =
|
||||
useSettingsStorage('loginMethodIndex', '0')
|
||||
const [personalIdNumber, setPersonalIdNumber] = useSettingsStorage(
|
||||
'cachedPersonalIdentityNumber'
|
||||
)
|
||||
const [loginMethodIndex, setLoginMethodIndex] =
|
||||
useSettingsStorage('loginMethodIndex')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const valid = Personnummer.valid(personalIdNumber)
|
||||
|
||||
const loginMethods = [
|
||||
translate('auth.bankid.OpenOnThisDevice'),
|
||||
translate('auth.bankid.OpenOnAnotherDevice'),
|
||||
translate('auth.loginAsTestUser'),
|
||||
t('auth.bankid.OpenOnThisDevice'),
|
||||
t('auth.bankid.OpenOnAnotherDevice'),
|
||||
t('auth.loginAsTestUser'),
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
if (loginMethodIndex !== parseInt(cachedLoginMethodIndex, 10)) {
|
||||
setCachedLoginMethodIndex(loginMethodIndex.toString())
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loginMethodIndex])
|
||||
|
||||
useEffect(() => {
|
||||
if (loginMethodIndex !== parseInt(cachedLoginMethodIndex, 10)) {
|
||||
setLoginMethodIndex(parseInt(cachedLoginMethodIndex, 10))
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cachedLoginMethodIndex])
|
||||
|
||||
useEffect(() => {
|
||||
setValid(Personnummer.valid(personalIdNumber))
|
||||
}, [personalIdNumber])
|
||||
|
||||
useEffect(() => {
|
||||
async function SetPersonalIdNumberIfSaved() {
|
||||
const storedPersonalIdNumber = await AppStorage.getSetting<string>(
|
||||
'cachedPersonalIdentityNumber'
|
||||
)
|
||||
|
||||
if (storedPersonalIdNumber) {
|
||||
setPersonalIdNumber(storedPersonalIdNumber)
|
||||
}
|
||||
}
|
||||
|
||||
SetPersonalIdNumberIfSaved()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
async function SavePersonalIdNumber(numberToSave: string) {
|
||||
if (numberToSave) {
|
||||
await AppStorage.setSetting(
|
||||
'cachedPersonalIdentityNumber',
|
||||
numberToSave
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SavePersonalIdNumber(personalIdNumber)
|
||||
}, [personalIdNumber])
|
||||
|
||||
const loginHandler = async () => {
|
||||
showModal(false)
|
||||
}
|
||||
|
@ -119,7 +76,6 @@ export const Login = () => {
|
|||
|
||||
/* Helpers */
|
||||
const handleInput = (text: string) => {
|
||||
setValid(Personnummer.valid(text))
|
||||
setPersonalIdNumber(text)
|
||||
}
|
||||
|
||||
|
@ -132,7 +88,7 @@ export const Login = () => {
|
|||
: `bankid:///?autostarttoken=${token}&redirect=null`
|
||||
Linking.openURL(bankIdUrl)
|
||||
} catch (err) {
|
||||
setError(translate('auth.bankid.OpenManually'))
|
||||
setError(t('auth.bankid.OpenManually'))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,7 +110,7 @@ export const Login = () => {
|
|||
status.on('PENDING', () => console.log('BankID app not yet opened'))
|
||||
status.on('USER_SIGN', () => console.log('BankID app is open'))
|
||||
status.on('ERROR', () => {
|
||||
setError(translate('auth.loginFailed'))
|
||||
setError(t('auth.loginFailed'))
|
||||
showModal(false)
|
||||
})
|
||||
status.on('OK', () => console.log('BankID ok'))
|
||||
|
@ -171,7 +127,7 @@ export const Login = () => {
|
|||
{loginMethodIndex === 1 && (
|
||||
<Input
|
||||
accessible={true}
|
||||
label={translate('general.socialSecurityNumber')}
|
||||
label={t('general.socialSecurityNumber')}
|
||||
autoFocus
|
||||
value={personalIdNumber}
|
||||
style={styles.pnrInput}
|
||||
|
@ -180,7 +136,7 @@ export const Login = () => {
|
|||
<TouchableWithoutFeedback
|
||||
accessible={true}
|
||||
onPress={() => handleInput('')}
|
||||
accessibilityHint={translate(
|
||||
accessibilityHint={t(
|
||||
'login.a11y_clear_social_security_input_field',
|
||||
{
|
||||
defaultValue: 'Rensa fältet för personnummer',
|
||||
|
@ -194,7 +150,7 @@ export const Login = () => {
|
|||
onSubmitEditing={(event) => startLogin(event.nativeEvent.text)}
|
||||
caption={error || ''}
|
||||
onChangeText={(text) => handleInput(text)}
|
||||
placeholder={translate('auth.placeholder_SocialSecurityNumber')}
|
||||
placeholder={t('auth.placeholder_SocialSecurityNumber')}
|
||||
/>
|
||||
)}
|
||||
<ButtonGroup style={styles.loginButtonGroup} status="primary">
|
||||
|
@ -220,7 +176,7 @@ export const Login = () => {
|
|||
status="primary"
|
||||
accessoryLeft={SelectIcon}
|
||||
size="medium"
|
||||
accessibilityHint={translate('login.a11y_select_login_method', {
|
||||
accessibilityHint={t('login.a11y_select_login_method', {
|
||||
defaultValue: 'Välj inloggningsmetod',
|
||||
})}
|
||||
/>
|
||||
|
@ -234,7 +190,7 @@ export const Login = () => {
|
|||
>
|
||||
<Card>
|
||||
<Text category="h5" style={styles.bankIdLoading}>
|
||||
{translate('auth.chooseLoginMethod')}
|
||||
{t('auth.chooseLoginMethod')}
|
||||
</Text>
|
||||
<List
|
||||
data={loginMethods}
|
||||
|
@ -260,7 +216,7 @@ export const Login = () => {
|
|||
setShowLoginMethod(false)
|
||||
}}
|
||||
>
|
||||
{translate('general.cancel')}
|
||||
{t('general.cancel')}
|
||||
</Button>
|
||||
</Card>
|
||||
</Modal>
|
||||
|
@ -271,9 +227,7 @@ export const Login = () => {
|
|||
backdropStyle={styles.backdrop}
|
||||
>
|
||||
<Card disabled>
|
||||
<Text style={styles.bankIdLoading}>
|
||||
{translate('auth.bankid.Waiting')}
|
||||
</Text>
|
||||
<Text style={styles.bankIdLoading}>{t('auth.bankid.Waiting')}</Text>
|
||||
|
||||
<Button
|
||||
status="primary"
|
||||
|
@ -283,7 +237,7 @@ export const Login = () => {
|
|||
showModal(false)
|
||||
}}
|
||||
>
|
||||
{translate('general.cancel')}
|
||||
{t('general.cancel')}
|
||||
</Button>
|
||||
</Card>
|
||||
</Modal>
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
NewsItem as NewsItemType,
|
||||
} from '@skolplattformen/embedded-api'
|
||||
import { useTheme } from '@ui-kitten/components'
|
||||
import { Library } from 'libraries.json'
|
||||
import React, { useEffect } from 'react'
|
||||
import { StatusBar, useColorScheme } from 'react-native'
|
||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||
|
@ -14,16 +15,42 @@ import {
|
|||
lightNavigationTheme,
|
||||
} from '../design/navigationThemes'
|
||||
import { useAppState } from '../hooks/useAppState'
|
||||
import { useLangCode } from '../hooks/useLangCode'
|
||||
import useSettingsStorage, {
|
||||
initializeSettingsState,
|
||||
} from '../hooks/useSettingsStorage'
|
||||
import { isRTL } from '../services/languageService'
|
||||
import Absence, { absenceRouteOptions } from './absence.component'
|
||||
import { Auth, authRouteOptions } from './auth.component'
|
||||
import { Child, childRouteOptions } from './child.component'
|
||||
import { childenRouteOptions, Children } from './children.component'
|
||||
import { libraryRouteOptions, LibraryScreen } from './library.component'
|
||||
import { NewsItem, newsItemRouteOptions } from './newsItem.component'
|
||||
import { SetLanguage, setLanguageRouteOptions } from './setLanguage.component'
|
||||
import { settingsRouteOptions, SettingsScreen } from './settings.component'
|
||||
import {
|
||||
settingsAppearanceRouteOptions,
|
||||
SettingsAppearanceScreen,
|
||||
} from './settingsAppearance.component'
|
||||
import {
|
||||
settingsAppearanceThemeRouteOptions,
|
||||
SettingsAppearanceThemeScreen,
|
||||
} from './settingsAppearanceTheme.component'
|
||||
import {
|
||||
settingsLicensesRouteOptions,
|
||||
SettingsLicensesScreen,
|
||||
} from './settingsLicenses.component'
|
||||
|
||||
export type RootStackParamList = {
|
||||
Login: undefined
|
||||
Children: undefined
|
||||
Settings: undefined
|
||||
SettingsAppearance: undefined
|
||||
SettingsAppearanceTheme: undefined
|
||||
SettingsLicenses: undefined
|
||||
Library: {
|
||||
library: Library
|
||||
}
|
||||
Child: {
|
||||
child: ChildType
|
||||
color: string
|
||||
|
@ -48,11 +75,20 @@ const linking = {
|
|||
export const AppNavigator = () => {
|
||||
const { isLoggedIn, api } = useApi()
|
||||
|
||||
const colorScheme = useColorScheme()
|
||||
const [usingSystemTheme] = useSettingsStorage('usingSystemTheme')
|
||||
const [theme] = useSettingsStorage('theme')
|
||||
const systemTheme = useColorScheme()
|
||||
const colorScheme = usingSystemTheme ? systemTheme : theme
|
||||
const langCode = useLangCode()
|
||||
|
||||
const colors = useTheme()
|
||||
|
||||
const currentAppState = useAppState()
|
||||
|
||||
useEffect(() => {
|
||||
initializeSettingsState()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const checkUser = async () => {
|
||||
if (currentAppState === 'active' && isLoggedIn) {
|
||||
|
@ -76,9 +112,16 @@ export const AppNavigator = () => {
|
|||
<StatusBar />
|
||||
<Navigator
|
||||
screenOptions={() => ({
|
||||
headerLargeTitle: false,
|
||||
headerLargeTitle: true,
|
||||
headerLargeTitleHideShadow: true,
|
||||
direction: isRTL(langCode) ? 'rtl' : 'ltr',
|
||||
headerStyle: {
|
||||
backgroundColor:
|
||||
colorScheme === 'dark'
|
||||
? colors['background-basic-color-2']
|
||||
: colors['background-basic-color-1'],
|
||||
},
|
||||
headerLargeStyle: {
|
||||
backgroundColor: colors['background-basic-color-2'],
|
||||
},
|
||||
headerLargeTitleStyle: {
|
||||
|
@ -112,13 +155,38 @@ export const AppNavigator = () => {
|
|||
) : (
|
||||
<>
|
||||
<Screen name="Login" component={Auth} options={authRouteOptions} />
|
||||
<Screen
|
||||
name="SetLanguage"
|
||||
component={SetLanguage}
|
||||
options={setLanguageRouteOptions}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Screen
|
||||
name="SetLanguage"
|
||||
component={SetLanguage}
|
||||
options={setLanguageRouteOptions}
|
||||
/>
|
||||
<Screen
|
||||
name="Settings"
|
||||
component={SettingsScreen}
|
||||
options={settingsRouteOptions}
|
||||
/>
|
||||
<Screen
|
||||
name="SettingsAppearance"
|
||||
component={SettingsAppearanceScreen}
|
||||
options={settingsAppearanceRouteOptions}
|
||||
/>
|
||||
<Screen
|
||||
name="SettingsAppearanceTheme"
|
||||
component={SettingsAppearanceThemeScreen}
|
||||
options={settingsAppearanceThemeRouteOptions}
|
||||
/>
|
||||
<Screen
|
||||
name="SettingsLicenses"
|
||||
component={SettingsLicensesScreen}
|
||||
options={settingsLicensesRouteOptions}
|
||||
/>
|
||||
<Screen
|
||||
name="Library"
|
||||
component={LibraryScreen}
|
||||
options={libraryRouteOptions}
|
||||
/>
|
||||
</Navigator>
|
||||
</NavigationContainer>
|
||||
)
|
||||
|
|
|
@ -3,12 +3,10 @@ import {
|
|||
Button,
|
||||
ButtonGroup,
|
||||
StyleService,
|
||||
Text,
|
||||
useStyleSheet,
|
||||
useTheme,
|
||||
} from '@ui-kitten/components'
|
||||
import React, { useState } from 'react'
|
||||
import { View, TouchableOpacity } from 'react-native'
|
||||
import { View } from 'react-native'
|
||||
import { ScrollView } from 'react-native-gesture-handler'
|
||||
import RNRestart from 'react-native-restart'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
|
@ -16,9 +14,11 @@ import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
|||
import { useLanguage } from '../hooks/useLanguage'
|
||||
import { isRTL, LanguageService } from '../services/languageService'
|
||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
||||
import { fontSize } from '../styles/typography'
|
||||
import { languages, translate } from '../utils/translation'
|
||||
import { CheckIcon } from './icon.component'
|
||||
import {
|
||||
SettingGroup,
|
||||
SettingListItemSelectable,
|
||||
} from './settingsComponents.component'
|
||||
|
||||
export const setLanguageRouteOptions = (): NativeStackNavigationOptions => ({
|
||||
title: translate('language.changeLanguage'),
|
||||
|
@ -27,7 +27,6 @@ export const setLanguageRouteOptions = (): NativeStackNavigationOptions => ({
|
|||
export const SetLanguage = () => {
|
||||
const navigation = useNavigation()
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
const colors = useTheme()
|
||||
|
||||
const currentLanguage = LanguageService.getLanguageCode()
|
||||
|
||||
|
@ -56,41 +55,27 @@ export const SetLanguage = () => {
|
|||
|
||||
const goBack = () => {
|
||||
// Need to reset the view so it updates the language
|
||||
navigation.navigate('Login', { rand: Math.random() })
|
||||
navigation.navigate('Settings', { rand: Math.random() })
|
||||
}
|
||||
|
||||
const activeLanguages = languages.filter((language) => language.active)
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||
<ScrollView>
|
||||
<View style={styles.content}>
|
||||
<ScrollView contentContainerStyle={styles.scrollView}>
|
||||
<SettingGroup>
|
||||
<View style={styles.languageList}>
|
||||
{activeLanguages.map((language) => (
|
||||
<TouchableOpacity
|
||||
<SettingListItemSelectable
|
||||
key={language.langCode}
|
||||
style={styles.languageButton}
|
||||
onPress={() => setSelectedLanguage(language.langCode)}
|
||||
>
|
||||
<View>
|
||||
<Text style={styles.languageButtonTitle}>
|
||||
{language.languageLocalName}
|
||||
</Text>
|
||||
<Text style={styles.languageButtonSubtitle}>
|
||||
{language.languageName}
|
||||
</Text>
|
||||
</View>
|
||||
{isSelected(language.langCode) ? (
|
||||
<CheckIcon
|
||||
height={24}
|
||||
width={24}
|
||||
fill={colors['color-success-600']}
|
||||
/>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
title={language.languageLocalName}
|
||||
subTitle={language.languageName}
|
||||
isSelected={isSelected(language.langCode)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</SettingGroup>
|
||||
</ScrollView>
|
||||
<ButtonGroup style={styles.buttonGroup}>
|
||||
<Button
|
||||
|
@ -114,6 +99,7 @@ const themedStyles = StyleService.create({
|
|||
alignSelf: 'stretch',
|
||||
flexDirection: 'column',
|
||||
marginTop: 8,
|
||||
paddingHorizontal: Sizing.t4,
|
||||
},
|
||||
icon: {
|
||||
width: 30,
|
||||
|
@ -121,32 +107,14 @@ const themedStyles = StyleService.create({
|
|||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'background-basic-color-2',
|
||||
},
|
||||
content: {
|
||||
...LayoutStyle.center,
|
||||
...LayoutStyle.flex.full,
|
||||
margin: Sizing.t5,
|
||||
paddingBottom: Sizing.t5,
|
||||
scrollView: {
|
||||
padding: Sizing.t4,
|
||||
},
|
||||
buttonGroup: {
|
||||
minHeight: 45,
|
||||
marginTop: 20,
|
||||
marginHorizontal: Sizing.t5,
|
||||
},
|
||||
languageButton: {
|
||||
minHeight: 45,
|
||||
marginBottom: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
languageButtonTitle: {
|
||||
...fontSize.lg,
|
||||
},
|
||||
languageButtonSubtitle: {
|
||||
...fontSize.sm,
|
||||
color: 'text-hint-color',
|
||||
},
|
||||
button: { ...LayoutStyle.flex.full },
|
||||
})
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/core'
|
||||
import { useApi } from '@skolplattformen/api-hooks'
|
||||
import React, { useCallback } from 'react'
|
||||
import { ScrollView } from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||
import AppStorage from '../services/appStorage'
|
||||
import { LanguageService } from '../services/languageService'
|
||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
||||
import { languages, translate } from '../utils/translation'
|
||||
import { AwardIcon, BrushIcon, GlobeIcon } from './icon.component'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
import {
|
||||
SettingGroup,
|
||||
SettingListItem,
|
||||
SettingListSeparator,
|
||||
} from './settingsComponents.component'
|
||||
import { VersionInfo } from './versionInfo.component'
|
||||
|
||||
export const settingsRouteOptions = (): NativeStackNavigationOptions => ({
|
||||
title: translate('settings.settings'),
|
||||
})
|
||||
|
||||
export const SettingsScreen = () => {
|
||||
const [isUsingSystemTheme] = useSettingsStorage('usingSystemTheme')
|
||||
const [settingsTheme] = useSettingsStorage('theme')
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>()
|
||||
const langCode = LanguageService.getLanguageCode()
|
||||
const language = languages.find((l) => l.langCode === langCode)
|
||||
const { api } = useApi()
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await AppStorage.clearTemporaryItems()
|
||||
await api.logout()
|
||||
navigation.reset({
|
||||
routes: [{ name: 'Login' }],
|
||||
})
|
||||
}, [api, navigation])
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={LayoutStyle.flex.full}
|
||||
contentContainerStyle={{
|
||||
padding: Sizing.t4,
|
||||
}}
|
||||
>
|
||||
<SettingGroup>
|
||||
<SettingListItem
|
||||
label={translate('settings.appearance')}
|
||||
value={
|
||||
isUsingSystemTheme
|
||||
? translate('settings.themeAuto')
|
||||
: translate(`themes.${settingsTheme}`)
|
||||
}
|
||||
icon={BrushIcon}
|
||||
onNavigate={() => navigation.navigate('SettingsAppearance')}
|
||||
/>
|
||||
<SettingListSeparator />
|
||||
<SettingListItem
|
||||
label={translate('settings.language')}
|
||||
value={language?.languageLocalName}
|
||||
icon={GlobeIcon}
|
||||
onNavigate={() => navigation.navigate('SetLanguage')}
|
||||
/>
|
||||
</SettingGroup>
|
||||
<SettingGroup>
|
||||
<SettingListItem
|
||||
label={translate('settings.licenses')}
|
||||
icon={AwardIcon}
|
||||
onNavigate={() => navigation.navigate('SettingsLicenses')}
|
||||
/>
|
||||
</SettingGroup>
|
||||
{api.isLoggedIn && (
|
||||
<SettingGroup>
|
||||
<SettingListItem
|
||||
label={translate('general.logout')}
|
||||
onPress={logout}
|
||||
/>
|
||||
</SettingGroup>
|
||||
)}
|
||||
|
||||
<VersionInfo />
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { NavigationProp, useNavigation } from '@react-navigation/core'
|
||||
import React from 'react'
|
||||
import { ScrollView, StyleSheet, Switch } from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
||||
import { translate } from '../utils/translation'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
import {
|
||||
SettingGroup,
|
||||
SettingListItem,
|
||||
SettingListSeparator,
|
||||
} from './settingsComponents.component'
|
||||
|
||||
export const settingsAppearanceRouteOptions =
|
||||
(): NativeStackNavigationOptions => ({
|
||||
title: translate('settings.appearance'),
|
||||
})
|
||||
|
||||
export const SettingsAppearanceScreen = () => {
|
||||
const [isUsingSystemTheme, setUsingSystemTheme] =
|
||||
useSettingsStorage('usingSystemTheme')
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>()
|
||||
|
||||
const [settingsTheme] = useSettingsStorage('theme')
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={LayoutStyle.flex.full}
|
||||
contentContainerStyle={styles.container}
|
||||
>
|
||||
<SettingGroup>
|
||||
<SettingListItem label={translate('settings.useSystemTheme')}>
|
||||
<Switch
|
||||
value={isUsingSystemTheme}
|
||||
onValueChange={setUsingSystemTheme}
|
||||
/>
|
||||
</SettingListItem>
|
||||
{!isUsingSystemTheme && (
|
||||
<>
|
||||
<SettingListSeparator />
|
||||
<SettingListItem
|
||||
label={translate('settings.theme')}
|
||||
value={translate(`themes.${settingsTheme}`)}
|
||||
onNavigate={() => navigation.navigate('SettingsAppearanceTheme')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: Sizing.t4,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,52 @@
|
|||
import React from 'react'
|
||||
import { ScrollView, StyleSheet, View } from 'react-native'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
||||
import { translate } from '../utils/translation'
|
||||
import {
|
||||
SettingGroup,
|
||||
SettingListItemSelectable,
|
||||
} from './settingsComponents.component'
|
||||
|
||||
export const settingsAppearanceThemeRouteOptions =
|
||||
(): NativeStackNavigationOptions => ({
|
||||
title: translate('settings.theme'),
|
||||
})
|
||||
|
||||
const themes = ['light', 'dark']
|
||||
|
||||
export const SettingsAppearanceThemeScreen = () => {
|
||||
const [settingsTheme, setSettingsTheme] = useSettingsStorage('theme')
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={LayoutStyle.flex.full}
|
||||
contentContainerStyle={styles.container}
|
||||
>
|
||||
<SettingGroup>
|
||||
<View style={styles.themeList}>
|
||||
{themes.map((theme) => {
|
||||
return (
|
||||
<SettingListItemSelectable
|
||||
key={theme}
|
||||
onPress={() => setSettingsTheme(theme)}
|
||||
title={translate(`themes.${theme}`)}
|
||||
isSelected={theme === settingsTheme}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
</SettingGroup>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: Sizing.t4,
|
||||
},
|
||||
themeList: {
|
||||
paddingHorizontal: Sizing.t4,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,194 @@
|
|||
import {
|
||||
IconProps,
|
||||
StyleService,
|
||||
Text,
|
||||
useStyleSheet,
|
||||
useTheme,
|
||||
} from '@ui-kitten/components'
|
||||
import React, { useState } from 'react'
|
||||
import { Pressable, TouchableOpacity, View } from 'react-native'
|
||||
import { useLangRTL } from '../hooks/useLangRTL'
|
||||
import { Sizing } from '../styles'
|
||||
import { fontSize } from '../styles/typography'
|
||||
import { CheckIcon, RightArrowIcon } from './icon.component'
|
||||
|
||||
export const SettingListItem = ({
|
||||
label,
|
||||
value,
|
||||
icon: Icon,
|
||||
onNavigate,
|
||||
onPress,
|
||||
children,
|
||||
}: {
|
||||
label?: string
|
||||
value?: string
|
||||
icon?: (props: IconProps) => JSX.Element
|
||||
onNavigate?: () => void
|
||||
onPress?: () => void
|
||||
children?: React.ReactNode
|
||||
}) => {
|
||||
const textHintColor = useTheme()['text-hint-color']
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
const isRTL = useLangRTL()
|
||||
|
||||
const [isPressing, setIsPressing] = useState(false)
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onNavigate || onPress}
|
||||
onPressIn={() => setIsPressing(true)}
|
||||
onPressOut={() => setIsPressing(false)}
|
||||
>
|
||||
<SettingListItemWrapper
|
||||
isPressing={(onNavigate || onPress) && isPressing}
|
||||
>
|
||||
{Icon && (
|
||||
<View style={styles.icon}>
|
||||
<Icon width="24" height="24" fill="#fff" />
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.listItemText}>
|
||||
{label && (
|
||||
<Text
|
||||
style={[styles.listItemLabel, onPress && styles.listItemButton]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
{value && <Text style={styles.listItemValue}>{value}</Text>}
|
||||
{children}
|
||||
</View>
|
||||
{onNavigate && (
|
||||
<View
|
||||
style={[
|
||||
styles.arrow,
|
||||
{ transform: [{ rotateY: isRTL ? '180deg' : '0deg' }] },
|
||||
]}
|
||||
>
|
||||
<RightArrowIcon width="24" height="24" fill={textHintColor} />
|
||||
</View>
|
||||
)}
|
||||
</SettingListItemWrapper>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingListSeparator = () => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
return <View style={styles.separator} />
|
||||
}
|
||||
|
||||
export const SettingListItemWrapper = ({
|
||||
children,
|
||||
isPressing = false,
|
||||
}: {
|
||||
isPressing?: boolean
|
||||
children?: React.ReactNode
|
||||
}) => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
return (
|
||||
<View style={[styles.listItem, isPressing ? styles.listItemPressed : null]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingGroup = ({ children }: { children?: React.ReactNode }) => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
|
||||
return <View style={styles.group}>{children}</View>
|
||||
}
|
||||
|
||||
export const SettingListItemSelectable = ({
|
||||
title,
|
||||
subTitle,
|
||||
isSelected,
|
||||
onPress,
|
||||
}: {
|
||||
title: string
|
||||
subTitle?: string
|
||||
isSelected?: boolean
|
||||
onPress: () => void
|
||||
}) => {
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
const colors = useTheme()
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={styles.selectableButton} onPress={onPress}>
|
||||
<View>
|
||||
<Text style={styles.selectableButtonTitle}>{title}</Text>
|
||||
{subTitle && (
|
||||
<Text style={styles.selectableButtonSubtitle}>{subTitle}</Text>
|
||||
)}
|
||||
</View>
|
||||
{isSelected ? (
|
||||
<CheckIcon height={24} width={24} fill={colors['color-success-600']} />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
const themedStyles = StyleService.create({
|
||||
group: {
|
||||
backgroundColor: 'background-basic-color-1',
|
||||
borderRadius: 15,
|
||||
marginBottom: Sizing.t5,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
listItem: {
|
||||
paddingHorizontal: Sizing.t4,
|
||||
paddingVertical: Sizing.t2,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
listItemButton: {
|
||||
color: 'color-tab-focused',
|
||||
},
|
||||
listItemPressed: {
|
||||
backgroundColor: 'color-separator',
|
||||
},
|
||||
listItemText: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
listItemLabel: {
|
||||
...fontSize.sm,
|
||||
},
|
||||
listItemValue: {
|
||||
...fontSize.xs,
|
||||
color: 'text-hint-color',
|
||||
flexShrink: 0,
|
||||
},
|
||||
separator: {
|
||||
height: 1,
|
||||
marginLeft: Sizing.t4,
|
||||
backgroundColor: 'color-separator',
|
||||
},
|
||||
icon: {
|
||||
backgroundColor: 'color-primary-500',
|
||||
borderRadius: 5,
|
||||
padding: 3,
|
||||
marginRight: Sizing.t3,
|
||||
},
|
||||
arrow: { flexShrink: 0 },
|
||||
selectableButton: {
|
||||
paddingVertical: Sizing.t2,
|
||||
minHeight: 45,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
selectableButtonTitle: {
|
||||
...fontSize.base,
|
||||
textAlign: 'left',
|
||||
},
|
||||
selectableButtonSubtitle: {
|
||||
...fontSize.sm,
|
||||
color: 'text-hint-color',
|
||||
textAlign: 'left',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react'
|
||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||
import libraries from '../libraries.json'
|
||||
import { translate } from '../utils/translation'
|
||||
import { LibraryList } from './libraryList.component'
|
||||
|
||||
export const settingsLicensesRouteOptions =
|
||||
(): NativeStackNavigationOptions => ({
|
||||
title: `${translate('settings.licenses')}`,
|
||||
})
|
||||
|
||||
export const SettingsLicensesScreen = () => {
|
||||
return <LibraryList libraries={libraries} />
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { Text } from '@ui-kitten/components'
|
||||
import React from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import { getBuildNumber, getVersion } from 'react-native-device-info'
|
||||
|
||||
export const VersionInfo = () => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>
|
||||
v{getVersion()} ({getBuildNumber()})
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
|
@ -66,7 +66,7 @@
|
|||
"color-danger-transparent-400": "rgba(186, 50, 127, 0.32)",
|
||||
"color-danger-transparent-500": "rgba(186, 50, 127, 0.4)",
|
||||
"color-danger-transparent-600": "rgba(186, 50, 127, 0.48)",
|
||||
"background-basic-color-1": "#150A12",
|
||||
"background-basic-color-1": "#0F1117",
|
||||
"background-basic-color-2": "#030200",
|
||||
"text-hint-color": "#B3BBCB",
|
||||
"color-control-default": "#E5E7EB",
|
||||
|
@ -78,5 +78,6 @@
|
|||
"color-input-border": "$color-basic-300",
|
||||
"color-tab-default": "$color-primary-50",
|
||||
"color-tab-focused": "$color-primary-200",
|
||||
"color-button-ghost-text": "$color-primary-200"
|
||||
"color-button-ghost-text": "$color-primary-200",
|
||||
"color-separator": "$color-basic-900"
|
||||
}
|
||||
|
|
|
@ -83,8 +83,9 @@
|
|||
"color-basic-text": "$color-primary-800",
|
||||
"color-input-border": "$color-basic-800",
|
||||
"background-basic-color-1": "#fff",
|
||||
"background-basic-color-2": "#f7f9fc",
|
||||
"background-basic-color-2": "#F2F1F6",
|
||||
"color-tab-default": "$color-basic-700",
|
||||
"color-tab-focused": "$color-primary-500",
|
||||
"color-button-ghost-text": "$color-primary-500"
|
||||
"color-button-ghost-text": "$color-primary-500",
|
||||
"color-separator": "$color-basic-400"
|
||||
}
|
||||
|
|
|
@ -1,59 +1,54 @@
|
|||
import { renderHook, act } from '@testing-library/react-hooks'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import useSettingsStorage from '../useSettingsStorage'
|
||||
import { act, renderHook } from '@testing-library/react-hooks'
|
||||
import AppStorage from '../../services/appStorage'
|
||||
import useSettingsStorage, { settingsState } from '../useSettingsStorage'
|
||||
|
||||
beforeEach(() => {
|
||||
AsyncStorage.clear()
|
||||
// TODO: This is a bit ugly. Should probably fix that.
|
||||
settingsState.settings.theme = 'light'
|
||||
})
|
||||
|
||||
const prefix = AppStorage.settingsStorageKeyPrefix
|
||||
|
||||
test('use key prefix on set', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsStorage('key', '')
|
||||
useSettingsStorage('theme')
|
||||
)
|
||||
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
const [, setValue] = result.current
|
||||
setValue('foo')
|
||||
setValue('dark')
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
const data = await AsyncStorage.getItem(prefix + 'SETTINGS')
|
||||
const parsed = JSON.parse(data ?? '')
|
||||
|
||||
expect(parsed.theme).toEqual('dark')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('foo')
|
||||
)
|
||||
})
|
||||
|
||||
test('return inital value if no set', async () => {
|
||||
const { result } = renderHook(() => useSettingsStorage('key', 'initialValue'))
|
||||
|
||||
const [value] = result.current
|
||||
|
||||
expect(value).toEqual('initialValue')
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(null)
|
||||
})
|
||||
|
||||
test('update value', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsStorage('key', 'initialValue')
|
||||
useSettingsStorage('theme')
|
||||
)
|
||||
|
||||
const [initValue, setValue] = result.current
|
||||
|
||||
act(() => {
|
||||
setValue('update')
|
||||
await act(async () => {
|
||||
setValue('dark')
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
const [updateValue] = result.current
|
||||
|
||||
expect(initValue).toEqual('light')
|
||||
expect(updateValue).toEqual('dark')
|
||||
|
||||
const data = await AsyncStorage.getItem(prefix + 'SETTINGS')
|
||||
const parsed = JSON.parse(data ?? '')
|
||||
|
||||
expect(parsed.theme).toEqual('dark')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
const [updateValue] = result.current
|
||||
|
||||
expect(initValue).toEqual('initialValue')
|
||||
expect(updateValue).toEqual('update')
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('update')
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function useAsyncStorage<T>(
|
||||
storageKey: string,
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import { LanguageService } from '../services/languageService'
|
||||
|
||||
const generateKey = () => {
|
||||
return `${Date.now()}-${Math.random() * 1000}`
|
||||
}
|
||||
|
||||
export const useLangCode = () => {
|
||||
const [langCode, setLangCode] = useState(LanguageService.getLanguageCode())
|
||||
|
||||
const key = useRef(generateKey())
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = LanguageService.onChange(
|
||||
{ key: key.current },
|
||||
(lang) => {
|
||||
setLangCode(lang)
|
||||
}
|
||||
)
|
||||
|
||||
return () => unsubscribe()
|
||||
}, [])
|
||||
|
||||
return langCode
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { isRTL } from '../services/languageService'
|
||||
import { useLangCode } from './useLangCode'
|
||||
|
||||
export const useLangRTL = () => {
|
||||
const langCode = useLangCode()
|
||||
return isRTL(langCode)
|
||||
}
|
|
@ -1,10 +1,50 @@
|
|||
import useAsyncStorage from './useAsyncStorage'
|
||||
import { useCallback } from 'react'
|
||||
import { proxy, subscribe, useSnapshot } from 'valtio'
|
||||
import AppStorage from '../services/appStorage'
|
||||
|
||||
export default function useSettingsStorage<T>(
|
||||
storageKey: string,
|
||||
defaultValue: T
|
||||
): [T, (val: T) => void] {
|
||||
const settingsKey = AppStorage.settingsStorageKeyPrefix + storageKey
|
||||
return useAsyncStorage(settingsKey, defaultValue)
|
||||
export const settingsState = proxy({
|
||||
hydrated: false,
|
||||
settings: {
|
||||
loginMethodIndex: 0,
|
||||
usingSystemTheme: true,
|
||||
theme: 'light',
|
||||
cachedPersonalIdentityNumber: '',
|
||||
},
|
||||
})
|
||||
|
||||
export type Settings = typeof settingsState['settings']
|
||||
|
||||
const SETTINGS_STORAGE_KEY = 'SETTINGS'
|
||||
|
||||
subscribe(settingsState, () => {
|
||||
AppStorage.setSetting(SETTINGS_STORAGE_KEY, settingsState.settings)
|
||||
})
|
||||
|
||||
export const initializeSettingsState = async () => {
|
||||
const settings = await AppStorage.getSetting<any>(SETTINGS_STORAGE_KEY)
|
||||
|
||||
settingsState.hydrated = true
|
||||
|
||||
if (settings) {
|
||||
settingsState.settings = {
|
||||
...settingsState.settings,
|
||||
...settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function useSettingsStorage<
|
||||
TKey extends keyof Settings,
|
||||
TValue = Settings[TKey]
|
||||
>(key: TKey) {
|
||||
const { settings } = useSnapshot(settingsState)
|
||||
|
||||
const setter = useCallback(
|
||||
(value: TValue) => {
|
||||
settingsState.settings[key] = value as any
|
||||
},
|
||||
[key]
|
||||
)
|
||||
|
||||
return [settings[key], setter] as const
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import i18n from 'i18n-js'
|
||||
import { useMemo } from 'react'
|
||||
import { useLangCode } from './useLangCode'
|
||||
|
||||
export const useTranslation = () => {
|
||||
const langCode = useLangCode()
|
||||
const output = useMemo(() => {
|
||||
return { t: i18n.t, langCode }
|
||||
}, [langCode])
|
||||
return output
|
||||
}
|
|
@ -287,8 +287,6 @@ PODS:
|
|||
- React-jsi (= 0.65.1)
|
||||
- React-perflogger (= 0.65.1)
|
||||
- React-jsinspector (0.65.1)
|
||||
- react-native-appearance (0.3.4):
|
||||
- React
|
||||
- react-native-cookies (5.0.1):
|
||||
- React-Core
|
||||
- react-native-restart (0.0.22):
|
||||
|
@ -372,6 +370,8 @@ PODS:
|
|||
- React
|
||||
- RNDateTimePicker (3.4.3):
|
||||
- React-Core
|
||||
- RNDeviceInfo (8.3.3):
|
||||
- React-Core
|
||||
- RNDevMenu (4.0.2):
|
||||
- React-Core
|
||||
- React-Core/DevSupport
|
||||
|
@ -461,7 +461,6 @@ DEPENDENCIES:
|
|||
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
||||
- react-native-appearance (from `../node_modules/react-native-appearance`)
|
||||
- "react-native-cookies (from `../node_modules/@react-native-community/cookies`)"
|
||||
- react-native-restart (from `../node_modules/react-native-restart`)
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
|
@ -483,6 +482,7 @@ DEPENDENCIES:
|
|||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
- RNDevMenu (from `../node_modules/react-native-dev-menu`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||
|
@ -544,8 +544,6 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
|
||||
React-jsinspector:
|
||||
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
||||
react-native-appearance:
|
||||
:path: "../node_modules/react-native-appearance"
|
||||
react-native-cookies:
|
||||
:path: "../node_modules/@react-native-community/cookies"
|
||||
react-native-restart:
|
||||
|
@ -588,6 +586,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/@react-native-community/masked-view"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNDeviceInfo:
|
||||
:path: "../node_modules/react-native-device-info"
|
||||
RNDevMenu:
|
||||
:path: "../node_modules/react-native-dev-menu"
|
||||
RNGestureHandler:
|
||||
|
@ -635,7 +635,6 @@ SPEC CHECKSUMS:
|
|||
React-jsi: 12913c841713a15f64eabf5c9ad98592c0ec5940
|
||||
React-jsiexecutor: 43f2542aed3c26e42175b339f8d37fe3dd683765
|
||||
React-jsinspector: 41e58e5b8e3e0bf061fdf725b03f2144014a8fb0
|
||||
react-native-appearance: 0f0e5fc2fcef70e03d48c8fe6b00b9158c2ba8aa
|
||||
react-native-cookies: ce50e42ace7cf0dd47769260ca5bbe8eee607e4e
|
||||
react-native-restart: 733a51ad137f15b0f8dc34c4082e55af7da00979
|
||||
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
||||
|
@ -657,6 +656,7 @@ SPEC CHECKSUMS:
|
|||
RNCAsyncStorage: 9b7605e899f9acb2fba33e87952c529731265453
|
||||
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
|
||||
RNDateTimePicker: d943800c936fb01c352fcfb70439550d2cb57092
|
||||
RNDeviceInfo: cc7de0772378f85d8f36ae439df20f05c590a651
|
||||
RNDevMenu: fd325b5554b61fe7f48d9205a3877cf5ee88cd7c
|
||||
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
|
||||
RNLocalize: 7f1e5792b65a839af55a9552d05b3558b66d017e
|
||||
|
|
|
@ -10,6 +10,6 @@ module.exports = {
|
|||
],
|
||||
testPathIgnorePatterns: ['__tests__/Classmates.test.js'],
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(jest-)?@react-native|react-native|@react-native-community|react-navigation|@react-navigation/.*|@ui-kitten|rn-actionsheet-module/.*)',
|
||||
'node_modules/(?!(jest-)?@react-native|react-native|@react-native-community|react-navigation|@react-navigation/.*|@ui-kitten/.*)',
|
||||
],
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
// Filters the output from 'react-native-oss-license'.
|
||||
|
||||
const fs = require('fs').promises
|
||||
const packageJson = require('./package.json')
|
||||
const rnLicenses = require('./licenses-oss.json')
|
||||
|
||||
/**
|
||||
* TOOD: Make this a bit more testable
|
||||
*/
|
||||
async function run() {
|
||||
try {
|
||||
const dependencies = Object.keys(packageJson.dependencies)
|
||||
|
||||
const result = rnLicenses.filter((pkg) => {
|
||||
return dependencies.find((name) => pkg.libraryName === name)
|
||||
})
|
||||
|
||||
await fs.writeFile(
|
||||
'./libraries.json',
|
||||
JSON.stringify(result, null, 2),
|
||||
'utf-8'
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
|
@ -12,7 +12,9 @@
|
|||
"test:watch": "jest --watch",
|
||||
"typecheck": "tsc --watch",
|
||||
"i18n": "sync-i18n --files '**/translations/*.json' --primary en --languages ar de pl so sv --space 2",
|
||||
"check-i18n": "npm run i18n -- --check"
|
||||
"check-i18n": "npm run i18n -- --check",
|
||||
"extract-licenses": "react-native-oss-license --json > licenses-oss.json && node library-extractor.js && rm licenses-oss.json",
|
||||
"postinstall": "yarn extract-licenses"
|
||||
},
|
||||
"dependencies": {
|
||||
"@eva-design/eva": "2.0.0",
|
||||
|
@ -33,18 +35,16 @@
|
|||
"deepmerge": "^4.2.2",
|
||||
"fast-fuzzy": "^1.10.8",
|
||||
"formik": "2.2.6",
|
||||
"hermes-engine": "0.7.2",
|
||||
"hermes-engine": "0.8.1",
|
||||
"i18n-js": "^3.8.0",
|
||||
"i18next-json-sync": "^2.3.1",
|
||||
"jsuri": "1.3.1",
|
||||
"moment": "^2.29.1",
|
||||
"personnummer": "3.1.3",
|
||||
"react": "17.0.2",
|
||||
"react-native": "0.65.1",
|
||||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-appearance": "^0.3.4",
|
||||
"react-native-calendar-events": "2.2.0",
|
||||
"react-native-dev-menu": "^4.0.2",
|
||||
"react-native-device-info": "^8.3.3",
|
||||
"react-native-fix-image": "2.1.0",
|
||||
"react-native-gesture-handler": "^1.10.3",
|
||||
"react-native-localize": "^2.0.2",
|
||||
|
@ -56,12 +56,9 @@
|
|||
"react-native-screens": "^3.3.0",
|
||||
"react-native-simple-toast": "1.1.3",
|
||||
"react-native-svg": "12.1.0",
|
||||
"react-native-svg-transformer": "0.14.3",
|
||||
"react-native-tab-view": "2.15.2",
|
||||
"react-native-typography": "1.4.1",
|
||||
"react-native-webview": "11.4.2",
|
||||
"react-native-weekly-calendar": "^0.2.0",
|
||||
"rn-actionsheet-module": "https://github.com/kolplattformen/rn-actionsheet-module.git",
|
||||
"valtio": "^1.2.3",
|
||||
"yup": "0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -92,6 +89,7 @@
|
|||
"prettier": "^2.2.1",
|
||||
"react-native-clean-project": "^3.6.3",
|
||||
"react-native-codegen": "^0.0.7",
|
||||
"react-native-oss-license": "^0.4.0",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { I18nManager } from 'react-native'
|
||||
import i18n from 'i18n-js'
|
||||
import merge from 'deepmerge'
|
||||
import i18n from 'i18n-js'
|
||||
import moment from 'moment'
|
||||
import 'moment/locale/ar'
|
||||
import 'moment/locale/de'
|
||||
|
@ -9,12 +8,13 @@ import 'moment/locale/fi'
|
|||
import 'moment/locale/fr'
|
||||
import 'moment/locale/it'
|
||||
import 'moment/locale/ja'
|
||||
import 'moment/locale/uz-latn'
|
||||
import 'moment/locale/nb'
|
||||
import 'moment/locale/nl'
|
||||
import 'moment/locale/pl'
|
||||
import 'moment/locale/ru'
|
||||
import 'moment/locale/sv'
|
||||
import 'moment/locale/uz-latn'
|
||||
import { I18nManager } from 'react-native'
|
||||
|
||||
const changeListeners: Record<string, any> = {}
|
||||
|
||||
|
@ -70,7 +70,12 @@ export const LanguageService = {
|
|||
},
|
||||
|
||||
onChange: ({ key }: { key: string }, cb: (langCode: string) => void) => {
|
||||
const unsubscribe = () => {
|
||||
delete changeListeners[key]
|
||||
}
|
||||
changeListeners[key] = (langCode: string) => cb(langCode)
|
||||
|
||||
return unsubscribe
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { systemWeights } from 'react-native-typography'
|
|||
type FontSize = 'xxs' | 'xs' | 'sm' | 'base' | 'lg' | 'xl'
|
||||
export const fontSize: Record<FontSize, TextStyle> = {
|
||||
xxs: {
|
||||
fontSize: 8,
|
||||
fontSize: 10,
|
||||
},
|
||||
xs: {
|
||||
fontSize: 12,
|
||||
|
|
|
@ -95,6 +95,20 @@
|
|||
"notifications": "Notifications",
|
||||
"classmates": "Classmates"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings",
|
||||
"appearance": "Appearance",
|
||||
"theme": "Theme",
|
||||
"licenses": "Licenses",
|
||||
"language": "Language",
|
||||
"themeAuto": "Auto",
|
||||
"useSystemTheme": "Use System Light/Dark Theme"
|
||||
|
||||
},
|
||||
"themes": {
|
||||
"light": "Light",
|
||||
"dark": "Dark"
|
||||
},
|
||||
"news": {
|
||||
"backToChild": "Back to child",
|
||||
"noNewNewsItemsThisWeek": "No news this week.",
|
||||
|
|
|
@ -95,6 +95,19 @@
|
|||
"notifications": "Aviseringar",
|
||||
"classmates": "Klassen"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Inställningar",
|
||||
"appearance": "Utseende",
|
||||
"theme": "Tema",
|
||||
"licenses": "Licenser",
|
||||
"language": "Språk",
|
||||
"themeAuto": "Auto",
|
||||
"useSystemTheme": "Använd telefonens inställning"
|
||||
},
|
||||
"themes": {
|
||||
"light": "Ljust",
|
||||
"dark": "Mörkt"
|
||||
},
|
||||
"news": {
|
||||
"backToChild": "Tillbaka till barn",
|
||||
"noNewNewsItemsThisWeek": "Inga nya inlägg denna vecka.",
|
||||
|
|
|
@ -1 +1,36 @@
|
|||
declare module 'rn-actionsheet-module'
|
||||
declare module 'libraries.json' {
|
||||
export interface Library {
|
||||
libraryName: string
|
||||
version: string
|
||||
_license?: License | string
|
||||
_description?: string
|
||||
homepage?: string
|
||||
author?: Author | string
|
||||
repository?: Repository
|
||||
_licenseContent?: string
|
||||
}
|
||||
|
||||
export interface License {
|
||||
type: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export interface Author {
|
||||
name: string
|
||||
url?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
type?: string
|
||||
url: string
|
||||
directory?: string
|
||||
baseUrl?: string
|
||||
web?: string
|
||||
dist?: string
|
||||
}
|
||||
|
||||
const libraries: Library[]
|
||||
|
||||
export default libraries
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue