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
|
# CocoaPods
|
||||||
/ios/Pods/
|
/ios/Pods/
|
||||||
|
|
||||||
|
libraries.json
|
|
@ -6,13 +6,13 @@ import init from '@skolplattformen/embedded-api'
|
||||||
import { ApplicationProvider, IconRegistry } from '@ui-kitten/components'
|
import { ApplicationProvider, IconRegistry } from '@ui-kitten/components'
|
||||||
import { EvaIconsPack } from '@ui-kitten/eva-icons'
|
import { EvaIconsPack } from '@ui-kitten/eva-icons'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StatusBar } from 'react-native'
|
import { StatusBar, useColorScheme } from 'react-native'
|
||||||
import { AppearanceProvider, useColorScheme } from 'react-native-appearance'
|
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
||||||
import { AppNavigator } from './components/navigation.component'
|
import { AppNavigator } from './components/navigation.component'
|
||||||
import { LanguageProvider } from './context/language/languageContext'
|
import { LanguageProvider } from './context/language/languageContext'
|
||||||
import { default as customMapping } from './design/mapping.json'
|
import { default as customMapping } from './design/mapping.json'
|
||||||
import { darkTheme, lightTheme } from './design/themes'
|
import { darkTheme, lightTheme } from './design/themes'
|
||||||
|
import useSettingsStorage from './hooks/useSettingsStorage'
|
||||||
import { translations } from './utils/translation'
|
import { translations } from './utils/translation'
|
||||||
const api = init(fetch, CookieManager)
|
const api = init(fetch, CookieManager)
|
||||||
|
|
||||||
|
@ -57,28 +57,30 @@ const logAsyncStorage = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const colorScheme = useColorScheme()
|
const [usingSystemTheme] = useSettingsStorage('usingSystemTheme')
|
||||||
|
const [theme] = useSettingsStorage('theme')
|
||||||
|
const systemTheme = useColorScheme()
|
||||||
|
|
||||||
|
const colorScheme = usingSystemTheme ? systemTheme : theme
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApiProvider api={api} storage={AsyncStorage} reporter={reporter}>
|
<ApiProvider api={api} storage={AsyncStorage} reporter={reporter}>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<AppearanceProvider>
|
<StatusBar
|
||||||
<StatusBar
|
backgroundColor={colorScheme === 'dark' ? '#2E3137' : '#FFF'}
|
||||||
backgroundColor={colorScheme === 'dark' ? '#2E3137' : '#FFF'}
|
barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'}
|
||||||
barStyle={colorScheme === 'dark' ? 'light-content' : 'dark-content'}
|
translucent
|
||||||
translucent
|
/>
|
||||||
/>
|
<IconRegistry icons={EvaIconsPack} />
|
||||||
<IconRegistry icons={EvaIconsPack} />
|
<ApplicationProvider
|
||||||
<ApplicationProvider
|
{...eva}
|
||||||
{...eva}
|
customMapping={customMapping}
|
||||||
customMapping={customMapping}
|
theme={colorScheme === 'dark' ? darkTheme : lightTheme}
|
||||||
theme={colorScheme === 'dark' ? darkTheme : lightTheme}
|
>
|
||||||
>
|
<LanguageProvider cache={true} data={translations}>
|
||||||
<LanguageProvider cache={true} data={translations}>
|
<AppNavigator />
|
||||||
<AppNavigator />
|
</LanguageProvider>
|
||||||
</LanguageProvider>
|
</ApplicationProvider>
|
||||||
</ApplicationProvider>
|
|
||||||
</AppearanceProvider>
|
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export * from 'react-native-appearance/src/mock'
|
|
|
@ -76,9 +76,9 @@ import com.android.build.OutputFile
|
||||||
* extraPackagerArgs: []
|
* extraPackagerArgs: []
|
||||||
* ]
|
* ]
|
||||||
*/
|
*/
|
||||||
// Crashes the app in RN 0.65.1
|
|
||||||
project.ext.react = [
|
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"
|
apply from: "../../node_modules/react-native/react.gradle"
|
||||||
|
|
|
@ -10,23 +10,24 @@ import {
|
||||||
Image,
|
Image,
|
||||||
ImageStyle,
|
ImageStyle,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
|
TouchableOpacity,
|
||||||
TouchableWithoutFeedback,
|
TouchableWithoutFeedback,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
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 { Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
||||||
import { fontSize } from '../styles/typography'
|
import { fontSize } from '../styles/typography'
|
||||||
import { KeyboardAvoidingView } from '../ui/keyboardAvoidingView.component'
|
import { KeyboardAvoidingView } from '../ui/keyboardAvoidingView.component'
|
||||||
import { SafeAreaView } from '../ui/safeAreaView.component'
|
import { SafeAreaView } from '../ui/safeAreaView.component'
|
||||||
import { SafeAreaViewContainer } from '../ui/safeAreaViewContainer.component'
|
import { SettingsIcon } from './icon.component'
|
||||||
import { languages, translate } from '../utils/translation'
|
|
||||||
import { GlobeIcon } from './icon.component'
|
|
||||||
import { Login } from './login.component'
|
import { Login } from './login.component'
|
||||||
import { RootStackParamList } from './navigation.component'
|
import { RootStackParamList } from './navigation.component'
|
||||||
|
|
||||||
const randomWord = () => {
|
const randomWord = (
|
||||||
const words = translate('auth.words')
|
t: (scope: I18n.Scope, options?: I18n.TranslateOptions | undefined) => string
|
||||||
|
) => {
|
||||||
|
const words = t('auth.words')
|
||||||
const keys = Object.keys(words)
|
const keys = Object.keys(words)
|
||||||
|
|
||||||
const randomIndex: number = Math.floor(Math.random() * keys.length)
|
const randomIndex: number = Math.floor(Math.random() * keys.length)
|
||||||
|
@ -51,73 +52,64 @@ export const authRouteOptions = (): NativeStackNavigationOptions => {
|
||||||
export const Auth: React.FC<AuthProps> = ({ navigation }) => {
|
export const Auth: React.FC<AuthProps> = ({ navigation }) => {
|
||||||
const styles = useStyleSheet(themeStyles)
|
const styles = useStyleSheet(themeStyles)
|
||||||
const colors = useTheme()
|
const colors = useTheme()
|
||||||
|
const { t } = useTranslation()
|
||||||
const currentLanguage = LanguageService.getLanguageCode()
|
|
||||||
const currentLanguageName = languages.find(
|
|
||||||
(language) => language.langCode === currentLanguage
|
|
||||||
)?.languageLocalName
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
<SafeAreaViewContainer>
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
||||||
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
<View style={LayoutStyle.flex.full}>
|
||||||
<View style={LayoutStyle.flex.full}>
|
<TouchableOpacity
|
||||||
<TouchableWithoutFeedback
|
style={styles.settingsLink}
|
||||||
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
onPress={() => navigation.navigate('Settings')}
|
||||||
onPress={() => navigation.navigate('SetLanguage')}
|
accessibilityHint={t('auth.a11y_navigate_to_settings', {
|
||||||
accessibilityHint={translate(
|
defaultValue: 'Navigerar till vyn för inställningar',
|
||||||
'auth.a11y_navigate_to_change_language',
|
})}
|
||||||
{
|
accessibilityLabel={t('auth.a11y_settings', {
|
||||||
defaultValue: 'Navigerar till vyn för att byta språk',
|
defaultValue: 'Inställningar',
|
||||||
}
|
})}
|
||||||
)}
|
>
|
||||||
accessibilityLabel={translate('auth.a11y_change_language', {
|
<View style={styles.language}>
|
||||||
defaultValue: 'Byt språk',
|
<SettingsIcon
|
||||||
})}
|
height={28}
|
||||||
>
|
width={28}
|
||||||
<View style={styles.language}>
|
fill={colors['color-primary-500']}
|
||||||
<GlobeIcon
|
/>
|
||||||
height={24}
|
<Text style={styles.languageText}>{t('general.settings')}</Text>
|
||||||
width={24}
|
</View>
|
||||||
fill={colors['color-primary-500']}
|
</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>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
<View style={styles.container}>
|
||||||
<KeyboardAvoidingView>
|
<Text
|
||||||
<View style={styles.content}>
|
category="h1"
|
||||||
<View style={styles.imageWrapper}>
|
style={styles.header}
|
||||||
<Image
|
adjustsFontSizeToFit
|
||||||
source={require('../assets/boys.png')}
|
numberOfLines={2}
|
||||||
style={styles.image as ImageStyle}
|
>
|
||||||
accessibilityHint={translate('login.a11y_image_two_boys', {
|
Öppna skolplattformen
|
||||||
defaultValue: 'Bild på två personer som kollar i mobilen',
|
</Text>
|
||||||
})}
|
<Login />
|
||||||
resizeMode="contain"
|
<Text category="c2" style={styles.subtitle}>
|
||||||
accessibilityIgnoresInvertColors={false}
|
{t('auth.subtitle', {
|
||||||
/>
|
word: randomWord(t),
|
||||||
</View>
|
})}
|
||||||
<View style={styles.container}>
|
</Text>
|
||||||
<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>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</View>
|
||||||
</View>
|
</KeyboardAvoidingView>
|
||||||
</TouchableWithoutFeedback>
|
</View>
|
||||||
</SafeAreaViewContainer>
|
</TouchableWithoutFeedback>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -153,10 +145,15 @@ const themeStyles = StyleService.create({
|
||||||
language: {
|
language: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingLeft: Sizing.t4,
|
padding: Sizing.t3,
|
||||||
|
paddingLeft: Sizing.t5,
|
||||||
},
|
},
|
||||||
languageText: {
|
languageText: {
|
||||||
...fontSize.xs,
|
...fontSize.sm,
|
||||||
marginLeft: Sizing.t1,
|
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 { TouchableOpacity, useColorScheme, View } from 'react-native'
|
||||||
import { Colors, Layout, Sizing } from '../styles'
|
import { Colors, Layout, Sizing } from '../styles'
|
||||||
import { studentName } from '../utils/peopleHelpers'
|
import { studentName } from '../utils/peopleHelpers'
|
||||||
import { translate } from '../utils/translation'
|
import { useTranslation } from '../hooks/useTranslation'
|
||||||
import { DaySummary } from './daySummary.component'
|
import { DaySummary } from './daySummary.component'
|
||||||
import { AlertIcon, RightArrowIcon } from './icon.component'
|
import { AlertIcon, RightArrowIcon } from './icon.component'
|
||||||
import { RootStackParamList } from './navigation.component'
|
import { RootStackParamList } from './navigation.component'
|
||||||
|
@ -41,6 +41,7 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
||||||
React.useEffect(() => {}, [child.id])
|
React.useEffect(() => {}, [child.id])
|
||||||
|
|
||||||
const navigation = useNavigation<ChildListItemNavigationProp>()
|
const navigation = useNavigation<ChildListItemNavigationProp>()
|
||||||
|
const { t } = useTranslation()
|
||||||
const { data: notifications } = useNotifications(child)
|
const { data: notifications } = useNotifications(child)
|
||||||
const { data: news } = useNews(child)
|
const { data: news } = useNews(child)
|
||||||
const { data: classmates } = useClassmates(child)
|
const { data: classmates } = useClassmates(child)
|
||||||
|
@ -91,10 +92,10 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
||||||
// Taken from Skolverket
|
// Taken from Skolverket
|
||||||
// https://www.skolverket.se/skolutveckling/anordna-och-administrera-utbildning/administrera-utbildning/skoltermer-pa-engelska
|
// https://www.skolverket.se/skolutveckling/anordna-och-administrera-utbildning/administrera-utbildning/skoltermer-pa-engelska
|
||||||
const abbrevations = {
|
const abbrevations = {
|
||||||
G: translate('abbrevations.upperSecondarySchool'),
|
G: t('abbrevations.upperSecondarySchool'),
|
||||||
GR: translate('abbrevations.compulsorySchool'),
|
GR: t('abbrevations.compulsorySchool'),
|
||||||
F: translate('abbrevations.leisureTimeCentre'),
|
F: t('abbrevations.leisureTimeCentre'),
|
||||||
FS: translate('abbrevations.preSchool'),
|
FS: t('abbrevations.preSchool'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return child.status
|
return child.status
|
||||||
|
@ -143,7 +144,7 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
<Text category="c2" style={styles.label}>
|
<Text category="c2" style={styles.label}>
|
||||||
{translate('navigation.news')}
|
{t('navigation.news')}
|
||||||
</Text>
|
</Text>
|
||||||
{notificationsThisWeek.slice(0, 3).map((notification, i) => (
|
{notificationsThisWeek.slice(0, 3).map((notification, i) => (
|
||||||
<Text category="p1" key={i}>
|
<Text category="p1" key={i}>
|
||||||
|
@ -159,14 +160,14 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
||||||
notificationsThisWeek.length ||
|
notificationsThisWeek.length ||
|
||||||
newsThisWeek.length ? null : (
|
newsThisWeek.length ? null : (
|
||||||
<Text category="p1" style={styles.noNewNewsItemsText}>
|
<Text category="p1" style={styles.noNewNewsItemsText}>
|
||||||
{translate('news.noNewNewsItemsThisWeek')}
|
{t('news.noNewNewsItemsThisWeek')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!menu[moment().isoWeekday() - 1] ? null : (
|
{!menu[moment().isoWeekday() - 1] ? null : (
|
||||||
<>
|
<>
|
||||||
<Text category="c2" style={styles.label}>
|
<Text category="c2" style={styles.label}>
|
||||||
{translate('schedule.lunch')}
|
{t('schedule.lunch')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
|
<Text>{menu[moment().isoWeekday() - 1]?.description}</Text>
|
||||||
</>
|
</>
|
||||||
|
@ -175,14 +176,14 @@ export const ChildListItem = ({ child, color }: ChildListItemProps) => {
|
||||||
<Button
|
<Button
|
||||||
accessible
|
accessible
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={`${child.name}, ${translate('abscense.title')}`}
|
accessibilityLabel={`${child.name}, ${t('abscense.title')}`}
|
||||||
appearance="ghost"
|
appearance="ghost"
|
||||||
accessoryLeft={AlertIcon}
|
accessoryLeft={AlertIcon}
|
||||||
status="primary"
|
status="primary"
|
||||||
style={styles.absenceButton}
|
style={styles.absenceButton}
|
||||||
onPress={() => navigation.navigate('Absence', { child })}
|
onPress={() => navigation.navigate('Absence', { child })}
|
||||||
>
|
>
|
||||||
{translate('abscense.title')}
|
{t('abscense.title')}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import AppStorage from '../services/appStorage'
|
|
||||||
import { useNavigation } from '@react-navigation/core'
|
import { useNavigation } from '@react-navigation/core'
|
||||||
import { useApi, useChildList } from '@skolplattformen/api-hooks'
|
import { useApi, useChildList } from '@skolplattformen/api-hooks'
|
||||||
import { Child } from '@skolplattformen/embedded-api'
|
import { Child } from '@skolplattformen/embedded-api'
|
||||||
|
@ -11,7 +10,7 @@ import {
|
||||||
TopNavigationAction,
|
TopNavigationAction,
|
||||||
useStyleSheet,
|
useStyleSheet,
|
||||||
} from '@ui-kitten/components'
|
} from '@ui-kitten/components'
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react'
|
import React, { useCallback, useEffect } from 'react'
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
ImageStyle,
|
ImageStyle,
|
||||||
|
@ -20,12 +19,12 @@ import {
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
import { NativeStackNavigationOptions } from 'react-native-screens/native-stack'
|
||||||
import ActionSheet from 'rn-actionsheet-module'
|
|
||||||
import { defaultStackStyling } from '../design/navigationThemes'
|
import { defaultStackStyling } from '../design/navigationThemes'
|
||||||
|
import AppStorage from '../services/appStorage'
|
||||||
import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
import { Colors, Layout as LayoutStyle, Sizing, Typography } from '../styles'
|
||||||
import { translate } from '../utils/translation'
|
import { translate } from '../utils/translation'
|
||||||
import { ChildListItem } from './childListItem.component'
|
import { ChildListItem } from './childListItem.component'
|
||||||
import { CloseOutlineIcon } from './icon.component'
|
import { SettingsIcon } from './icon.component'
|
||||||
|
|
||||||
const colors = ['primary', 'success', 'info', 'warning', 'danger']
|
const colors = ['primary', 'success', 'info', 'warning', 'danger']
|
||||||
|
|
||||||
|
@ -54,56 +53,18 @@ export const Children = () => {
|
||||||
AppStorage.clearTemporaryItems().then(() => api.logout())
|
AppStorage.clearTemporaryItems().then(() => api.logout())
|
||||||
}, [api])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
navigation.setOptions({
|
navigation.setOptions({
|
||||||
headerLeft: () => {
|
headerLeft: () => {
|
||||||
return (
|
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
|
// 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.
|
// 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 AlertIcon = uiIcon('alert-circle-outline')
|
||||||
export const BackIcon = uiIcon('arrow-back')
|
export const BackIcon = uiIcon('arrow-back')
|
||||||
|
export const BrushIcon = uiIcon('brush')
|
||||||
export const CalendarOutlineIcon = uiIcon('calendar-outline')
|
export const CalendarOutlineIcon = uiIcon('calendar-outline')
|
||||||
export const CallIcon = uiIcon('phone-outline')
|
export const CallIcon = uiIcon('phone-outline')
|
||||||
export const CheckIcon = uiIcon('checkmark-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 ExternalLinkIcon = uiIcon('external-link-outline')
|
||||||
export const ClipboardIcon = uiIcon('clipboard-outline')
|
export const ClipboardIcon = uiIcon('clipboard-outline')
|
||||||
export const RightArrowIcon = uiIcon('arrow-ios-forward-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,
|
TouchableWithoutFeedback,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
|
||||||
import AppStorage from '../services/appStorage'
|
|
||||||
import { schema } from '../app.json'
|
import { schema } from '../app.json'
|
||||||
|
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||||
|
import { useTranslation } from '../hooks/useTranslation'
|
||||||
import { Layout } from '../styles'
|
import { Layout } from '../styles'
|
||||||
import { translate } from '../utils/translation'
|
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
CloseOutlineIcon,
|
CloseOutlineIcon,
|
||||||
|
@ -49,63 +48,21 @@ export const Login = () => {
|
||||||
const [visible, showModal] = useState(false)
|
const [visible, showModal] = useState(false)
|
||||||
const [showLoginMethod, setShowLoginMethod] = useState(false)
|
const [showLoginMethod, setShowLoginMethod] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [personalIdNumber, setPersonalIdNumber] = useState('')
|
const [personalIdNumber, setPersonalIdNumber] = useSettingsStorage(
|
||||||
const [valid, setValid] = useState(false)
|
'cachedPersonalIdentityNumber'
|
||||||
const [loginMethodIndex, setLoginMethodIndex] = useState(0)
|
)
|
||||||
const [cachedLoginMethodIndex, setCachedLoginMethodIndex] =
|
const [loginMethodIndex, setLoginMethodIndex] =
|
||||||
useSettingsStorage('loginMethodIndex', '0')
|
useSettingsStorage('loginMethodIndex')
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const valid = Personnummer.valid(personalIdNumber)
|
||||||
|
|
||||||
const loginMethods = [
|
const loginMethods = [
|
||||||
translate('auth.bankid.OpenOnThisDevice'),
|
t('auth.bankid.OpenOnThisDevice'),
|
||||||
translate('auth.bankid.OpenOnAnotherDevice'),
|
t('auth.bankid.OpenOnAnotherDevice'),
|
||||||
translate('auth.loginAsTestUser'),
|
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 () => {
|
const loginHandler = async () => {
|
||||||
showModal(false)
|
showModal(false)
|
||||||
}
|
}
|
||||||
|
@ -119,7 +76,6 @@ export const Login = () => {
|
||||||
|
|
||||||
/* Helpers */
|
/* Helpers */
|
||||||
const handleInput = (text: string) => {
|
const handleInput = (text: string) => {
|
||||||
setValid(Personnummer.valid(text))
|
|
||||||
setPersonalIdNumber(text)
|
setPersonalIdNumber(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +88,7 @@ export const Login = () => {
|
||||||
: `bankid:///?autostarttoken=${token}&redirect=null`
|
: `bankid:///?autostarttoken=${token}&redirect=null`
|
||||||
Linking.openURL(bankIdUrl)
|
Linking.openURL(bankIdUrl)
|
||||||
} catch (err) {
|
} 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('PENDING', () => console.log('BankID app not yet opened'))
|
||||||
status.on('USER_SIGN', () => console.log('BankID app is open'))
|
status.on('USER_SIGN', () => console.log('BankID app is open'))
|
||||||
status.on('ERROR', () => {
|
status.on('ERROR', () => {
|
||||||
setError(translate('auth.loginFailed'))
|
setError(t('auth.loginFailed'))
|
||||||
showModal(false)
|
showModal(false)
|
||||||
})
|
})
|
||||||
status.on('OK', () => console.log('BankID ok'))
|
status.on('OK', () => console.log('BankID ok'))
|
||||||
|
@ -171,7 +127,7 @@ export const Login = () => {
|
||||||
{loginMethodIndex === 1 && (
|
{loginMethodIndex === 1 && (
|
||||||
<Input
|
<Input
|
||||||
accessible={true}
|
accessible={true}
|
||||||
label={translate('general.socialSecurityNumber')}
|
label={t('general.socialSecurityNumber')}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={personalIdNumber}
|
value={personalIdNumber}
|
||||||
style={styles.pnrInput}
|
style={styles.pnrInput}
|
||||||
|
@ -180,7 +136,7 @@ export const Login = () => {
|
||||||
<TouchableWithoutFeedback
|
<TouchableWithoutFeedback
|
||||||
accessible={true}
|
accessible={true}
|
||||||
onPress={() => handleInput('')}
|
onPress={() => handleInput('')}
|
||||||
accessibilityHint={translate(
|
accessibilityHint={t(
|
||||||
'login.a11y_clear_social_security_input_field',
|
'login.a11y_clear_social_security_input_field',
|
||||||
{
|
{
|
||||||
defaultValue: 'Rensa fältet för personnummer',
|
defaultValue: 'Rensa fältet för personnummer',
|
||||||
|
@ -194,7 +150,7 @@ export const Login = () => {
|
||||||
onSubmitEditing={(event) => startLogin(event.nativeEvent.text)}
|
onSubmitEditing={(event) => startLogin(event.nativeEvent.text)}
|
||||||
caption={error || ''}
|
caption={error || ''}
|
||||||
onChangeText={(text) => handleInput(text)}
|
onChangeText={(text) => handleInput(text)}
|
||||||
placeholder={translate('auth.placeholder_SocialSecurityNumber')}
|
placeholder={t('auth.placeholder_SocialSecurityNumber')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ButtonGroup style={styles.loginButtonGroup} status="primary">
|
<ButtonGroup style={styles.loginButtonGroup} status="primary">
|
||||||
|
@ -220,7 +176,7 @@ export const Login = () => {
|
||||||
status="primary"
|
status="primary"
|
||||||
accessoryLeft={SelectIcon}
|
accessoryLeft={SelectIcon}
|
||||||
size="medium"
|
size="medium"
|
||||||
accessibilityHint={translate('login.a11y_select_login_method', {
|
accessibilityHint={t('login.a11y_select_login_method', {
|
||||||
defaultValue: 'Välj inloggningsmetod',
|
defaultValue: 'Välj inloggningsmetod',
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -234,7 +190,7 @@ export const Login = () => {
|
||||||
>
|
>
|
||||||
<Card>
|
<Card>
|
||||||
<Text category="h5" style={styles.bankIdLoading}>
|
<Text category="h5" style={styles.bankIdLoading}>
|
||||||
{translate('auth.chooseLoginMethod')}
|
{t('auth.chooseLoginMethod')}
|
||||||
</Text>
|
</Text>
|
||||||
<List
|
<List
|
||||||
data={loginMethods}
|
data={loginMethods}
|
||||||
|
@ -260,7 +216,7 @@ export const Login = () => {
|
||||||
setShowLoginMethod(false)
|
setShowLoginMethod(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{translate('general.cancel')}
|
{t('general.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -271,9 +227,7 @@ export const Login = () => {
|
||||||
backdropStyle={styles.backdrop}
|
backdropStyle={styles.backdrop}
|
||||||
>
|
>
|
||||||
<Card disabled>
|
<Card disabled>
|
||||||
<Text style={styles.bankIdLoading}>
|
<Text style={styles.bankIdLoading}>{t('auth.bankid.Waiting')}</Text>
|
||||||
{translate('auth.bankid.Waiting')}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
status="primary"
|
status="primary"
|
||||||
|
@ -283,7 +237,7 @@ export const Login = () => {
|
||||||
showModal(false)
|
showModal(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{translate('general.cancel')}
|
{t('general.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
NewsItem as NewsItemType,
|
NewsItem as NewsItemType,
|
||||||
} from '@skolplattformen/embedded-api'
|
} from '@skolplattformen/embedded-api'
|
||||||
import { useTheme } from '@ui-kitten/components'
|
import { useTheme } from '@ui-kitten/components'
|
||||||
|
import { Library } from 'libraries.json'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { StatusBar, useColorScheme } from 'react-native'
|
import { StatusBar, useColorScheme } from 'react-native'
|
||||||
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
import { createNativeStackNavigator } from 'react-native-screens/native-stack'
|
||||||
|
@ -14,16 +15,42 @@ import {
|
||||||
lightNavigationTheme,
|
lightNavigationTheme,
|
||||||
} from '../design/navigationThemes'
|
} from '../design/navigationThemes'
|
||||||
import { useAppState } from '../hooks/useAppState'
|
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 Absence, { absenceRouteOptions } from './absence.component'
|
||||||
import { Auth, authRouteOptions } from './auth.component'
|
import { Auth, authRouteOptions } from './auth.component'
|
||||||
import { Child, childRouteOptions } from './child.component'
|
import { Child, childRouteOptions } from './child.component'
|
||||||
import { childenRouteOptions, Children } from './children.component'
|
import { childenRouteOptions, Children } from './children.component'
|
||||||
|
import { libraryRouteOptions, LibraryScreen } from './library.component'
|
||||||
import { NewsItem, newsItemRouteOptions } from './newsItem.component'
|
import { NewsItem, newsItemRouteOptions } from './newsItem.component'
|
||||||
import { SetLanguage, setLanguageRouteOptions } from './setLanguage.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 = {
|
export type RootStackParamList = {
|
||||||
Login: undefined
|
Login: undefined
|
||||||
Children: undefined
|
Children: undefined
|
||||||
|
Settings: undefined
|
||||||
|
SettingsAppearance: undefined
|
||||||
|
SettingsAppearanceTheme: undefined
|
||||||
|
SettingsLicenses: undefined
|
||||||
|
Library: {
|
||||||
|
library: Library
|
||||||
|
}
|
||||||
Child: {
|
Child: {
|
||||||
child: ChildType
|
child: ChildType
|
||||||
color: string
|
color: string
|
||||||
|
@ -48,11 +75,20 @@ const linking = {
|
||||||
export const AppNavigator = () => {
|
export const AppNavigator = () => {
|
||||||
const { isLoggedIn, api } = useApi()
|
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 colors = useTheme()
|
||||||
|
|
||||||
const currentAppState = useAppState()
|
const currentAppState = useAppState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeSettingsState()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkUser = async () => {
|
const checkUser = async () => {
|
||||||
if (currentAppState === 'active' && isLoggedIn) {
|
if (currentAppState === 'active' && isLoggedIn) {
|
||||||
|
@ -76,9 +112,16 @@ export const AppNavigator = () => {
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<Navigator
|
<Navigator
|
||||||
screenOptions={() => ({
|
screenOptions={() => ({
|
||||||
headerLargeTitle: false,
|
headerLargeTitle: true,
|
||||||
headerLargeTitleHideShadow: true,
|
headerLargeTitleHideShadow: true,
|
||||||
|
direction: isRTL(langCode) ? 'rtl' : 'ltr',
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
|
backgroundColor:
|
||||||
|
colorScheme === 'dark'
|
||||||
|
? colors['background-basic-color-2']
|
||||||
|
: colors['background-basic-color-1'],
|
||||||
|
},
|
||||||
|
headerLargeStyle: {
|
||||||
backgroundColor: colors['background-basic-color-2'],
|
backgroundColor: colors['background-basic-color-2'],
|
||||||
},
|
},
|
||||||
headerLargeTitleStyle: {
|
headerLargeTitleStyle: {
|
||||||
|
@ -112,13 +155,38 @@ export const AppNavigator = () => {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Screen name="Login" component={Auth} options={authRouteOptions} />
|
<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>
|
</Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,12 +3,10 @@ import {
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
StyleService,
|
StyleService,
|
||||||
Text,
|
|
||||||
useStyleSheet,
|
useStyleSheet,
|
||||||
useTheme,
|
|
||||||
} from '@ui-kitten/components'
|
} from '@ui-kitten/components'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { View, TouchableOpacity } from 'react-native'
|
import { View } from 'react-native'
|
||||||
import { ScrollView } from 'react-native-gesture-handler'
|
import { ScrollView } from 'react-native-gesture-handler'
|
||||||
import RNRestart from 'react-native-restart'
|
import RNRestart from 'react-native-restart'
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
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 { useLanguage } from '../hooks/useLanguage'
|
||||||
import { isRTL, LanguageService } from '../services/languageService'
|
import { isRTL, LanguageService } from '../services/languageService'
|
||||||
import { Layout as LayoutStyle, Sizing } from '../styles'
|
import { Layout as LayoutStyle, Sizing } from '../styles'
|
||||||
import { fontSize } from '../styles/typography'
|
|
||||||
import { languages, translate } from '../utils/translation'
|
import { languages, translate } from '../utils/translation'
|
||||||
import { CheckIcon } from './icon.component'
|
import {
|
||||||
|
SettingGroup,
|
||||||
|
SettingListItemSelectable,
|
||||||
|
} from './settingsComponents.component'
|
||||||
|
|
||||||
export const setLanguageRouteOptions = (): NativeStackNavigationOptions => ({
|
export const setLanguageRouteOptions = (): NativeStackNavigationOptions => ({
|
||||||
title: translate('language.changeLanguage'),
|
title: translate('language.changeLanguage'),
|
||||||
|
@ -27,7 +27,6 @@ export const setLanguageRouteOptions = (): NativeStackNavigationOptions => ({
|
||||||
export const SetLanguage = () => {
|
export const SetLanguage = () => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const styles = useStyleSheet(themedStyles)
|
const styles = useStyleSheet(themedStyles)
|
||||||
const colors = useTheme()
|
|
||||||
|
|
||||||
const currentLanguage = LanguageService.getLanguageCode()
|
const currentLanguage = LanguageService.getLanguageCode()
|
||||||
|
|
||||||
|
@ -56,41 +55,27 @@ export const SetLanguage = () => {
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
// Need to reset the view so it updates the language
|
// 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)
|
const activeLanguages = languages.filter((language) => language.active)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['bottom']}>
|
<SafeAreaView style={styles.container} edges={['bottom']}>
|
||||||
<ScrollView>
|
<ScrollView contentContainerStyle={styles.scrollView}>
|
||||||
<View style={styles.content}>
|
<SettingGroup>
|
||||||
<View style={styles.languageList}>
|
<View style={styles.languageList}>
|
||||||
{activeLanguages.map((language) => (
|
{activeLanguages.map((language) => (
|
||||||
<TouchableOpacity
|
<SettingListItemSelectable
|
||||||
key={language.langCode}
|
key={language.langCode}
|
||||||
style={styles.languageButton}
|
|
||||||
onPress={() => setSelectedLanguage(language.langCode)}
|
onPress={() => setSelectedLanguage(language.langCode)}
|
||||||
>
|
title={language.languageLocalName}
|
||||||
<View>
|
subTitle={language.languageName}
|
||||||
<Text style={styles.languageButtonTitle}>
|
isSelected={isSelected(language.langCode)}
|
||||||
{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>
|
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</SettingGroup>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<ButtonGroup style={styles.buttonGroup}>
|
<ButtonGroup style={styles.buttonGroup}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -114,6 +99,7 @@ const themedStyles = StyleService.create({
|
||||||
alignSelf: 'stretch',
|
alignSelf: 'stretch',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
|
paddingHorizontal: Sizing.t4,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
width: 30,
|
width: 30,
|
||||||
|
@ -121,32 +107,14 @@ const themedStyles = StyleService.create({
|
||||||
},
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: 'background-basic-color-2',
|
|
||||||
},
|
},
|
||||||
content: {
|
scrollView: {
|
||||||
...LayoutStyle.center,
|
padding: Sizing.t4,
|
||||||
...LayoutStyle.flex.full,
|
|
||||||
margin: Sizing.t5,
|
|
||||||
paddingBottom: Sizing.t5,
|
|
||||||
},
|
},
|
||||||
buttonGroup: {
|
buttonGroup: {
|
||||||
minHeight: 45,
|
minHeight: 45,
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
marginHorizontal: Sizing.t5,
|
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 },
|
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-400": "rgba(186, 50, 127, 0.32)",
|
||||||
"color-danger-transparent-500": "rgba(186, 50, 127, 0.4)",
|
"color-danger-transparent-500": "rgba(186, 50, 127, 0.4)",
|
||||||
"color-danger-transparent-600": "rgba(186, 50, 127, 0.48)",
|
"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",
|
"background-basic-color-2": "#030200",
|
||||||
"text-hint-color": "#B3BBCB",
|
"text-hint-color": "#B3BBCB",
|
||||||
"color-control-default": "#E5E7EB",
|
"color-control-default": "#E5E7EB",
|
||||||
|
@ -78,5 +78,6 @@
|
||||||
"color-input-border": "$color-basic-300",
|
"color-input-border": "$color-basic-300",
|
||||||
"color-tab-default": "$color-primary-50",
|
"color-tab-default": "$color-primary-50",
|
||||||
"color-tab-focused": "$color-primary-200",
|
"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-basic-text": "$color-primary-800",
|
||||||
"color-input-border": "$color-basic-800",
|
"color-input-border": "$color-basic-800",
|
||||||
"background-basic-color-1": "#fff",
|
"background-basic-color-1": "#fff",
|
||||||
"background-basic-color-2": "#f7f9fc",
|
"background-basic-color-2": "#F2F1F6",
|
||||||
"color-tab-default": "$color-basic-700",
|
"color-tab-default": "$color-basic-700",
|
||||||
"color-tab-focused": "$color-primary-500",
|
"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 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 AppStorage from '../../services/appStorage'
|
||||||
|
import useSettingsStorage, { settingsState } from '../useSettingsStorage'
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
AsyncStorage.clear()
|
AsyncStorage.clear()
|
||||||
|
// TODO: This is a bit ugly. Should probably fix that.
|
||||||
|
settingsState.settings.theme = 'light'
|
||||||
})
|
})
|
||||||
|
|
||||||
const prefix = AppStorage.settingsStorageKeyPrefix
|
const prefix = AppStorage.settingsStorageKeyPrefix
|
||||||
|
|
||||||
test('use key prefix on set', async () => {
|
test('use key prefix on set', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useSettingsStorage('key', '')
|
useSettingsStorage('theme')
|
||||||
)
|
)
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
const [, setValue] = result.current
|
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 () => {
|
test('update value', async () => {
|
||||||
const { result, waitForNextUpdate } = renderHook(() =>
|
const { result, waitForNextUpdate } = renderHook(() =>
|
||||||
useSettingsStorage('key', 'initialValue')
|
useSettingsStorage('theme')
|
||||||
)
|
)
|
||||||
|
|
||||||
const [initValue, setValue] = result.current
|
const [initValue, setValue] = result.current
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
setValue('update')
|
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 AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
export default function useAsyncStorage<T>(
|
export default function useAsyncStorage<T>(
|
||||||
storageKey: string,
|
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'
|
import AppStorage from '../services/appStorage'
|
||||||
|
|
||||||
export default function useSettingsStorage<T>(
|
export const settingsState = proxy({
|
||||||
storageKey: string,
|
hydrated: false,
|
||||||
defaultValue: T
|
settings: {
|
||||||
): [T, (val: T) => void] {
|
loginMethodIndex: 0,
|
||||||
const settingsKey = AppStorage.settingsStorageKeyPrefix + storageKey
|
usingSystemTheme: true,
|
||||||
return useAsyncStorage(settingsKey, defaultValue)
|
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-jsi (= 0.65.1)
|
||||||
- React-perflogger (= 0.65.1)
|
- React-perflogger (= 0.65.1)
|
||||||
- React-jsinspector (0.65.1)
|
- React-jsinspector (0.65.1)
|
||||||
- react-native-appearance (0.3.4):
|
|
||||||
- React
|
|
||||||
- react-native-cookies (5.0.1):
|
- react-native-cookies (5.0.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-restart (0.0.22):
|
- react-native-restart (0.0.22):
|
||||||
|
@ -372,6 +370,8 @@ PODS:
|
||||||
- React
|
- React
|
||||||
- RNDateTimePicker (3.4.3):
|
- RNDateTimePicker (3.4.3):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
- RNDeviceInfo (8.3.3):
|
||||||
|
- React-Core
|
||||||
- RNDevMenu (4.0.2):
|
- RNDevMenu (4.0.2):
|
||||||
- React-Core
|
- React-Core
|
||||||
- React-Core/DevSupport
|
- React-Core/DevSupport
|
||||||
|
@ -461,7 +461,6 @@ DEPENDENCIES:
|
||||||
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
|
||||||
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
|
||||||
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
|
- 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-cookies (from `../node_modules/@react-native-community/cookies`)"
|
||||||
- react-native-restart (from `../node_modules/react-native-restart`)
|
- react-native-restart (from `../node_modules/react-native-restart`)
|
||||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
- 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`)"
|
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||||
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
|
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
|
||||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||||
|
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||||
- RNDevMenu (from `../node_modules/react-native-dev-menu`)
|
- RNDevMenu (from `../node_modules/react-native-dev-menu`)
|
||||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||||
|
@ -544,8 +544,6 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
|
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
|
||||||
React-jsinspector:
|
React-jsinspector:
|
||||||
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
:path: "../node_modules/react-native/ReactCommon/jsinspector"
|
||||||
react-native-appearance:
|
|
||||||
:path: "../node_modules/react-native-appearance"
|
|
||||||
react-native-cookies:
|
react-native-cookies:
|
||||||
:path: "../node_modules/@react-native-community/cookies"
|
:path: "../node_modules/@react-native-community/cookies"
|
||||||
react-native-restart:
|
react-native-restart:
|
||||||
|
@ -588,6 +586,8 @@ EXTERNAL SOURCES:
|
||||||
:path: "../node_modules/@react-native-community/masked-view"
|
:path: "../node_modules/@react-native-community/masked-view"
|
||||||
RNDateTimePicker:
|
RNDateTimePicker:
|
||||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||||
|
RNDeviceInfo:
|
||||||
|
:path: "../node_modules/react-native-device-info"
|
||||||
RNDevMenu:
|
RNDevMenu:
|
||||||
:path: "../node_modules/react-native-dev-menu"
|
:path: "../node_modules/react-native-dev-menu"
|
||||||
RNGestureHandler:
|
RNGestureHandler:
|
||||||
|
@ -635,7 +635,6 @@ SPEC CHECKSUMS:
|
||||||
React-jsi: 12913c841713a15f64eabf5c9ad98592c0ec5940
|
React-jsi: 12913c841713a15f64eabf5c9ad98592c0ec5940
|
||||||
React-jsiexecutor: 43f2542aed3c26e42175b339f8d37fe3dd683765
|
React-jsiexecutor: 43f2542aed3c26e42175b339f8d37fe3dd683765
|
||||||
React-jsinspector: 41e58e5b8e3e0bf061fdf725b03f2144014a8fb0
|
React-jsinspector: 41e58e5b8e3e0bf061fdf725b03f2144014a8fb0
|
||||||
react-native-appearance: 0f0e5fc2fcef70e03d48c8fe6b00b9158c2ba8aa
|
|
||||||
react-native-cookies: ce50e42ace7cf0dd47769260ca5bbe8eee607e4e
|
react-native-cookies: ce50e42ace7cf0dd47769260ca5bbe8eee607e4e
|
||||||
react-native-restart: 733a51ad137f15b0f8dc34c4082e55af7da00979
|
react-native-restart: 733a51ad137f15b0f8dc34c4082e55af7da00979
|
||||||
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
react-native-safe-area-context: 584dc04881deb49474363f3be89e4ca0e854c057
|
||||||
|
@ -657,6 +656,7 @@ SPEC CHECKSUMS:
|
||||||
RNCAsyncStorage: 9b7605e899f9acb2fba33e87952c529731265453
|
RNCAsyncStorage: 9b7605e899f9acb2fba33e87952c529731265453
|
||||||
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
|
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
|
||||||
RNDateTimePicker: d943800c936fb01c352fcfb70439550d2cb57092
|
RNDateTimePicker: d943800c936fb01c352fcfb70439550d2cb57092
|
||||||
|
RNDeviceInfo: cc7de0772378f85d8f36ae439df20f05c590a651
|
||||||
RNDevMenu: fd325b5554b61fe7f48d9205a3877cf5ee88cd7c
|
RNDevMenu: fd325b5554b61fe7f48d9205a3877cf5ee88cd7c
|
||||||
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
|
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
|
||||||
RNLocalize: 7f1e5792b65a839af55a9552d05b3558b66d017e
|
RNLocalize: 7f1e5792b65a839af55a9552d05b3558b66d017e
|
||||||
|
|
|
@ -10,6 +10,6 @@ module.exports = {
|
||||||
],
|
],
|
||||||
testPathIgnorePatterns: ['__tests__/Classmates.test.js'],
|
testPathIgnorePatterns: ['__tests__/Classmates.test.js'],
|
||||||
transformIgnorePatterns: [
|
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",
|
"test:watch": "jest --watch",
|
||||||
"typecheck": "tsc --watch",
|
"typecheck": "tsc --watch",
|
||||||
"i18n": "sync-i18n --files '**/translations/*.json' --primary en --languages ar de pl so sv --space 2",
|
"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": {
|
"dependencies": {
|
||||||
"@eva-design/eva": "2.0.0",
|
"@eva-design/eva": "2.0.0",
|
||||||
|
@ -33,18 +35,16 @@
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"fast-fuzzy": "^1.10.8",
|
"fast-fuzzy": "^1.10.8",
|
||||||
"formik": "2.2.6",
|
"formik": "2.2.6",
|
||||||
"hermes-engine": "0.7.2",
|
"hermes-engine": "0.8.1",
|
||||||
"i18n-js": "^3.8.0",
|
"i18n-js": "^3.8.0",
|
||||||
"i18next-json-sync": "^2.3.1",
|
|
||||||
"jsuri": "1.3.1",
|
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"personnummer": "3.1.3",
|
"personnummer": "3.1.3",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-native": "0.65.1",
|
"react-native": "0.65.1",
|
||||||
"react-native-animatable": "^1.3.3",
|
"react-native-animatable": "^1.3.3",
|
||||||
"react-native-appearance": "^0.3.4",
|
|
||||||
"react-native-calendar-events": "2.2.0",
|
"react-native-calendar-events": "2.2.0",
|
||||||
"react-native-dev-menu": "^4.0.2",
|
"react-native-dev-menu": "^4.0.2",
|
||||||
|
"react-native-device-info": "^8.3.3",
|
||||||
"react-native-fix-image": "2.1.0",
|
"react-native-fix-image": "2.1.0",
|
||||||
"react-native-gesture-handler": "^1.10.3",
|
"react-native-gesture-handler": "^1.10.3",
|
||||||
"react-native-localize": "^2.0.2",
|
"react-native-localize": "^2.0.2",
|
||||||
|
@ -56,12 +56,9 @@
|
||||||
"react-native-screens": "^3.3.0",
|
"react-native-screens": "^3.3.0",
|
||||||
"react-native-simple-toast": "1.1.3",
|
"react-native-simple-toast": "1.1.3",
|
||||||
"react-native-svg": "12.1.0",
|
"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-typography": "1.4.1",
|
||||||
"react-native-webview": "11.4.2",
|
"react-native-webview": "11.4.2",
|
||||||
"react-native-weekly-calendar": "^0.2.0",
|
"valtio": "^1.2.3",
|
||||||
"rn-actionsheet-module": "https://github.com/kolplattformen/rn-actionsheet-module.git",
|
|
||||||
"yup": "0.32.9"
|
"yup": "0.32.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -92,6 +89,7 @@
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"react-native-clean-project": "^3.6.3",
|
"react-native-clean-project": "^3.6.3",
|
||||||
"react-native-codegen": "^0.0.7",
|
"react-native-codegen": "^0.0.7",
|
||||||
|
"react-native-oss-license": "^0.4.0",
|
||||||
"react-test-renderer": "17.0.2",
|
"react-test-renderer": "17.0.2",
|
||||||
"typescript": "^4.2.4"
|
"typescript": "^4.2.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { I18nManager } from 'react-native'
|
|
||||||
import i18n from 'i18n-js'
|
|
||||||
import merge from 'deepmerge'
|
import merge from 'deepmerge'
|
||||||
|
import i18n from 'i18n-js'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import 'moment/locale/ar'
|
import 'moment/locale/ar'
|
||||||
import 'moment/locale/de'
|
import 'moment/locale/de'
|
||||||
|
@ -9,12 +8,13 @@ import 'moment/locale/fi'
|
||||||
import 'moment/locale/fr'
|
import 'moment/locale/fr'
|
||||||
import 'moment/locale/it'
|
import 'moment/locale/it'
|
||||||
import 'moment/locale/ja'
|
import 'moment/locale/ja'
|
||||||
import 'moment/locale/uz-latn'
|
|
||||||
import 'moment/locale/nb'
|
import 'moment/locale/nb'
|
||||||
import 'moment/locale/nl'
|
import 'moment/locale/nl'
|
||||||
import 'moment/locale/pl'
|
import 'moment/locale/pl'
|
||||||
import 'moment/locale/ru'
|
import 'moment/locale/ru'
|
||||||
import 'moment/locale/sv'
|
import 'moment/locale/sv'
|
||||||
|
import 'moment/locale/uz-latn'
|
||||||
|
import { I18nManager } from 'react-native'
|
||||||
|
|
||||||
const changeListeners: Record<string, any> = {}
|
const changeListeners: Record<string, any> = {}
|
||||||
|
|
||||||
|
@ -70,7 +70,12 @@ export const LanguageService = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange: ({ key }: { key: string }, cb: (langCode: string) => void) => {
|
onChange: ({ key }: { key: string }, cb: (langCode: string) => void) => {
|
||||||
|
const unsubscribe = () => {
|
||||||
|
delete changeListeners[key]
|
||||||
|
}
|
||||||
changeListeners[key] = (langCode: string) => cb(langCode)
|
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'
|
type FontSize = 'xxs' | 'xs' | 'sm' | 'base' | 'lg' | 'xl'
|
||||||
export const fontSize: Record<FontSize, TextStyle> = {
|
export const fontSize: Record<FontSize, TextStyle> = {
|
||||||
xxs: {
|
xxs: {
|
||||||
fontSize: 8,
|
fontSize: 10,
|
||||||
},
|
},
|
||||||
xs: {
|
xs: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
|
@ -95,6 +95,20 @@
|
||||||
"notifications": "Notifications",
|
"notifications": "Notifications",
|
||||||
"classmates": "Classmates"
|
"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": {
|
"news": {
|
||||||
"backToChild": "Back to child",
|
"backToChild": "Back to child",
|
||||||
"noNewNewsItemsThisWeek": "No news this week.",
|
"noNewNewsItemsThisWeek": "No news this week.",
|
||||||
|
|
|
@ -95,6 +95,19 @@
|
||||||
"notifications": "Aviseringar",
|
"notifications": "Aviseringar",
|
||||||
"classmates": "Klassen"
|
"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": {
|
"news": {
|
||||||
"backToChild": "Tillbaka till barn",
|
"backToChild": "Tillbaka till barn",
|
||||||
"noNewNewsItemsThisWeek": "Inga nya inlägg denna vecka.",
|
"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