diff --git a/native/account/registration/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js --- a/native/account/registration/siwe-backup-message-creation.react.js +++ b/native/account/registration/siwe-backup-message-creation.react.js @@ -37,9 +37,9 @@ function CreateSIWEBackupMessageBase( props: CreateSIWEBackupMessageBaseProps, ): React.Node { + const styles = useStyles(unboundStyles); const { onSuccessfulWalletSignature, onExistingWalletSignature, onSkip } = props; - const styles = useStyles(unboundStyles); const { panelState, @@ -198,6 +198,74 @@ ); } +type SignSIWEBackupMessageForRestoreBaseProps = { + +messageToSign: string, + +onSuccessfulWalletSignature: (result: SIWEResult) => void, + +onSkip: () => void, +}; + +function SignSIWEBackupMessageForRestore( + props: SignSIWEBackupMessageForRestoreBaseProps, +): React.Node { + const styles = useStyles(unboundStyles); + const { + panelState, + openPanel, + onPanelClosed, + onPanelClosing, + siwePanelSetLoading, + } = useSIWEPanelState(); + + const { messageToSign, onSuccessfulWalletSignature, onSkip } = props; + + let siwePanel; + if (panelState !== 'closed') { + siwePanel = ( + + ); + } + + return ( + <> + + + Decrypting your Comm backup + + Comm user backups are encrypted so that our backend is not able to + see user data. + + + + + + + + + + + {siwePanel} + + ); +} + const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, @@ -224,4 +292,8 @@ }, }; -export { CreateSIWEBackupMessageBase, CreateSIWEBackupMessage }; +export { + CreateSIWEBackupMessageBase, + CreateSIWEBackupMessage, + SignSIWEBackupMessageForRestore, +}; diff --git a/native/backup/restore-siwe-backup.react.js b/native/backup/restore-siwe-backup.react.js new file mode 100644 --- /dev/null +++ b/native/backup/restore-siwe-backup.react.js @@ -0,0 +1,76 @@ +// @flow + +import * as React from 'react'; +import { Alert } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { type SIWEResult } from 'lib/types/siwe-types.js'; +import { getMessageForException } from 'lib/utils/errors.js'; + +import { SignSIWEBackupMessageForRestore } from '../account/registration/siwe-backup-message-creation.react.js'; +import { commCoreModule } from '../native-modules.js'; +import { type RootNavigationProp } from '../navigation/root-navigator.react.js'; +import { type NavigationRoute } from '../navigation/route-names.js'; +import { useStyles } from '../themes/colors.js'; + +export type RestoreSIWEBackupParams = { + +backupID: string, + +siweBackupMsg: string, +}; + +type Props = { + +navigation: RootNavigationProp<'RestoreSIWEBackup'>, + +route: NavigationRoute<'RestoreSIWEBackup'>, +}; + +function RestoreSIWEBackup(props: Props): React.Node { + const styles = useStyles(unboundStyles); + const { goBack } = props.navigation; + const { route } = props; + const { + params: { backupID, siweBackupMsg }, + } = route; + + const onSuccessfulWalletSignature = React.useCallback( + (result: SIWEResult) => { + void (async () => { + const { signature } = result; + let message = 'success'; + try { + await commCoreModule.restoreSIWEBackup(signature, backupID); + } catch (e) { + message = `Backup restore error: ${String( + getMessageForException(e), + )}`; + console.error(message); + } + Alert.alert('Restore protocol result', message); + goBack(); + })(); + }, + [goBack, backupID], + ); + + return ( + + { + goBack(); + }} + onSuccessfulWalletSignature={onSuccessfulWalletSignature} + /> + + ); +} + +const safeAreaEdges = ['top']; +const unboundStyles = { + container: { + flex: 1, + backgroundColor: 'panelBackground', + justifyContent: 'space-between', + }, +}; + +export default RestoreSIWEBackup; diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -11,9 +11,15 @@ import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; +type SIWEBackupData = { + +backupID: string, + +siweBackupMsg: string, +}; + type ClientBackup = { +uploadBackupProtocol: () => Promise, - +restoreBackupProtocol: () => Promise, + +restorePasswordUserBackupProtocol: () => Promise, + +retrieveLatestSIWEBackupData: () => Promise, }; async function getBackupSecret(): Promise { @@ -78,21 +84,60 @@ currentUserInfo, ]); - const restoreBackupProtocol = React.useCallback(async () => { + const restorePasswordUserBackupProtocol = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to restore backup for not logged in user.'); } - console.info('Start restoring backup...'); + if (!accountHasPassword(currentUserInfo)) { + throw new Error( + 'Attempt to restore from password for non-password user.', + ); + } + console.info('Start restoring backup...'); await setMockCommServicesAuthMetadata(); + const backupSecret = await getBackupSecret(); await commCoreModule.restoreBackup(backupSecret); console.info('Backup restored.'); - }, [currentUserID, loggedIn, setMockCommServicesAuthMetadata]); + return; + }, [ + currentUserID, + loggedIn, + setMockCommServicesAuthMetadata, + 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.', + ); + } + + await setMockCommServicesAuthMetadata(); + const serializedBackupData = + await commCoreModule.retrieveLatestSIWEBackupData(); + const siweBackupData: SIWEBackupData = JSON.parse(serializedBackupData); + return siweBackupData; + }, [ + currentUserID, + currentUserInfo, + loggedIn, + setMockCommServicesAuthMetadata, + ]); - return { uploadBackupProtocol, restoreBackupProtocol }; + return { + uploadBackupProtocol, + restorePasswordUserBackupProtocol, + retrieveLatestSIWEBackupData, + }; } export { getBackupSecret, useClientBackup }; diff --git a/native/navigation/root-navigator.react.js b/native/navigation/root-navigator.react.js --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -55,11 +55,13 @@ ConnectFarcasterBottomSheetRouteName, TagFarcasterChannelNavigatorRouteName, CreateMissingSIWEBackupMessageRouteName, + RestoreSIWEBackupRouteName, } from './route-names.js'; import LoggedOutModal from '../account/logged-out-modal.react.js'; import CreateMissingSIWEBackupMessage from '../account/registration/missing-registration-data/missing-siwe-backup-message.react.js'; import RegistrationNavigator from '../account/registration/registration-navigator.react.js'; import TermsAndPrivacyModal from '../account/terms-and-privacy-modal.react.js'; +import RestoreSIWEBackup from '../backup/restore-siwe-backup.react.js'; import ThreadPickerModal from '../calendar/thread-picker-modal.react.js'; import ImagePasteModal from '../chat/image-paste-modal.react.js'; import MessageReactionsModal from '../chat/message-reactions-modal.react.js'; @@ -303,6 +305,10 @@ name={CreateMissingSIWEBackupMessageRouteName} component={CreateMissingSIWEBackupMessage} /> + ); } diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -15,6 +15,7 @@ import type { CreateSIWEBackupMessageParams } from '../account/registration/siwe-backup-message-creation.react.js'; import type { UsernameSelectionParams } from '../account/registration/username-selection.react.js'; import type { TermsAndPrivacyModalParams } from '../account/terms-and-privacy-modal.react.js'; +import type { RestoreSIWEBackupParams } from '../backup/restore-siwe-backup.react.js'; import type { ThreadPickerModalParams } from '../calendar/thread-picker-modal.react.js'; import type { ComposeSubchannelParams } from '../chat/compose-subchannel.react.js'; import type { FullScreenThreadMediaGalleryParams } from '../chat/fullscreen-thread-media-gallery.react.js'; @@ -127,6 +128,7 @@ export const CreateSIWEBackupMessageRouteName = 'CreateSIWEBackupMessage'; export const CreateMissingSIWEBackupMessageRouteName = 'CreateMissingSIWEBackupMessage'; +export const RestoreSIWEBackupRouteName = 'RestoreSIWEBackup'; export const ExistingEthereumAccountRouteName = 'ExistingEthereumAccount'; export const ConnectFarcasterRouteName = 'ConnectFarcaster'; export const UsernameSelectionRouteName = 'UsernameSelection'; @@ -187,6 +189,7 @@ +ConnectFarcasterBottomSheet: void, +TagFarcasterChannelNavigator: void, +CreateMissingSIWEBackupMessage: void, + +RestoreSIWEBackup: RestoreSIWEBackupParams, }; export type MessageTooltipRouteNames = diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,9 +1,11 @@ // @flow +import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Alert, 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'; @@ -11,6 +13,7 @@ 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'; @@ -24,12 +27,18 @@ 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, restoreBackupProtocol } = useClientBackup(); + const { + uploadBackupProtocol, + restorePasswordUserBackupProtocol, + retrieveLatestSIWEBackupData, + } = useClientBackup(); const uploadBackup = React.useCallback(async () => { let message = 'Success'; @@ -42,16 +51,34 @@ Alert.alert('Upload protocol result', message); }, [uploadBackupProtocol]); - const testRestore = React.useCallback(async () => { + const testRestoreForPasswordUser = React.useCallback(async () => { let message = 'success'; try { - await restoreBackupProtocol(); + await restorePasswordUserBackupProtocol(); } catch (e) { message = `Backup restore error: ${String(getMessageForException(e))}`; console.error(message); } Alert.alert('Restore protocol result', message); - }, [restoreBackupProtocol]); + }, [restorePasswordUserBackupProtocol]); + + const testRestoreForSIWEUser = React.useCallback(async () => { + let message = 'success'; + try { + const { backupID, siweBackupMsg } = await retrieveLatestSIWEBackupData(); + + navigation.navigate<'RestoreSIWEBackup'>({ + name: RestoreSIWEBackupRouteName, + params: { + backupID, + siweBackupMsg, + }, + }); + } catch (e) { + message = `Backup restore error: ${String(getMessageForException(e))}`; + console.error(message); + } + }, [navigation, retrieveLatestSIWEBackupData]); const onBackupToggled = React.useCallback( (value: boolean) => { @@ -63,6 +90,10 @@ [dispatch], ); + const onPressRestoreButton = accountHasPassword(currentUserInfo) + ? testRestoreForPasswordUser + : testRestoreForSIWEUser; + return (