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 = () => {