diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js index 1145f66bd..d7cbe6cb0 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,73 +1,73 @@ // @flow import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { latestBackupInfoResponseValidator, type LatestBackupInfo, } from 'lib/types/backup-types.js'; import { assertWithValidator } from 'lib/utils/validation-utils.js'; import { useGetBackupSecretForLoggedInUser } from './use-get-backup-secret.js'; import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; type ClientBackup = { - +createFullBackup: () => Promise, - +createUserKeysBackup: () => Promise, + +createFullBackup: () => Promise, + +createUserKeysBackup: () => Promise, +retrieveLatestBackupInfo: () => Promise, }; function useClientBackup(): ClientBackup { const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const currentUserInfo = useSelector(state => state.currentUserInfo); const loggedIn = useSelector(isLoggedIn); const getBackupSecret = useGetBackupSecretForLoggedInUser(); const createFullBackup = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload backup for not logged in user.'); } const backupSecret = await getBackupSecret(); - await commCoreModule.createFullBackup(backupSecret); + return commCoreModule.createFullBackup(backupSecret); }, [loggedIn, currentUserID, getBackupSecret]); const createUserKeysBackup = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to upload User Keys for not logged in user.'); } const backupSecret = await getBackupSecret(); - await commCoreModule.createUserKeysBackup(backupSecret); + return commCoreModule.createUserKeysBackup(backupSecret); }, [loggedIn, currentUserID, getBackupSecret]); 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( () => ({ createFullBackup, createUserKeysBackup, retrieveLatestBackupInfo, }), [retrieveLatestBackupInfo, createFullBackup, createUserKeysBackup], ); } export { useClientBackup }; diff --git a/native/native_rust_library/src/backup/compaction_upload_promises.rs b/native/native_rust_library/src/backup/compaction_upload_promises.rs index 6f84d4855..930103bda 100644 --- a/native/native_rust_library/src/backup/compaction_upload_promises.rs +++ b/native/native_rust_library/src/backup/compaction_upload_promises.rs @@ -1,24 +1,28 @@ -use crate::handle_void_result_as_callback; +use crate::handle_string_result_as_callback; use lazy_static::lazy_static; use std::{collections::HashMap, sync::Mutex}; lazy_static! { static ref COMPACTION_UPLOAD_PROMISES: Mutex> = Default::default(); } pub fn insert(backup_id: String, promise_id: u32) { if let Ok(mut backups_to_promises) = COMPACTION_UPLOAD_PROMISES.lock() { backups_to_promises.insert(backup_id, promise_id); }; } pub fn resolve(backup_id: &str, result: Result<(), String>) { let Ok(mut backups_to_promises) = COMPACTION_UPLOAD_PROMISES.lock() else { return; }; let Some(promise_id) = backups_to_promises.remove(backup_id) else { return; }; - handle_void_result_as_callback(result, promise_id); + + let backup_id_result: Result = + result.map(|_| backup_id.to_string()); + + handle_string_result_as_callback(backup_id_result, promise_id); } diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js index 3a5903b28..86646b720 100644 --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,256 +1,258 @@ // @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 { useGetBackupSecretForLoggedInUser } from '../backup/use-get-backup-secret.js'; import Button from '../components/button.react.js'; import { commCoreModule } from '../native-modules.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { RestoreSIWEBackupRouteName } from '../navigation/route-names.js'; import { setLocalSettingsActionType } from '../redux/action-types.js'; import { persistConfig } from '../redux/persist.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 getBackupSecret = useGetBackupSecretForLoggedInUser(); const isBackupEnabled = useSelector( state => state.localSettings.isBackupEnabled, ); const { createFullBackup, retrieveLatestBackupInfo, createUserKeysBackup } = useClientBackup(); const uploadBackup = React.useCallback(async () => { - let message = 'Success'; + let message; try { - await createFullBackup(); + const backupID = await createFullBackup(); + message = `Success!\n` + `Backup ID: ${backupID}`; } catch (e) { message = `Backup upload error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Upload protocol result', message); }, [createFullBackup]); const uploadUserKeys = React.useCallback(async () => { - let message = 'Success'; + let message; try { - await createUserKeysBackup(); + const backupID = await createUserKeysBackup(); + message = `Success!\n` + `Backup ID: ${backupID}`; } catch (e) { message = `User Keys upload error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Upload User Keys result', message); }, [createUserKeysBackup]); const testRestoreForPasswordUser = React.useCallback(async () => { let message = 'success'; try { const [latestBackupInfo, backupSecret] = await Promise.all([ retrieveLatestBackupInfo(), getBackupSecret(), ]); await commCoreModule.restoreBackup( backupSecret, persistConfig.version.toString(), latestBackupInfo.backupID, ); console.info('Backup restored.'); } catch (e) { message = `Backup restore error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Restore protocol result', message); }, [getBackupSecret, retrieveLatestBackupInfo]); 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 backupInfo = await retrieveLatestBackupInfo(); const { siweBackupData, backupID } = backupInfo; if (!siweBackupData) { throw new Error('Missing SIWE message for Wallet user backup'); } const { 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, retrieveLatestBackupInfo]); 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; diff --git a/native/schema/CommCoreModuleSchema.js b/native/schema/CommCoreModuleSchema.js index 5789a655c..483d1341f 100644 --- a/native/schema/CommCoreModuleSchema.js +++ b/native/schema/CommCoreModuleSchema.js @@ -1,247 +1,247 @@ // @flow 'use strict'; import { TurboModuleRegistry } from 'react-native'; import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport.js'; import type { ClientDBMessageStoreOperation } from 'lib/ops/message-store-ops.js'; import type { ClientDBReportStoreOperation } from 'lib/ops/report-store-ops.js'; import type { ClientDBThreadStoreOperation } from 'lib/ops/thread-store-ops.js'; import type { OneTimeKeysResult, SignedPrekeys, ClientPublicKeys, EncryptedData, OutboundSessionCreationResult, } from 'lib/types/crypto-types.js'; import type { ClientDBMessageInfo } from 'lib/types/message-types.js'; import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js'; import type { InboundP2PMessage, OutboundP2PMessage, } from 'lib/types/sqlite-types.js'; import type { ClientDBStore, ClientDBStoreOperations, } from 'lib/types/store-ops-types'; import type { ClientDBThreadInfo } from 'lib/types/thread-types.js'; type CommServicesAuthMetadata = { +userID?: ?string, +deviceID?: ?string, +accessToken?: ?string, }; interface Spec extends TurboModule { +getDraft: (key: string) => Promise; +updateDraft: (key: string, text: string) => Promise; +moveDraft: (oldKey: string, newKey: string) => Promise; +getClientDBStore: () => Promise; +removeAllDrafts: () => Promise; +getInitialMessagesSync: () => $ReadOnlyArray; +processMessageStoreOperationsSync: ( operations: $ReadOnlyArray, ) => void; +getAllThreadsSync: () => $ReadOnlyArray; +processReportStoreOperationsSync: ( operations: $ReadOnlyArray, ) => void; +processThreadStoreOperationsSync: ( operations: $ReadOnlyArray, ) => void; +processDBStoreOperations: (operations: Object) => Promise; +initializeCryptoAccount: () => Promise; +getUserPublicKey: () => Promise; +getOneTimeKeys: (oneTimeKeysAmount: number) => Promise; +validateAndGetPrekeys: () => Promise; +validateAndUploadPrekeys: ( authUserID: string, authDeviceID: string, authAccessToken: string, ) => Promise; +initializeNotificationsSession: ( identityKeys: string, prekey: string, prekeySignature: string, oneTimeKey: ?string, keyserverID: string, ) => Promise; +isNotificationsSessionInitialized: () => Promise; +isDeviceNotificationsSessionInitialized: ( deviceID: string, ) => Promise; +isNotificationsSessionInitializedWithDevices: ( deviceIDs: $ReadOnlyArray, ) => Promise<{ +[deviceID: string]: boolean }>; +updateKeyserverDataInNotifStorage: ( keyserversData: $ReadOnlyArray<{ +id: string, +unreadCount: number }>, ) => Promise; +removeKeyserverDataFromNotifStorage: ( keyserverIDsToDelete: $ReadOnlyArray, ) => Promise; +getKeyserverDataFromNotifStorage: ( keyserverIDs: $ReadOnlyArray, ) => Promise<$ReadOnlyArray<{ +id: string, +unreadCount: number }>>; +updateUnreadThickThreadsInNotifsStorage: ( unreadThickThreadIDs: $ReadOnlyArray, ) => Promise; +getUnreadThickThreadIDsFromNotifsStorage: () => Promise< $ReadOnlyArray, >; +initializeContentOutboundSession: ( identityKeys: string, prekey: string, prekeySignature: string, oneTimeKey: ?string, deviceID: string, ) => Promise; +initializeContentInboundSession: ( identityKeys: string, encryptedContent: Object, deviceID: string, sessionVersion: number, overwrite: boolean, ) => Promise; +isContentSessionInitialized: (deviceID: string) => Promise; +initializeNotificationsOutboundSession: ( identityKeys: string, prekey: string, prekeySignature: string, oneTimeKey: ?string, deviceID: string, ) => Promise; +encrypt: (message: string, deviceID: string) => Promise; +encryptNotification: ( payload: string, deviceID: string, ) => Promise; +encryptAndPersist: ( message: string, deviceID: string, messageID: string, ) => Promise; +decrypt: (encryptedData: Object, deviceID: string) => Promise; +decryptAndPersist: ( encryptedData: Object, deviceID: string, userID: string, messageID: string, ) => Promise; +signMessage: (message: string) => Promise; +verifySignature: ( publicKey: string, message: string, signature: string, ) => Promise; +getCodeVersion: () => number; +terminate: () => void; +setNotifyToken: (token: string) => Promise; +clearNotifyToken: () => Promise; +stampSQLiteDBUserID: (userID: string) => Promise; +getSQLiteStampedUserID: () => Promise; +clearSensitiveData: () => Promise; +checkIfDatabaseNeedsDeletion: () => boolean; +reportDBOperationsFailure: () => void; +computeBackupKey: (password: string, backupID: string) => Promise; +generateRandomString: (size: number) => Promise; +setCommServicesAuthMetadata: ( userID: string, deviceID: string, accessToken: string, ) => Promise; +getCommServicesAuthMetadata: () => Promise; +clearCommServicesAuthMetadata: () => Promise; +setCommServicesAccessToken: (accessToken: string) => Promise; +clearCommServicesAccessToken: () => Promise; +startBackupHandler: () => void; +stopBackupHandler: () => void; - +createUserKeysBackup: (backupSecret: string) => Promise; - +createFullBackup: (backupSecret: string) => Promise; + +createUserKeysBackup: (backupSecret: string) => Promise; + +createFullBackup: (backupSecret: string) => Promise; +restoreBackup: ( backupSecret: string, maxVersion: string, backupID: string, ) => Promise; +restoreBackupData: ( backupID: string, backupDataKey: string, backupLogDataKey: string, maxVersion: string, ) => Promise; +retrieveBackupKeys: ( backupSecret: string, backupID: string, ) => Promise; +retrieveLatestBackupInfo: (userIdentifier: string) => Promise; +setSIWEBackupSecrets: (siweBackupSecrets: Object) => Promise; +getSIWEBackupSecrets: () => Promise; +getAllInboundP2PMessages: () => Promise>; +removeInboundP2PMessages: (ids: $ReadOnlyArray) => Promise; +getInboundP2PMessagesByID: ( ids: $ReadOnlyArray, ) => Promise>; +getOutboundP2PMessagesByID: ( ids: $ReadOnlyArray, ) => Promise>; +getUnsentOutboundP2PMessages: () => Promise>; +markOutboundP2PMessageAsSent: ( messageID: string, deviceID: string, ) => Promise; +removeOutboundP2PMessage: ( messageID: string, deviceID: string, ) => Promise; +resetOutboundP2PMessagesForDevice: ( deviceID: string, ) => Promise>; +getSyncedDatabaseVersion: () => Promise; +markPrekeysAsPublished: () => Promise; +getRelatedMessages: ( messageID: string, ) => Promise>; +searchMessages: ( query: string, threadID: string, timestampCursor: ?string, messageIDCursor: ?string, ) => Promise>; +fetchMessages: ( threadID: string, limit: number, offset: number, ) => Promise>; } export interface CoreModuleSpec extends Spec { +computeBackupKey: ( password: string, backupID: string, ) => Promise; +decrypt: (encryptedData: EncryptedData, deviceID: string) => Promise; +decryptAndPersist: ( encryptedData: EncryptedData, deviceID: string, userID: string, messageID: string, ) => Promise; +initializeContentInboundSession: ( identityKeys: string, encryptedContent: EncryptedData, deviceID: string, sessionVersion: number, overwrite: boolean, ) => Promise; +setSIWEBackupSecrets: ( siweBackupSecrets: SIWEBackupSecrets, ) => Promise; +getSIWEBackupSecrets: () => Promise; +processDBStoreOperations: ( operations: ClientDBStoreOperations, ) => Promise; } export default (TurboModuleRegistry.getEnforcing( 'CommTurboModule', ): Spec);