diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js index a8788fcd3..31b2a27f4 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,115 +1,142 @@ // @flow import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { - type SIWEBackupData, + latestBackupInfoResponseValidator, siweBackupDataValidator, + type LatestBackupInfo, + type SIWEBackupData, } from 'lib/types/backup-types.js'; import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { fetchNativeKeychainCredentials } from '../account/native-credentials.js'; import { commCoreModule } from '../native-modules.js'; import { persistConfig } from '../redux/persist.js'; import { useSelector } from '../redux/redux-utils.js'; type ClientBackup = { +uploadBackupProtocol: () => Promise, +restorePasswordUserBackupProtocol: () => Promise, +retrieveLatestSIWEBackupData: () => Promise, + +retrieveLatestBackupInfo: () => Promise, }; async function getBackupSecret(): Promise { const nativeCredentials = await fetchNativeKeychainCredentials(); if (!nativeCredentials) { throw new Error('Native credentials are missing'); } return nativeCredentials.password; } async function getSIWEBackupSecrets(): Promise { const siweBackupSecrets = await commCoreModule.getSIWEBackupSecrets(); if (!siweBackupSecrets) { throw new Error('SIWE backup message and its signature are missing'); } return siweBackupSecrets; } function useClientBackup(): ClientBackup { const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const currentUserInfo = useSelector(state => state.currentUserInfo); const loggedIn = useSelector(isLoggedIn); const uploadBackupProtocol = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload backup for not logged in user.'); } console.info('Start uploading backup...'); if (accountHasPassword(currentUserInfo)) { const backupSecret = await getBackupSecret(); await commCoreModule.createNewBackup(backupSecret); } else { const { message, signature } = await getSIWEBackupSecrets(); await commCoreModule.createNewSIWEBackup(signature, message); } console.info('Backup uploaded.'); }, [currentUserID, loggedIn, currentUserInfo]); const restorePasswordUserBackupProtocol = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to restore backup for not logged in user.'); } if (!accountHasPassword(currentUserInfo)) { throw new Error( 'Attempt to restore from password for non-password user.', ); } console.info('Start restoring backup...'); const backupSecret = await getBackupSecret(); await commCoreModule.restoreBackup( backupSecret, persistConfig.version.toString(), ); console.info('Backup restored.'); }, [currentUserID, loggedIn, currentUserInfo]); const retrieveLatestSIWEBackupData = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to restore backup for not logged in user.'); } if (accountHasPassword(currentUserInfo)) { throw new Error( 'Attempt to retrieve siwe backup data for password user.', ); } const serializedBackupData = await commCoreModule.retrieveLatestSIWEBackupData(); return assertWithValidator( JSON.parse(serializedBackupData), siweBackupDataValidator, ); }, [currentUserID, currentUserInfo, loggedIn]); - return { - uploadBackupProtocol, - restorePasswordUserBackupProtocol, - retrieveLatestSIWEBackupData, - }; + const retrieveLatestBackupInfo = React.useCallback(async () => { + if (!loggedIn || !currentUserID || !currentUserInfo?.username) { + throw new Error('Attempt to restore backup for not logged in user.'); + } + const userIdentitifer = currentUserInfo?.username; + + const response = + await commCoreModule.retrieveLatestBackupInfo(userIdentitifer); + + return assertWithValidator( + JSON.parse(response), + latestBackupInfoResponseValidator, + ); + }, [currentUserID, currentUserInfo, loggedIn]); + + return React.useMemo( + () => ({ + uploadBackupProtocol, + restorePasswordUserBackupProtocol, + retrieveLatestSIWEBackupData, + retrieveLatestBackupInfo, + }), + [ + restorePasswordUserBackupProtocol, + retrieveLatestBackupInfo, + retrieveLatestSIWEBackupData, + uploadBackupProtocol, + ], + ); } export { getBackupSecret, useClientBackup }; diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js index 3f2506ec0..54dbc4a30 100644 --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,188 +1,221 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Switch, Text, View } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { useClientBackup } from '../backup/use-client-backup.js'; import Button from '../components/button.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { RestoreSIWEBackupRouteName } from '../navigation/route-names.js'; import { setLocalSettingsActionType } from '../redux/action-types.js'; import { useSelector } from '../redux/redux-utils.js'; import { useColors, useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; type Props = { +navigation: ProfileNavigationProp<'BackupMenu'>, +route: NavigationRoute<'BackupMenu'>, }; // eslint-disable-next-line no-unused-vars function BackupMenu(props: Props): React.Node { const styles = useStyles(unboundStyles); const dispatch = useDispatch(); const colors = useColors(); const currentUserInfo = useSelector(state => state.currentUserInfo); const navigation = useNavigation(); const isBackupEnabled = useSelector( state => state.localSettings.isBackupEnabled, ); const { uploadBackupProtocol, restorePasswordUserBackupProtocol, retrieveLatestSIWEBackupData, + retrieveLatestBackupInfo, } = useClientBackup(); const uploadBackup = React.useCallback(async () => { let message = 'Success'; try { await uploadBackupProtocol(); } catch (e) { message = `Backup upload error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Upload protocol result', message); }, [uploadBackupProtocol]); const testRestoreForPasswordUser = React.useCallback(async () => { let message = 'success'; try { await restorePasswordUserBackupProtocol(); } catch (e) { message = `Backup restore error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Restore protocol result', message); }, [restorePasswordUserBackupProtocol]); + const testLatestBackupInfo = React.useCallback(async () => { + let message; + try { + const backupInfo = await retrieveLatestBackupInfo(); + const { backupID, userID } = backupInfo; + message = + `Success!\n` + + `Backup ID: ${backupID},\n` + + `userID: ${userID},\n` + + `userID check: ${currentUserInfo?.id === userID ? 'true' : 'false'}`; + } catch (e) { + message = `Latest backup info error: ${String( + getMessageForException(e), + )}`; + console.error(message); + } + Alert.alert('Latest backup info result', message); + }, [currentUserInfo?.id, retrieveLatestBackupInfo]); + const testRestoreForSIWEUser = React.useCallback(async () => { let message = 'success'; try { const siweBackupData = await retrieveLatestSIWEBackupData(); const { backupID, siweBackupMsgNonce, siweBackupMsgIssuedAt, siweBackupMsgStatement, } = siweBackupData; navigation.navigate<'RestoreSIWEBackup'>({ name: RestoreSIWEBackupRouteName, params: { backupID, siweNonce: siweBackupMsgNonce, siweStatement: siweBackupMsgStatement, siweIssuedAt: siweBackupMsgIssuedAt, }, }); } catch (e) { message = `Backup restore error: ${String(getMessageForException(e))}`; console.error(message); } }, [navigation, retrieveLatestSIWEBackupData]); const onBackupToggled = React.useCallback( (value: boolean) => { dispatch({ type: setLocalSettingsActionType, payload: { isBackupEnabled: value }, }); }, [dispatch], ); const onPressRestoreButton = accountHasPassword(currentUserInfo) ? testRestoreForPasswordUser : testRestoreForSIWEUser; return ( SETTINGS Toggle automatic backup ACTIONS + + + ); } const unboundStyles = { scrollViewContentContainer: { paddingTop: 24, }, scrollView: { backgroundColor: 'panelBackground', }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, marginVertical: 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, submenuButton: { flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 10, alignItems: 'center', }, submenuText: { color: 'panelForegroundLabel', flex: 1, fontSize: 16, }, row: { flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 14, }, }; export default BackupMenu;