feat: 🎸 Use custom async storage (#461)
Service and hooks for storing temp-data, settings and personal data from the app Also: * Remove npm package use-async-storage * Changed naming to use personal identity number instead of social security number
This commit is contained in:
parent
4c6868fc90
commit
98f7c04f1c
|
@ -26,6 +26,36 @@ const reporter = __DEV__
|
|||
error: () => {},
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
const DevMenu = require('react-native-dev-menu')
|
||||
DevMenu.addItem('Log AsyncStorage contents', () => logAsyncStorage())
|
||||
}
|
||||
|
||||
const safeJsonParse = (maybeJson) => {
|
||||
if (maybeJson) {
|
||||
try {
|
||||
return JSON.parse(maybeJson)
|
||||
} catch (error) {
|
||||
return maybeJson
|
||||
}
|
||||
}
|
||||
return 'null'
|
||||
}
|
||||
|
||||
const logAsyncStorage = async () => {
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
const keysAndValues = await AsyncStorage.multiGet(allKeys)
|
||||
console.log('*** AsyncStorage contents:')
|
||||
keysAndValues.forEach((keyAndValue) => {
|
||||
console.log(
|
||||
keyAndValue[0],
|
||||
'=>',
|
||||
keyAndValue[1] ? safeJsonParse(keyAndValue[1]) : 'null'
|
||||
)
|
||||
})
|
||||
console.log('***')
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const colorScheme = useColorScheme()
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export {
|
||||
default,
|
||||
useAsyncStorage,
|
||||
} from '@react-native-async-storage/async-storage/jest/async-storage-mock'
|
|
@ -1 +0,0 @@
|
|||
export { default } from '@react-native-async-storage/async-storage/jest/async-storage-mock'
|
|
@ -1,4 +1,3 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { useRoute } from '@react-navigation/native'
|
||||
import { fireEvent, waitFor } from '@testing-library/react-native'
|
||||
import Mockdate from 'mockdate'
|
||||
|
@ -6,15 +5,15 @@ import React from 'react'
|
|||
import { useSMS } from '../../utils/SMS'
|
||||
import { render } from '../../utils/testHelpers'
|
||||
import Absence from '../absence.component'
|
||||
import { useUser } from '@skolplattformen/api-hooks'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
jest.mock('@react-navigation/native')
|
||||
jest.mock('@react-native-async-storage/async-storage')
|
||||
jest.mock('@skolplattformen/api-hooks')
|
||||
jest.mock('../../utils/SMS')
|
||||
|
||||
let sendSMS
|
||||
|
||||
// needed to skip tests due to bug in RN 0.65.1
|
||||
// https://github.com/facebook/react-native/issues/29849#issuecomment-734533635
|
||||
let user = { personalNumber: '201701092395' }
|
||||
|
||||
const setup = (customProps = {}) => {
|
||||
sendSMS = jest.fn()
|
||||
|
@ -35,18 +34,21 @@ beforeAll(() => {
|
|||
jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
AsyncStorage.clear()
|
||||
useUser.mockReturnValue({
|
||||
data: user,
|
||||
status: 'loaded',
|
||||
})
|
||||
await AsyncStorage.clear()
|
||||
})
|
||||
|
||||
test.skip('can fill out the form with full day absence', async () => {
|
||||
test('can fill out the form with full day absence', async () => {
|
||||
const screen = setup()
|
||||
|
||||
await waitFor(() =>
|
||||
fireEvent.changeText(
|
||||
screen.getByTestId('socialSecurityNumberInput'),
|
||||
screen.getByTestId('personalIdentityNumberInput'),
|
||||
'1212121212'
|
||||
)
|
||||
)
|
||||
|
@ -56,10 +58,9 @@ test.skip('can fill out the form with full day absence', async () => {
|
|||
expect(screen.queryByText(/sluttid/i)).toBeFalsy()
|
||||
|
||||
expect(sendSMS).toHaveBeenCalledWith('121212-1212')
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith('@childssn.1', '1212121212')
|
||||
})
|
||||
|
||||
test.skip('handles missing social security number', async () => {
|
||||
test('handles missing social security number', async () => {
|
||||
const screen = setup()
|
||||
|
||||
await waitFor(() => fireEvent.press(screen.getByText('Skicka')))
|
||||
|
@ -68,12 +69,12 @@ test.skip('handles missing social security number', async () => {
|
|||
expect(sendSMS).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test.skip('validates social security number', async () => {
|
||||
test('validates social security number', async () => {
|
||||
const screen = setup()
|
||||
|
||||
await waitFor(() =>
|
||||
fireEvent.changeText(
|
||||
screen.getByTestId('socialSecurityNumberInput'),
|
||||
screen.getByTestId('personalIdentityNumberInput'),
|
||||
'12121212'
|
||||
)
|
||||
)
|
||||
|
@ -83,14 +84,14 @@ test.skip('validates social security number', async () => {
|
|||
expect(sendSMS).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test.skip('can fill out the form with part of day absence', async () => {
|
||||
test('can fill out the form with part of day absence', async () => {
|
||||
Mockdate.set('2021-02-18 15:30')
|
||||
|
||||
const screen = setup()
|
||||
|
||||
await waitFor(() =>
|
||||
fireEvent.changeText(
|
||||
screen.getByTestId('socialSecurityNumberInput'),
|
||||
screen.getByTestId('personalIdentityNumberInput'),
|
||||
'1212121212'
|
||||
)
|
||||
)
|
||||
|
|
|
@ -2,10 +2,8 @@ import { useApi } from '@skolplattformen/api-hooks'
|
|||
import { render } from '../../utils/testHelpers'
|
||||
import React from 'react'
|
||||
import { Auth } from '../auth.component'
|
||||
import { useAsyncStorage } from 'use-async-storage'
|
||||
|
||||
jest.mock('@skolplattformen/api-hooks')
|
||||
jest.mock('use-async-storage')
|
||||
jest.mock('react-native-localize')
|
||||
|
||||
const setup = () => {
|
||||
|
@ -18,8 +16,6 @@ const setup = () => {
|
|||
navigate: jest.fn(),
|
||||
}
|
||||
|
||||
useAsyncStorage.mockReturnValue(['ssn', jest.fn()])
|
||||
|
||||
return render(<Auth navigation={navigation} />)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { RouteProp, useRoute } from '@react-navigation/native'
|
||||
import {
|
||||
Button,
|
||||
|
@ -24,13 +23,15 @@ import { translate } from '../utils/translation'
|
|||
import { AlertIcon } from './icon.component'
|
||||
import { RootStackParamList } from './navigation.component'
|
||||
import { NavigationTitle } from './navigationTitle.component'
|
||||
import { useUser } from '@skolplattformen/api-hooks'
|
||||
import usePersonalStorage from '../hooks/usePersonalStorage'
|
||||
|
||||
type AbsenceRouteProps = RouteProp<RootStackParamList, 'Absence'>
|
||||
|
||||
interface AbsenceFormValues {
|
||||
displayStartTimePicker: boolean
|
||||
displayEndTimePicker: boolean
|
||||
socialSecurityNumber: string
|
||||
personalIdentityNumber: string
|
||||
isFullDay: boolean
|
||||
startTime: moment.Moment
|
||||
endTime: moment.Moment
|
||||
|
@ -57,7 +58,7 @@ export const absenceRouteOptions =
|
|||
|
||||
const Absence = () => {
|
||||
const AbsenceSchema = Yup.object().shape({
|
||||
socialSecurityNumber: Yup.string()
|
||||
personalIdentityNumber: Yup.string()
|
||||
.required(translate('abscense.personalNumberMissing'))
|
||||
.test('is-valid', translate('abscense.invalidPersonalNumber'), (value) =>
|
||||
value ? Personnummer.valid(value) : true
|
||||
|
@ -65,49 +66,50 @@ const Absence = () => {
|
|||
isFullDay: Yup.bool().required(),
|
||||
})
|
||||
|
||||
const { data: user } = useUser()
|
||||
const route = useRoute<AbsenceRouteProps>()
|
||||
const { sendSMS } = useSMS()
|
||||
const { child } = route.params
|
||||
const [socialSecurityNumber, setSocialSecurityNumber] = React.useState('')
|
||||
const [personalIdFromStorage, setPersonalIdInStorage] = usePersonalStorage(
|
||||
user,
|
||||
`@childssn.${child.id}`,
|
||||
''
|
||||
)
|
||||
const [personalIdentityNumber, setPersonalIdentityNumber] = React.useState('')
|
||||
const minumumDate = moment().hours(8).minute(0)
|
||||
const maximumDate = moment().hours(17).minute(0)
|
||||
const styles = useStyleSheet(themedStyles)
|
||||
|
||||
const submit = useCallback(
|
||||
async (values: AbsenceFormValues) => {
|
||||
const ssn = Personnummer.parse(values.socialSecurityNumber).format()
|
||||
const personalIdNumber = Personnummer.parse(
|
||||
values.personalIdentityNumber
|
||||
).format()
|
||||
|
||||
if (values.isFullDay) {
|
||||
sendSMS(ssn)
|
||||
sendSMS(personalIdNumber)
|
||||
} else {
|
||||
sendSMS(
|
||||
`${ssn} ${moment(values.startTime).format('HHmm')}-${moment(
|
||||
values.endTime
|
||||
).format('HHmm')}`
|
||||
`${personalIdNumber} ${moment(values.startTime).format(
|
||||
'HHmm'
|
||||
)}-${moment(values.endTime).format('HHmm')}`
|
||||
)
|
||||
}
|
||||
|
||||
await AsyncStorage.setItem(
|
||||
`@childssn.${child.id}`,
|
||||
values.socialSecurityNumber
|
||||
)
|
||||
setPersonalIdInStorage(values.personalIdentityNumber)
|
||||
setPersonalIdentityNumber(values.personalIdentityNumber)
|
||||
},
|
||||
[child.id, sendSMS]
|
||||
[sendSMS, setPersonalIdInStorage]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const getSocialSecurityNumber = async () => {
|
||||
const ssn = await AsyncStorage.getItem(`@childssn.${child.id}`)
|
||||
setSocialSecurityNumber(ssn || '')
|
||||
}
|
||||
|
||||
getSocialSecurityNumber()
|
||||
}, [child])
|
||||
setPersonalIdentityNumber(personalIdFromStorage || '')
|
||||
}, [child, personalIdFromStorage, user])
|
||||
|
||||
const initialValues: AbsenceFormValues = {
|
||||
displayStartTimePicker: false,
|
||||
displayEndTimePicker: false,
|
||||
socialSecurityNumber: socialSecurityNumber || '',
|
||||
personalIdentityNumber: personalIdentityNumber || '',
|
||||
isFullDay: true,
|
||||
startTime: moment().hours(Math.max(8, new Date().getHours())).minute(0),
|
||||
endTime: maximumDate,
|
||||
|
@ -139,20 +141,22 @@ const Absence = () => {
|
|||
{translate('general.socialSecurityNumber')}
|
||||
</Text>
|
||||
<Input
|
||||
testID="socialSecurityNumberInput"
|
||||
testID="personalIdentityNumberInput"
|
||||
keyboardType="number-pad"
|
||||
onChangeText={handleChange('socialSecurityNumber')}
|
||||
onBlur={handleBlur('socialSecurityNumber')}
|
||||
status={hasError('socialSecurityNumber') ? 'danger' : 'basic'}
|
||||
value={values.socialSecurityNumber}
|
||||
onChangeText={handleChange('personalIdentityNumber')}
|
||||
onBlur={handleBlur('personalIdentityNumber')}
|
||||
status={hasError('personalIdentityNumber') ? 'danger' : 'basic'}
|
||||
value={values.personalIdentityNumber}
|
||||
style={styles.input}
|
||||
placeholder="YYYYMMDD-XXXX"
|
||||
accessoryRight={
|
||||
hasError('socialSecurityNumber') ? AlertIcon : undefined
|
||||
hasError('personalIdentityNumber') ? AlertIcon : undefined
|
||||
}
|
||||
/>
|
||||
{hasError('socialSecurityNumber') && (
|
||||
<Text style={styles.error}>{errors.socialSecurityNumber}</Text>
|
||||
{hasError('personalIdentityNumber') && (
|
||||
<Text style={styles.error}>
|
||||
{errors.personalIdentityNumber}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.field}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import AppStorage from '../services/appStorage'
|
||||
import { useNavigation } from '@react-navigation/core'
|
||||
import { useApi, useChildList } from '@skolplattformen/api-hooks'
|
||||
import { Child } from '@skolplattformen/embedded-api'
|
||||
|
@ -51,24 +51,41 @@ export const Children = () => {
|
|||
}
|
||||
|
||||
const logout = useCallback(() => {
|
||||
api.logout()
|
||||
AsyncStorage.clear()
|
||||
AppStorage.clearTemporaryItems().then(() => api.logout())
|
||||
}, [api])
|
||||
|
||||
const logoutAndClearPersonalData = useCallback(() => {
|
||||
api
|
||||
.getUser()
|
||||
.then((user) => AppStorage.clearPersonalData(user))
|
||||
.then(() => AppStorage.clearTemporaryItems().then(() => api.logout()))
|
||||
}, [api])
|
||||
|
||||
const logoutAndClearAll = useCallback(() => {
|
||||
AppStorage.nukeAllStorage().then(() => api.logout())
|
||||
}, [api])
|
||||
|
||||
const settingsOptions = useMemo(() => {
|
||||
return [translate('general.logout'), translate('general.cancel')]
|
||||
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]
|
||||
[logout, logoutAndClearAll, logoutAndClearPersonalData]
|
||||
)
|
||||
|
||||
const settings = useCallback(() => {
|
||||
const options = {
|
||||
cancelButtonIndex: 1,
|
||||
cancelButtonIndex: settingsOptions.length - 1,
|
||||
title: translate('general.settings'),
|
||||
optionsIOS: settingsOptions,
|
||||
optionsAndroid: settingsOptions,
|
||||
|
|
|
@ -21,7 +21,8 @@ import {
|
|||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import { useAsyncStorage } from 'use-async-storage'
|
||||
import useSettingsStorage from '../hooks/useSettingsStorage'
|
||||
import AppStorage from '../services/appStorage'
|
||||
import { schema } from '../app.json'
|
||||
import { Layout } from '../styles'
|
||||
import { translate } from '../utils/translation'
|
||||
|
@ -48,14 +49,11 @@ export const Login = () => {
|
|||
const [visible, showModal] = useState(false)
|
||||
const [showLoginMethod, setShowLoginMethod] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [cachedSsn, setCachedSsn] = useAsyncStorage('socialSecurityNumber', '')
|
||||
const [socialSecurityNumber, setSocialSecurityNumber] = useState('')
|
||||
const [personalIdNumber, setPersonalIdNumber] = useState('')
|
||||
const [valid, setValid] = useState(false)
|
||||
const [loginMethodIndex, setLoginMethodIndex] = useState(0)
|
||||
const [cachedLoginMethodIndex, setCachedLoginMethodIndex] = useAsyncStorage(
|
||||
'loginMethodIndex',
|
||||
'0'
|
||||
)
|
||||
const [cachedLoginMethodIndex, setCachedLoginMethodIndex] =
|
||||
useSettingsStorage('loginMethodIndex', '0')
|
||||
|
||||
const loginMethods = [
|
||||
translate('auth.bankid.OpenOnThisDevice'),
|
||||
|
@ -69,6 +67,7 @@ export const Login = () => {
|
|||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [loginMethodIndex])
|
||||
|
||||
useEffect(() => {
|
||||
if (loginMethodIndex !== parseInt(cachedLoginMethodIndex, 10)) {
|
||||
setLoginMethodIndex(parseInt(cachedLoginMethodIndex, 10))
|
||||
|
@ -76,17 +75,36 @@ export const Login = () => {
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cachedLoginMethodIndex])
|
||||
|
||||
/* Initial load functions */
|
||||
useEffect(() => {
|
||||
setValid(Personnummer.valid(socialSecurityNumber))
|
||||
}, [socialSecurityNumber])
|
||||
setValid(Personnummer.valid(personalIdNumber))
|
||||
}, [personalIdNumber])
|
||||
|
||||
useEffect(() => {
|
||||
if (cachedSsn && socialSecurityNumber !== cachedSsn) {
|
||||
setSocialSecurityNumber(cachedSsn)
|
||||
async function SetPersonalIdNumberIfSaved() {
|
||||
const storedPersonalIdNumber = await AppStorage.getSetting<string>(
|
||||
'cachedPersonalIdentityNumber'
|
||||
)
|
||||
|
||||
if (storedPersonalIdNumber) {
|
||||
setPersonalIdNumber(storedPersonalIdNumber)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [cachedSsn])
|
||||
|
||||
SetPersonalIdNumberIfSaved()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
async function SavePersonalIdNumber(numberToSave: string) {
|
||||
if (numberToSave) {
|
||||
await AppStorage.setSetting(
|
||||
'cachedPersonalIdentityNumber',
|
||||
numberToSave
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SavePersonalIdNumber(personalIdNumber)
|
||||
}, [personalIdNumber])
|
||||
|
||||
const loginHandler = async () => {
|
||||
showModal(false)
|
||||
|
@ -97,14 +115,12 @@ export const Login = () => {
|
|||
return () => {
|
||||
api.off('login', loginHandler)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
}, [api])
|
||||
|
||||
/* Helpers */
|
||||
const handleInput = (text: string) => {
|
||||
setValid(Personnummer.valid(text))
|
||||
setCachedSsn(text)
|
||||
setSocialSecurityNumber(text)
|
||||
setPersonalIdNumber(text)
|
||||
}
|
||||
|
||||
const openBankId = (token: string) => {
|
||||
|
@ -127,8 +143,7 @@ export const Login = () => {
|
|||
let ssn
|
||||
if (loginMethodIndex === 1) {
|
||||
ssn = Personnummer.parse(text).format(true)
|
||||
setCachedSsn(ssn)
|
||||
setSocialSecurityNumber(ssn)
|
||||
setPersonalIdNumber(ssn)
|
||||
}
|
||||
|
||||
const status = await api.login(ssn)
|
||||
|
@ -158,7 +173,7 @@ export const Login = () => {
|
|||
accessible={true}
|
||||
label={translate('general.socialSecurityNumber')}
|
||||
autoFocus
|
||||
value={socialSecurityNumber}
|
||||
value={personalIdNumber}
|
||||
style={styles.pnrInput}
|
||||
accessoryLeft={PersonIcon}
|
||||
accessoryRight={(props) => (
|
||||
|
@ -185,7 +200,7 @@ export const Login = () => {
|
|||
<ButtonGroup style={styles.loginButtonGroup} status="primary">
|
||||
<Button
|
||||
accessible={true}
|
||||
onPress={() => startLogin(socialSecurityNumber)}
|
||||
onPress={() => startLogin(personalIdNumber)}
|
||||
style={styles.loginButton}
|
||||
appearance="ghost"
|
||||
disabled={loginMethodIndex === 1 && !valid}
|
||||
|
|
|
@ -3,9 +3,10 @@ import * as RNLocalize from 'react-native-localize'
|
|||
import { LoadingComponent } from '../../components/loading.component'
|
||||
|
||||
import { LanguageService } from '../../services/languageService'
|
||||
import { LanguageStorage } from '../../services/languageStorage'
|
||||
import { translations } from '../../utils/translation'
|
||||
|
||||
import AppStorage from '../../services/appStorage'
|
||||
|
||||
interface LanguageContextProps {
|
||||
Strings: Record<string, any>
|
||||
languageCode?: string
|
||||
|
@ -67,7 +68,7 @@ export const LanguageProvider: React.FC<Props> = ({
|
|||
setLanguageCode(langCode)
|
||||
setStrings(data[langCode])
|
||||
if (cache) {
|
||||
LanguageStorage.save(langCode)
|
||||
AppStorage.setSetting('langCode', langCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +78,7 @@ export const LanguageProvider: React.FC<Props> = ({
|
|||
// Saved language
|
||||
if (cache) {
|
||||
// Get cached lang
|
||||
const cachedLang = await LanguageStorage.get()
|
||||
const cachedLang = await AppStorage.getSetting<string>('langCode')
|
||||
|
||||
// Try to find best suited language
|
||||
const { languageTag } =
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { renderHook, act } from '@testing-library/react-hooks'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import usePersonalStorage from '../usePersonalStorage'
|
||||
import { User } from '@skolplattformen/embedded-api'
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
await AsyncStorage.clear()
|
||||
})
|
||||
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
const prefix = user.personalNumber + '_'
|
||||
|
||||
test('use key prefix on set', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
usePersonalStorage(user, 'key', '')
|
||||
)
|
||||
|
||||
act(() => {
|
||||
const [, setValue] = result.current
|
||||
setValue('foo')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('foo')
|
||||
)
|
||||
})
|
||||
|
||||
test('return inital value if no set', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
usePersonalStorage(user, 'key', 'initialValue')
|
||||
)
|
||||
|
||||
const [value] = result.current
|
||||
|
||||
expect(value).toEqual('initialValue')
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(null)
|
||||
})
|
||||
|
||||
test('update value', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
usePersonalStorage(user, 'key', 'initialValue')
|
||||
)
|
||||
|
||||
const [initValue, setValue] = result.current
|
||||
setValue('update')
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
const [updateValue] = result.current
|
||||
|
||||
expect(initValue).toEqual('initialValue')
|
||||
expect(updateValue).toEqual('update')
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('update')
|
||||
)
|
||||
})
|
||||
|
||||
test('do nothing if personalId is empty', async () => {
|
||||
const emptyUser: User = { personalNumber: '' }
|
||||
let hookUser = emptyUser
|
||||
const { result, rerender, waitForNextUpdate } = renderHook(() =>
|
||||
usePersonalStorage(hookUser, 'key', '')
|
||||
)
|
||||
|
||||
act(() => {
|
||||
const [, setValue] = result.current
|
||||
setValue('foo')
|
||||
})
|
||||
|
||||
expect(AsyncStorage.setItem).not.toHaveBeenCalled()
|
||||
|
||||
hookUser = user
|
||||
rerender()
|
||||
|
||||
act(() => {
|
||||
const [, setValue] = result.current
|
||||
setValue('foo')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalled()
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
import { renderHook, act } from '@testing-library/react-hooks'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import useSettingsStorage from '../useSettingsStorage'
|
||||
import AppStorage from '../../services/appStorage'
|
||||
|
||||
beforeEach(() => {
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
const prefix = AppStorage.settingsStorageKeyPrefix
|
||||
|
||||
test('use key prefix on set', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsStorage('key', '')
|
||||
)
|
||||
|
||||
act(() => {
|
||||
const [, setValue] = result.current
|
||||
setValue('foo')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('foo')
|
||||
)
|
||||
})
|
||||
|
||||
test('return inital value if no set', async () => {
|
||||
const { result } = renderHook(() => useSettingsStorage('key', 'initialValue'))
|
||||
|
||||
const [value] = result.current
|
||||
|
||||
expect(value).toEqual('initialValue')
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(null)
|
||||
})
|
||||
|
||||
test('update value', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useSettingsStorage('key', 'initialValue')
|
||||
)
|
||||
|
||||
const [initValue, setValue] = result.current
|
||||
|
||||
act(() => {
|
||||
setValue('update')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
const [updateValue] = result.current
|
||||
|
||||
expect(initValue).toEqual('initialValue')
|
||||
expect(updateValue).toEqual('update')
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('update')
|
||||
)
|
||||
})
|
|
@ -0,0 +1,59 @@
|
|||
import { renderHook, act } from '@testing-library/react-hooks'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import useTempStorage from '../useTempStorage'
|
||||
import AppStorage from '../../services/appStorage'
|
||||
|
||||
beforeEach(() => {
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
const prefix = AppStorage.tempStorageKeyPrefix
|
||||
|
||||
test('use key prefix on set', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useTempStorage('key', '')
|
||||
)
|
||||
|
||||
act(() => {
|
||||
const [, setValue] = result.current
|
||||
setValue('foo')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('foo')
|
||||
)
|
||||
})
|
||||
|
||||
test('return inital value if no set', async () => {
|
||||
const { result } = renderHook(() => useTempStorage('key', 'initialValue'))
|
||||
|
||||
const [value] = result.current
|
||||
|
||||
expect(value).toEqual('initialValue')
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(null)
|
||||
})
|
||||
|
||||
test('update value', async () => {
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useTempStorage('key', 'initialValue')
|
||||
)
|
||||
|
||||
const [initValue, setValue] = result.current
|
||||
|
||||
act(() => {
|
||||
setValue('update')
|
||||
})
|
||||
|
||||
await waitForNextUpdate()
|
||||
|
||||
const [updateValue] = result.current
|
||||
|
||||
expect(initValue).toEqual('initialValue')
|
||||
expect(updateValue).toEqual('update')
|
||||
|
||||
expect(await AsyncStorage.getItem(prefix + 'key')).toEqual(
|
||||
JSON.stringify('update')
|
||||
)
|
||||
})
|
|
@ -0,0 +1,33 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
export default function useAsyncStorage<T>(
|
||||
storageKey: string,
|
||||
defaultValue: T
|
||||
): [T, (val: T) => void] {
|
||||
const [storageItem, setStorageItem] = useState(defaultValue)
|
||||
|
||||
async function setStoredValue(value: T) {
|
||||
try {
|
||||
if (!storageKey) return
|
||||
await AsyncStorage.setItem(storageKey, JSON.stringify(value))
|
||||
setStorageItem(value)
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function getStoredValue() {
|
||||
try {
|
||||
const data = await AsyncStorage.getItem(storageKey)
|
||||
if (typeof data === 'string') setStorageItem(JSON.parse(data))
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
getStoredValue()
|
||||
}, [storageKey])
|
||||
|
||||
return [
|
||||
storageItem !== undefined ? storageItem : defaultValue,
|
||||
setStoredValue,
|
||||
]
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { User } from '@skolplattformen/embedded-api'
|
||||
import useAsyncStorage from './useAsyncStorage'
|
||||
|
||||
export default function usePersonalStorage<T>(
|
||||
user: User,
|
||||
storageKey: string,
|
||||
defaultValue: T
|
||||
): [T, (val: T) => void] {
|
||||
const internalKey =
|
||||
user && user.personalNumber ? user.personalNumber + '_' + storageKey : ''
|
||||
return useAsyncStorage(internalKey, defaultValue)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import useAsyncStorage from './useAsyncStorage'
|
||||
import AppStorage from '../services/appStorage'
|
||||
|
||||
export default function useSettingsStorage<T>(
|
||||
storageKey: string,
|
||||
defaultValue: T
|
||||
): [T, (val: T) => void] {
|
||||
const settingsKey = AppStorage.settingsStorageKeyPrefix + storageKey
|
||||
return useAsyncStorage(settingsKey, defaultValue)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import useAsyncStorage from './useAsyncStorage'
|
||||
import AppStorage from '../services/appStorage'
|
||||
|
||||
export default function useTempStorage<T>(
|
||||
storageKey: string,
|
||||
defaultValue: T
|
||||
): [T, (val: T) => void] {
|
||||
const tempKey = AppStorage.tempStorageKeyPrefix + storageKey
|
||||
return useAsyncStorage(tempKey, defaultValue)
|
||||
}
|
|
@ -372,6 +372,10 @@ PODS:
|
|||
- React
|
||||
- RNDateTimePicker (3.4.3):
|
||||
- React-Core
|
||||
- RNDevMenu (4.0.2):
|
||||
- React-Core
|
||||
- React-Core/DevSupport
|
||||
- React-RCTNetwork
|
||||
- RNGestureHandler (1.10.3):
|
||||
- React-Core
|
||||
- RNLocalize (2.1.4):
|
||||
|
@ -479,6 +483,7 @@ DEPENDENCIES:
|
|||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
|
||||
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
|
||||
- RNDevMenu (from `../node_modules/react-native-dev-menu`)
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNLocalize (from `../node_modules/react-native-localize`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
|
@ -583,6 +588,8 @@ EXTERNAL SOURCES:
|
|||
:path: "../node_modules/@react-native-community/masked-view"
|
||||
RNDateTimePicker:
|
||||
:path: "../node_modules/@react-native-community/datetimepicker"
|
||||
RNDevMenu:
|
||||
:path: "../node_modules/react-native-dev-menu"
|
||||
RNGestureHandler:
|
||||
:path: "../node_modules/react-native-gesture-handler"
|
||||
RNLocalize:
|
||||
|
@ -650,6 +657,7 @@ SPEC CHECKSUMS:
|
|||
RNCAsyncStorage: 9b7605e899f9acb2fba33e87952c529731265453
|
||||
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
|
||||
RNDateTimePicker: d943800c936fb01c352fcfb70439550d2cb57092
|
||||
RNDevMenu: fd325b5554b61fe7f48d9205a3877cf5ee88cd7c
|
||||
RNGestureHandler: a479ebd5ed4221a810967000735517df0d2db211
|
||||
RNLocalize: 7f1e5792b65a839af55a9552d05b3558b66d017e
|
||||
RNReanimated: ad24db8af24e3fe1b5c462785bc3db8d5baae2ee
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"react-native-animatable": "^1.3.3",
|
||||
"react-native-appearance": "^0.3.4",
|
||||
"react-native-calendar-events": "2.2.0",
|
||||
"react-native-dev-menu": "^4.0.2",
|
||||
"react-native-fix-image": "2.1.0",
|
||||
"react-native-gesture-handler": "^1.10.3",
|
||||
"react-native-localize": "^2.0.2",
|
||||
|
@ -61,7 +62,6 @@
|
|||
"react-native-webview": "11.4.2",
|
||||
"react-native-weekly-calendar": "^0.2.0",
|
||||
"rn-actionsheet-module": "https://github.com/kolplattformen/rn-actionsheet-module.git",
|
||||
"use-async-storage": "1.2.0",
|
||||
"yup": "0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import AppStorage from '../appStorage'
|
||||
import { User } from '@skolplattformen/embedded-api'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
const prefix = AppStorage.settingsStorageKeyPrefix
|
||||
const temp = AppStorage.tempStorageKeyPrefix
|
||||
|
||||
test('Sets setting with prefix in storage', async () => {
|
||||
await AppStorage.setSetting('key', 'value')
|
||||
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
||||
prefix + 'key',
|
||||
JSON.stringify('value')
|
||||
)
|
||||
})
|
||||
|
||||
test('Can get setting from storage', async () => {
|
||||
await AppStorage.setSetting('key', 'value')
|
||||
const result = await AppStorage.getSetting<string>('key')
|
||||
|
||||
expect(result).toEqual('value')
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith(prefix + 'key')
|
||||
})
|
||||
|
||||
test('Clear only settings', async () => {
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
await AppStorage.setSetting('key', 'value')
|
||||
await AppStorage.setSetting('key2', 'value2')
|
||||
await AppStorage.setSetting('key3', 'value3')
|
||||
|
||||
await AppStorage.setTemporaryItem('nonSetting', 'nonSettingValue')
|
||||
await AppStorage.setPersonalData(user, 'personalData', 'personal id value')
|
||||
|
||||
await AppStorage.clearAllSettings()
|
||||
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
|
||||
expect(allKeys).toHaveLength(2)
|
||||
expect(allKeys[0]).toEqual(temp + 'nonSetting')
|
||||
expect(allKeys[1]).toEqual(user.personalNumber + '_' + 'personalData')
|
||||
})
|
||||
|
||||
test('Clear temporary items', async () => {
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
await AppStorage.setSetting('settingKey1', 'settingValue1')
|
||||
await AppStorage.setSetting('settingKey2', 'settingValue2')
|
||||
await AppStorage.setSetting('settingKey3', 'settingValue3')
|
||||
await AppStorage.setTemporaryItem('tempKey1', 'tempValue1')
|
||||
await AppStorage.setTemporaryItem('tempKey2', 'tempValue2')
|
||||
await AppStorage.setTemporaryItem('tempKey3', 'tempValue3')
|
||||
await AppStorage.setPersonalData(user, 'personalData', 'personal id value')
|
||||
|
||||
await AppStorage.clearTemporaryItems()
|
||||
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
|
||||
expect(allKeys).toHaveLength(4)
|
||||
expect(allKeys[0]).toEqual(prefix + 'settingKey1')
|
||||
expect(allKeys[3]).toEqual(user.personalNumber + '_' + 'personalData')
|
||||
})
|
||||
|
||||
test('Store temporary string in AsyncStorage', async () => {
|
||||
await AppStorage.setTemporaryItem('tempkey', 'tempvalue')
|
||||
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
||||
temp + 'tempkey',
|
||||
JSON.stringify('tempvalue')
|
||||
)
|
||||
})
|
||||
|
||||
test('Get temporary string from AsyncStorage', async () => {
|
||||
await AppStorage.getTemporaryItem('tempkey')
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith(temp + 'tempkey')
|
||||
})
|
||||
|
||||
test('Store temporary object in AsyncStorage', async () => {
|
||||
const obj = { a: 'foo', b: 5 }
|
||||
await AppStorage.setTemporaryItem('tempkey', obj)
|
||||
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
||||
temp + 'tempkey',
|
||||
JSON.stringify(obj)
|
||||
)
|
||||
})
|
||||
|
||||
test('Get temporary object from AsyncStorage', async () => {
|
||||
await AppStorage.getTemporaryItem('tempkey')
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith(temp + 'tempkey')
|
||||
})
|
||||
|
||||
test('Set personal data with personal number prefix', async () => {
|
||||
const obj = { a: 'gdpr', b: 'is fun' }
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
await AppStorage.setPersonalData(user, 'key', obj)
|
||||
|
||||
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
|
||||
user.personalNumber + '_' + 'key',
|
||||
JSON.stringify(obj)
|
||||
)
|
||||
})
|
||||
|
||||
test('Set personal data does nothing if personal number missing', async () => {
|
||||
const obj = { a: 'gdpr', b: 'is fun' }
|
||||
const user: User = { personalNumber: '' }
|
||||
await AppStorage.setPersonalData(user, 'key', obj)
|
||||
|
||||
expect(AsyncStorage.setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('Get personal data gets data if personal number matches', async () => {
|
||||
const data = 'personal data'
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
|
||||
await AppStorage.setPersonalData(user, 'key', data)
|
||||
const storedData = await AppStorage.getPersonalData(user, 'key')
|
||||
|
||||
expect(storedData).toEqual(data)
|
||||
})
|
||||
|
||||
test('Get no personal data gets data if personal number does not match', async () => {
|
||||
const data = 'personal data'
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
const anotherAser: User = { personalNumber: '202112312380' }
|
||||
|
||||
await AppStorage.setPersonalData(user, 'key', data)
|
||||
const storedData = await AppStorage.getPersonalData(anotherAser, 'key')
|
||||
|
||||
expect(user).not.toEqual(anotherAser)
|
||||
expect(storedData).toEqual(null)
|
||||
})
|
||||
|
||||
test('Clear only PersonalData', async () => {
|
||||
await AppStorage.setSetting('settingKey1', 'settingValue1')
|
||||
await AppStorage.setTemporaryItem('tempKey1', 'tempValue1')
|
||||
|
||||
const data = 'personal data'
|
||||
const user: User = { personalNumber: '201701012393' }
|
||||
await AppStorage.setPersonalData(user, 'key', data)
|
||||
|
||||
await AppStorage.clearPersonalData(user)
|
||||
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
expect(allKeys).toHaveLength(2)
|
||||
expect(allKeys).not.toContain(user.personalNumber + '_key')
|
||||
})
|
||||
|
||||
test('Clear PersonalData does nothing if personalnumber is empty', async () => {
|
||||
const user: User = { personalNumber: '' }
|
||||
await AppStorage.clearPersonalData(user)
|
||||
|
||||
expect(AsyncStorage.multiRemove).not.toHaveBeenCalled()
|
||||
})
|
|
@ -0,0 +1,122 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { User } from '@skolplattformen/embedded-api'
|
||||
|
||||
export default class AppStorage {
|
||||
static settingsStorageKeyPrefix = 'appsetting_'
|
||||
static tempStorageKeyPrefix = 'tempItem_'
|
||||
|
||||
/**
|
||||
* Stores a setting
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
static async setSetting<T>(key: string, value: T) {
|
||||
const jsonValue = JSON.stringify(value)
|
||||
await AsyncStorage.setItem(this.settingsStorageKeyPrefix + key, jsonValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a stored setting
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
static async getSetting<T>(key: string): Promise<T | null> {
|
||||
const value = await AsyncStorage.getItem(
|
||||
this.settingsStorageKeyPrefix + key
|
||||
)
|
||||
return value ? (JSON.parse(value) as T) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a personal data item
|
||||
* @param user
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
static async setPersonalData<T>(user: User, key: string, value: T) {
|
||||
const jsonValue = JSON.stringify(value)
|
||||
if (user.personalNumber) {
|
||||
const storageKey = user.personalNumber + '_' + key
|
||||
await AsyncStorage.setItem(storageKey, jsonValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored personal data
|
||||
* @param user
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
static async getPersonalData<T>(user: User, key: string): Promise<T | null> {
|
||||
if (user.personalNumber) {
|
||||
const value = await AsyncStorage.getItem(user.personalNumber + '_' + key)
|
||||
return value ? (JSON.parse(value) as T) : null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a temporary items. The items are cleared at logout.
|
||||
* Think of this as a session storage
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
static async setTemporaryItem<T>(key: string, value: T) {
|
||||
const jsonValue = JSON.stringify(value)
|
||||
await AsyncStorage.setItem(this.tempStorageKeyPrefix + key, jsonValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a temporary stored item
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
static async getTemporaryItem<T>(key: string): Promise<T | null> {
|
||||
const value = await AsyncStorage.getItem(this.tempStorageKeyPrefix + key)
|
||||
return value ? (JSON.parse(value) as T) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all settings
|
||||
*/
|
||||
static async clearAllSettings(): Promise<void> {
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
const settingsKeys = allKeys.filter((x) =>
|
||||
x.startsWith(this.settingsStorageKeyPrefix)
|
||||
)
|
||||
await AsyncStorage.multiRemove(settingsKeys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all temporary items
|
||||
*/
|
||||
static async clearTemporaryItems() {
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
const notSettingsKeys = allKeys.filter((x) =>
|
||||
x.startsWith(this.tempStorageKeyPrefix)
|
||||
)
|
||||
await AsyncStorage.multiRemove(notSettingsKeys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all personal identififieble data (GDPR)
|
||||
* @param user
|
||||
* @returns
|
||||
*/
|
||||
static async clearPersonalData(user: User): Promise<void> {
|
||||
if (!user.personalNumber) return
|
||||
|
||||
const allKeys = await AsyncStorage.getAllKeys()
|
||||
const personalDataKeys = allKeys.filter((x) =>
|
||||
x.startsWith(user.personalNumber ?? '')
|
||||
)
|
||||
await AsyncStorage.multiRemove(personalDataKeys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all async storage for this app and all libs that it uses
|
||||
*/
|
||||
static async nukeAllStorage() {
|
||||
await AsyncStorage.clear()
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import AsyncStorage from '@react-native-community/async-storage'
|
||||
|
||||
const AsyncStoreKey = { language: 'local-language-async' }
|
||||
|
||||
export const LanguageStorage = {
|
||||
save: async (languageCode: string) => {
|
||||
try {
|
||||
await AsyncStorage.setItem(AsyncStoreKey.language, languageCode)
|
||||
} catch (error) {}
|
||||
},
|
||||
remove: () => {
|
||||
AsyncStorage.removeItem(AsyncStoreKey.language)
|
||||
},
|
||||
get: async () => {
|
||||
try {
|
||||
const result = await AsyncStorage.getItem(AsyncStoreKey.language)
|
||||
return result
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock'
|
||||
import moment from 'moment'
|
||||
import 'moment/locale/sv'
|
||||
import 'react-native-gesture-handler/jestSetup'
|
||||
|
||||
moment.locale('sv')
|
||||
|
||||
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage)
|
||||
|
||||
// Silence useNativeDriver error
|
||||
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper')
|
||||
|
||||
|
|
|
@ -73,6 +73,8 @@
|
|||
"confirm": "Confirm",
|
||||
"loading": "Loading…",
|
||||
"logout": "Log out",
|
||||
"logoutAndClearPersonalData": "Log out and clear personal data",
|
||||
"logoutAndClearAllDataInclSettings": "Log out and clear all data including settings",
|
||||
"send": "Send",
|
||||
"settings": "Settings",
|
||||
"socialSecurityNumber": "Personal identity number",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue