diff --git a/packages/app/App.js b/packages/app/App.js
index 393876ce..b9c77e07 100644
--- a/packages/app/App.js
+++ b/packages/app/App.js
@@ -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()
diff --git a/packages/app/__mocks__/@react-native-async-storage/async-storage.js b/packages/app/__mocks__/@react-native-async-storage/async-storage.js
new file mode 100644
index 00000000..d78ea925
--- /dev/null
+++ b/packages/app/__mocks__/@react-native-async-storage/async-storage.js
@@ -0,0 +1,4 @@
+export {
+ default,
+ useAsyncStorage,
+} from '@react-native-async-storage/async-storage/jest/async-storage-mock'
diff --git a/packages/app/__mocks__/@react-native-community/async-storage.js b/packages/app/__mocks__/@react-native-community/async-storage.js
deleted file mode 100644
index 74e22103..00000000
--- a/packages/app/__mocks__/@react-native-community/async-storage.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from '@react-native-async-storage/async-storage/jest/async-storage-mock'
diff --git a/packages/app/components/__tests__/Absence.test.js b/packages/app/components/__tests__/Absence.test.js
index 9cbee456..80c5d47b 100644
--- a/packages/app/components/__tests__/Absence.test.js
+++ b/packages/app/components/__tests__/Absence.test.js
@@ -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'
)
)
diff --git a/packages/app/components/__tests__/Auth.test.js b/packages/app/components/__tests__/Auth.test.js
index 576f31cc..24387224 100644
--- a/packages/app/components/__tests__/Auth.test.js
+++ b/packages/app/components/__tests__/Auth.test.js
@@ -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()
}
diff --git a/packages/app/components/absence.component.tsx b/packages/app/components/absence.component.tsx
index 87baede5..4e3b901b 100644
--- a/packages/app/components/absence.component.tsx
+++ b/packages/app/components/absence.component.tsx
@@ -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
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()
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')}
- {hasError('socialSecurityNumber') && (
- {errors.socialSecurityNumber}
+ {hasError('personalIdentityNumber') && (
+
+ {errors.personalIdentityNumber}
+
)}
diff --git a/packages/app/components/children.component.tsx b/packages/app/components/children.component.tsx
index 7eaa2afa..29068e0b 100644
--- a/packages/app/components/children.component.tsx
+++ b/packages/app/components/children.component.tsx
@@ -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,
diff --git a/packages/app/components/login.component.tsx b/packages/app/components/login.component.tsx
index 7183730d..2a38cb8a 100644
--- a/packages/app/components/login.component.tsx
+++ b/packages/app/components/login.component.tsx
@@ -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(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(
+ '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 = () => {