diff --git a/native/account/registration/siwe-backup-message-creation.react.js b/native/account/registration/siwe-backup-message-creation.react.js index ed0f50d1a..6234abc3d 100644 --- a/native/account/registration/siwe-backup-message-creation.react.js +++ b/native/account/registration/siwe-backup-message-creation.react.js @@ -1,244 +1,326 @@ // @flow import Icon from '@expo/vector-icons/MaterialIcons.js'; import invariant from 'invariant'; import * as React from 'react'; import { View, Text } from 'react-native'; import { type SIWEResult, SIWEMessageTypes } from 'lib/types/siwe-types.js'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import { type RegistrationNavigationProp } from './registration-navigator.react.js'; import type { CoolOrNerdMode, AccountSelection, AvatarData, } from './registration-types.js'; import { type NavigationRoute, RegistrationTermsRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; import Alert from '../../utils/alert.js'; import { useSIWEPanelState } from '../siwe-hooks.js'; import SIWEPanel from '../siwe-panel.react.js'; const siweBackupSignatureRequestData = { messageType: SIWEMessageTypes.MSG_BACKUP, }; type CreateSIWEBackupMessageBaseProps = { +onSuccessfulWalletSignature: (result: SIWEResult) => void, +onExistingWalletSignature?: () => void, +onSkip?: () => void, }; const CreateSIWEBackupMessageBase: React.ComponentType = React.memo( function CreateSIWEBackupMessageBase( props: CreateSIWEBackupMessageBaseProps, ): React.Node { + const styles = useStyles(unboundStyles); const { onSuccessfulWalletSignature, onExistingWalletSignature, onSkip } = props; - const styles = useStyles(unboundStyles); const { panelState, onPanelClosed, onPanelClosing, openPanel, siwePanelSetLoading, } = useSIWEPanelState(); let siwePanel; if (panelState !== 'closed') { siwePanel = ( ); } const newSignatureButtonText = onExistingWalletSignature ? 'Encrypt with new signature' : 'Encrypt with Ethereum signature'; const newSignatureButtonVariant = onExistingWalletSignature ? 'outline' : 'enabled'; let useExistingSignatureButton; if (onExistingWalletSignature) { useExistingSignatureButton = ( ); } let onSkipButton; if (onSkip) { onSkipButton = ( ); } return ( <> Encrypting your Comm backup To make sure we can’t see your data, Comm encrypts your backup using a signature from your wallet. This signature is private and never leaves your device, unlike the prior signature, which is public. This signature ensures that you can always recover your data as long as you still control your wallet. {useExistingSignatureButton} {onSkipButton} {siwePanel} ); }, ); export type CreateSIWEBackupMessageParams = { +userSelections: { +coolOrNerdMode?: ?CoolOrNerdMode, +keyserverURL?: ?string, +farcasterID: ?string, +accountSelection: AccountSelection, +avatarData: ?AvatarData, }, }; type Props = { +navigation: RegistrationNavigationProp<'CreateSIWEBackupMessage'>, +route: NavigationRoute<'CreateSIWEBackupMessage'>, }; function CreateSIWEBackupMessage(props: Props): React.Node { const { navigate } = props.navigation; const { params } = props.route; const { userSelections } = params; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const onSuccessfulWalletSignature = React.useCallback( (result: SIWEResult) => { const selectedEthereumAddress = userSelections.accountSelection.address; const { message, signature, address } = result; if (address !== selectedEthereumAddress) { Alert.alert( 'Mismatched Ethereum address', 'You picked a different wallet than the one you use to sign in.', ); return; } const newUserSelections = { ...userSelections, siweBackupSecrets: { message, signature }, }; setCachedSelections(oldUserSelections => ({ ...oldUserSelections, siweBackupSecrets: { message, signature }, })); navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, params: { userSelections: newUserSelections }, }); }, [navigate, setCachedSelections, userSelections], ); const { siweBackupSecrets } = cachedSelections; const onExistingWalletSignature = React.useCallback(() => { const registrationTermsParams = { userSelections: { ...userSelections, siweBackupSecrets, }, }; navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, params: registrationTermsParams, }); }, [navigate, siweBackupSecrets, userSelections]); if (siweBackupSecrets) { return ( ); } return ( ); } +type SignSIWEBackupMessageForRestoreBaseProps = { + +siweNonce: string, + +siweIssuedAt: string, + +siweStatement: 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 { + siweIssuedAt, + siweNonce, + siweStatement, + onSuccessfulWalletSignature, + onSkip, + } = props; + const siweSignatureRequestData = React.useMemo( + () => ({ + messageType: SIWEMessageTypes.MSG_BACKUP_RESTORE, + siweNonce, + siweStatement, + siweIssuedAt, + }), + [siweStatement, siweIssuedAt, siweNonce], + ); + + let siwePanel; + if (panelState !== 'closed') { + siwePanel = ( + + ); + } + + return ( + <> + + + Decrypting your Comm backup + + To make sure we can’t see your data, Comm encrypts your backup using + a signature from your wallet. + + + + + + + + + + + {siwePanel} + + ); +} + const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, }, body: { fontFamily: 'Arial', fontSize: 15, lineHeight: 20, color: 'panelForegroundSecondaryLabel', paddingBottom: 16, }, siweBackupIcon: { color: 'panelForegroundIcon', }, siweBackupIconContainer: { flexGrow: 1, alignItems: 'center', justifyContent: 'center', }, }; -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 index 000000000..623daba2c --- /dev/null +++ b/native/backup/restore-siwe-backup.react.js @@ -0,0 +1,78 @@ +// @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, + +siweNonce: string, + +siweStatement: string, + +siweIssuedAt: 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, siweStatement, siweIssuedAt, siweNonce }, + } = 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 ( + + + + ); +} + +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 index 35741667a..c1939a6ef 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,98 +1,146 @@ // @flow import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import type { SIWEBackupSecrets } from 'lib/types/siwe-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { fetchNativeKeychainCredentials } from '../account/native-credentials.js'; import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; +type SIWEBackupData = { + +backupID: string, + +siweBackupMsg: string, + +siweBackupMsgNonce: string, + +siweBackupMsgStatement: string, + +siweBackupMsgIssuedAt: string, +}; + type ClientBackup = { +uploadBackupProtocol: () => Promise, - +restoreBackupProtocol: () => Promise, + +restorePasswordUserBackupProtocol: () => Promise, + +retrieveLatestSIWEBackupData: () => 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 accessToken = useSelector(state => state.commServicesAccessToken); const currentUserID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const currentUserInfo = useSelector(state => state.currentUserInfo); const loggedIn = useSelector(isLoggedIn); const setMockCommServicesAuthMetadata = React.useCallback(async () => { if (!currentUserID) { return; } const ed25519 = await getContentSigningKey(); await commCoreModule.setCommServicesAuthMetadata( currentUserID, ed25519, accessToken ? accessToken : '', ); }, [accessToken, currentUserID]); 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...'); await setMockCommServicesAuthMetadata(); 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, setMockCommServicesAuthMetadata, 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 index 0bbbd25ae..d3d15fb7e 100644 --- a/native/navigation/root-navigator.react.js +++ b/native/navigation/root-navigator.react.js @@ -1,309 +1,315 @@ // @flow import type { StackNavigationState, StackOptions, StackNavigationEventMap, StackNavigatorProps, ExtraStackNavigatorProps, ParamListBase, StackNavigationHelpers, StackNavigationProp, StackRouterOptions, RouteProp, } from '@react-navigation/core'; import { createNavigatorFactory, useNavigationBuilder, } from '@react-navigation/native'; import { StackView } from '@react-navigation/stack'; import * as React from 'react'; import { Platform } from 'react-native'; import { enableScreens } from 'react-native-screens'; import AppNavigator from './app-navigator.react.js'; import InviteLinkModal from './invite-link-modal.react.js'; import { defaultStackScreenOptions, transitionPreset } from './options.js'; import { RootNavigatorContext } from './root-navigator-context.js'; import RootRouter, { type RootRouterExtraNavigationHelpers, type RootRouterNavigationAction, } from './root-router.js'; import { LoggedOutModalRouteName, AppRouteName, ThreadPickerModalRouteName, ImagePasteModalRouteName, AddUsersModalRouteName, CustomServerModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, SidebarListModalRouteName, SubchannelsListModalRouteName, MessageReactionsModalRouteName, type ScreenParamList, type RootParamList, TermsAndPrivacyRouteName, RegistrationRouteName, InviteLinkModalRouteName, InviteLinkNavigatorRouteName, CommunityCreationRouteName, RolesNavigatorRouteName, QRCodeSignInNavigatorRouteName, UserProfileBottomSheetNavigatorRouteName, KeyserverSelectionBottomSheetRouteName, 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'; import AddUsersModal from '../chat/settings/add-users-modal.react.js'; import ColorSelectorModal from '../chat/settings/color-selector-modal.react.js'; import ComposeSubchannelModal from '../chat/settings/compose-subchannel-modal.react.js'; import SidebarListModal from '../chat/sidebar-list-modal.react.js'; import SubchannelsListModal from '../chat/subchannels-list-modal.react.js'; import CommunityCreationNavigator from '../community-creation/community-creation-navigator.react.js'; import TagFarcasterChannelNavigator from '../community-settings/tag-farcaster-channel/tag-farcaster-channel-navigator.react.js'; import ConnectFarcasterBottomSheet from '../components/connect-farcaster-bottom-sheet.react.js'; import InviteLinksNavigator from '../invite-links/invite-links-navigator.react.js'; import CustomServerModal from '../profile/custom-server-modal.react.js'; import KeyserverSelectionBottomSheet from '../profile/keyserver-selection-bottom-sheet.react.js'; import QRCodeSignInNavigator from '../qr-code/qr-code-sign-in-navigator.react.js'; import RolesNavigator from '../roles/roles-navigator.react.js'; import UserProfileBottomSheetNavigator from '../user-profile/user-profile-bottom-sheet-navigator.react.js'; enableScreens(); export type RootNavigationHelpers = { ...$Exact>, ...RootRouterExtraNavigationHelpers, ... }; type RootNavigatorProps = StackNavigatorProps>; function RootNavigator({ initialRouteName, children, screenOptions, defaultScreenOptions, screenListeners, id, ...rest }: RootNavigatorProps) { const [keyboardHandlingEnabled, setKeyboardHandlingEnabled] = React.useState(true); const mergedScreenOptions = React.useMemo(() => { if (typeof screenOptions === 'function') { return (input: { +route: RouteProp<>, +navigation: RootNavigationHelpers<>, }) => ({ ...screenOptions(input), keyboardHandlingEnabled, }); } return { ...screenOptions, keyboardHandlingEnabled, }; }, [screenOptions, keyboardHandlingEnabled]); const { state, descriptors, navigation } = useNavigationBuilder< StackNavigationState, RootRouterNavigationAction, StackOptions, StackRouterOptions, RootNavigationHelpers<>, StackNavigationEventMap, ExtraStackNavigatorProps, >(RootRouter, { id, initialRouteName, children, screenOptions: mergedScreenOptions, defaultScreenOptions, screenListeners, }); const rootNavigationContext = React.useMemo( () => ({ setKeyboardHandlingEnabled }), [setKeyboardHandlingEnabled], ); return ( ); } const createRootNavigator = createNavigatorFactory< StackNavigationState, StackOptions, StackNavigationEventMap, RootNavigationHelpers<>, ExtraStackNavigatorProps, >(RootNavigator); const defaultScreenOptions = { ...defaultStackScreenOptions, ...transitionPreset, cardStyle: { backgroundColor: 'transparent' }, presentation: 'modal', headerShown: false, }; const disableGesturesScreenOptions = { gestureEnabled: false, }; const modalOverlayScreenOptions = { cardOverlayEnabled: true, presentation: 'transparentModal', }; const termsAndPrivacyModalScreenOptions = { gestureEnabled: false, cardOverlayEnabled: true, presentation: 'transparentModal', }; export type RootRouterNavigationProp< ParamList: ParamListBase = ParamListBase, RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; export type RootNavigationProp< RouteName: $Keys = $Keys, > = { ...StackNavigationProp, ...RootRouterExtraNavigationHelpers, }; const Root = createRootNavigator< ScreenParamList, RootParamList, RootNavigationHelpers, >(); function RootComponent(): React.Node { return ( + ); } export default RootComponent; diff --git a/native/navigation/route-names.js b/native/navigation/route-names.js index a5ea2309d..94cae8482 100644 --- a/native/navigation/route-names.js +++ b/native/navigation/route-names.js @@ -1,368 +1,371 @@ // @flow import type { RouteProp } from '@react-navigation/core'; import type { ActionResultModalParams } from './action-result-modal.react.js'; import type { InviteLinkModalParams } from './invite-link-modal.react'; import type { AvatarSelectionParams } from '../account/registration/avatar-selection.react.js'; import type { ConnectEthereumParams } from '../account/registration/connect-ethereum.react.js'; import type { ConnectFarcasterParams } from '../account/registration/connect-farcaster.react.js'; import type { EmojiAvatarSelectionParams } from '../account/registration/emoji-avatar-selection.react.js'; import type { ExistingEthereumAccountParams } from '../account/registration/existing-ethereum-account.react.js'; import type { KeyserverSelectionParams } from '../account/registration/keyserver-selection.react.js'; import type { PasswordSelectionParams } from '../account/registration/password-selection.react.js'; import type { RegistrationTermsParams } from '../account/registration/registration-terms.react.js'; 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'; import type { ImagePasteModalParams } from '../chat/image-paste-modal.react.js'; import type { MessageListParams } from '../chat/message-list-types.js'; import type { MessageReactionsModalParams } from '../chat/message-reactions-modal.react.js'; import type { MultimediaMessageTooltipModalParams } from '../chat/multimedia-message-tooltip-modal.react.js'; import type { PinnedMessagesScreenParams } from '../chat/pinned-messages-screen.react.js'; import type { RobotextMessageTooltipModalParams } from '../chat/robotext-message-tooltip-modal.react.js'; import type { AddUsersModalParams } from '../chat/settings/add-users-modal.react.js'; import type { ColorSelectorModalParams } from '../chat/settings/color-selector-modal.react.js'; import type { ComposeSubchannelModalParams } from '../chat/settings/compose-subchannel-modal.react.js'; import type { DeleteThreadParams } from '../chat/settings/delete-thread.react.js'; import type { EmojiThreadAvatarCreationParams } from '../chat/settings/emoji-thread-avatar-creation.react.js'; import type { ThreadSettingsMemberTooltipModalParams } from '../chat/settings/thread-settings-member-tooltip-modal.react.js'; import type { ThreadSettingsParams } from '../chat/settings/thread-settings.react.js'; import type { SidebarListModalParams } from '../chat/sidebar-list-modal.react.js'; import type { SubchannelListModalParams } from '../chat/subchannels-list-modal.react.js'; import type { TextMessageTooltipModalParams } from '../chat/text-message-tooltip-modal.react.js'; import type { TogglePinModalParams } from '../chat/toggle-pin-modal.react.js'; import type { CommunityCreationMembersScreenParams } from '../community-creation/community-creation-members.react.js'; import type { TagFarcasterChannelByNameParams } from '../community-settings/tag-farcaster-channel/tag-farcaster-channel-by-name.react.js'; import type { TagFarcasterChannelParams } from '../community-settings/tag-farcaster-channel/tag-farcaster-channel.react.js'; import type { ManagePublicLinkScreenParams } from '../invite-links/manage-public-link-screen.react.js'; import type { ViewInviteLinksScreenParams } from '../invite-links/view-invite-links-screen.react.js'; import type { ChatCameraModalParams } from '../media/chat-camera-modal.react.js'; import type { ImageModalParams } from '../media/image-modal.react.js'; import type { ThreadAvatarCameraModalParams } from '../media/thread-avatar-camera-modal.react.js'; import type { VideoPlaybackModalParams } from '../media/video-playback-modal.react.js'; import type { CustomServerModalParams } from '../profile/custom-server-modal.react.js'; import type { KeyserverSelectionBottomSheetParams } from '../profile/keyserver-selection-bottom-sheet.react.js'; import type { UserRelationshipTooltipModalParams } from '../profile/user-relationship-tooltip-modal.react.js'; import type { ChangeRolesScreenParams } from '../roles/change-roles-screen.react.js'; import type { CommunityRolesScreenParams } from '../roles/community-roles-screen.react.js'; import type { CreateRolesScreenParams } from '../roles/create-roles-screen.react.js'; import type { MessageSearchParams } from '../search/message-search.react.js'; import type { UserProfileAvatarModalParams } from '../user-profile/user-profile-avatar-modal.react.js'; import type { UserProfileBottomSheetParams } from '../user-profile/user-profile-bottom-sheet.react.js'; export const ActionResultModalRouteName = 'ActionResultModal'; export const AddUsersModalRouteName = 'AddUsersModal'; export const AppearancePreferencesRouteName = 'AppearancePreferences'; export const AppRouteName = 'App'; export const AppsRouteName = 'Apps'; export const BackgroundChatThreadListRouteName = 'BackgroundChatThreadList'; export const BackupMenuRouteName = 'BackupMenu'; export const BlockListRouteName = 'BlockList'; export const BuildInfoRouteName = 'BuildInfo'; export const CalendarRouteName = 'Calendar'; export const ChangeRolesScreenRouteName = 'ChangeRolesScreen'; export const ChatCameraModalRouteName = 'ChatCameraModal'; export const ChatRouteName = 'Chat'; export const ChatThreadListRouteName = 'ChatThreadList'; export const ColorSelectorModalRouteName = 'ColorSelectorModal'; export const ComposeSubchannelModalRouteName = 'ComposeSubchannelModal'; export const ComposeSubchannelRouteName = 'ComposeSubchannel'; export const CommunityDrawerNavigatorRouteName = 'CommunityDrawerNavigator'; export const CustomServerModalRouteName = 'CustomServerModal'; export const DefaultNotificationsPreferencesRouteName = 'DefaultNotifications'; export const DeleteAccountRouteName = 'DeleteAccount'; export const DeleteThreadRouteName = 'DeleteThread'; export const DevToolsRouteName = 'DevTools'; export const EditPasswordRouteName = 'EditPassword'; export const EmojiThreadAvatarCreationRouteName = 'EmojiThreadAvatarCreation'; export const EmojiUserAvatarCreationRouteName = 'EmojiUserAvatarCreation'; export const FriendListRouteName = 'FriendList'; export const FullScreenThreadMediaGalleryRouteName = 'FullScreenThreadMediaGallery'; export const HomeChatThreadListRouteName = 'HomeChatThreadList'; export const ImageModalRouteName = 'ImageModal'; export const ImagePasteModalRouteName = 'ImagePasteModal'; export const InviteLinkModalRouteName = 'InviteLinkModal'; export const InviteLinkNavigatorRouteName = 'InviteLinkNavigator'; export const LinkedDevicesRouteName = 'LinkedDevices'; export const LoggedOutModalRouteName = 'LoggedOutModal'; export const ManagePublicLinkRouteName = 'ManagePublicLink'; export const MessageListRouteName = 'MessageList'; export const MessageReactionsModalRouteName = 'MessageReactionsModal'; export const PinnedMessagesScreenRouteName = 'PinnedMessagesScreen'; export const MultimediaMessageTooltipModalRouteName = 'MultimediaMessageTooltipModal'; export const PrivacyPreferencesRouteName = 'PrivacyPreferences'; export const ProfileRouteName = 'Profile'; export const ProfileScreenRouteName = 'ProfileScreen'; export const UserRelationshipTooltipModalRouteName = 'UserRelationshipTooltipModal'; export const RobotextMessageTooltipModalRouteName = 'RobotextMessageTooltipModal'; export const SecondaryDeviceQRCodeScannerRouteName = 'SecondaryDeviceQRCodeScanner'; export const SidebarListModalRouteName = 'SidebarListModal'; export const SubchannelsListModalRouteName = 'SubchannelsListModal'; export const TabNavigatorRouteName = 'TabNavigator'; export const TextMessageTooltipModalRouteName = 'TextMessageTooltipModal'; export const ThreadAvatarCameraModalRouteName = 'ThreadAvatarCameraModal'; export const ThreadPickerModalRouteName = 'ThreadPickerModal'; export const ThreadSettingsMemberTooltipModalRouteName = 'ThreadSettingsMemberTooltipModal'; export const ThreadSettingsRouteName = 'ThreadSettings'; export const TunnelbrokerMenuRouteName = 'TunnelbrokerMenu'; export const UserAvatarCameraModalRouteName = 'UserAvatarCameraModal'; export const TogglePinModalRouteName = 'TogglePinModal'; export const VideoPlaybackModalRouteName = 'VideoPlaybackModal'; export const ViewInviteLinksRouteName = 'ViewInviteLinks'; export const TermsAndPrivacyRouteName = 'TermsAndPrivacyModal'; export const RegistrationRouteName = 'Registration'; export const KeyserverSelectionRouteName = 'KeyserverSelection'; export const CoolOrNerdModeSelectionRouteName = 'CoolOrNerdModeSelection'; export const ConnectEthereumRouteName = 'ConnectEthereum'; export const CreateSIWEBackupMessageRouteName = 'CreateSIWEBackupMessage'; export const CreateMissingSIWEBackupMessageRouteName = 'CreateMissingSIWEBackupMessage'; +export const RestoreSIWEBackupRouteName = 'RestoreSIWEBackup'; export const ExistingEthereumAccountRouteName = 'ExistingEthereumAccount'; export const ConnectFarcasterRouteName = 'ConnectFarcaster'; export const UsernameSelectionRouteName = 'UsernameSelection'; export const CommunityCreationRouteName = 'CommunityCreation'; export const CommunityConfigurationRouteName = 'CommunityConfiguration'; export const CommunityCreationMembersRouteName = 'CommunityCreationMembers'; export const MessageSearchRouteName = 'MessageSearch'; export const PasswordSelectionRouteName = 'PasswordSelection'; export const AvatarSelectionRouteName = 'AvatarSelection'; export const EmojiAvatarSelectionRouteName = 'EmojiAvatarSelection'; export const RegistrationUserAvatarCameraModalRouteName = 'RegistrationUserAvatarCameraModal'; export const RegistrationTermsRouteName = 'RegistrationTerms'; export const RolesNavigatorRouteName = 'RolesNavigator'; export const CommunityRolesScreenRouteName = 'CommunityRolesScreen'; export const CreateRolesScreenRouteName = 'CreateRolesScreen'; export const QRCodeSignInNavigatorRouteName = 'QRCodeSignInNavigator'; export const QRCodeScreenRouteName = 'QRCodeScreen'; export const UserProfileBottomSheetNavigatorRouteName = 'UserProfileBottomSheetNavigator'; export const UserProfileBottomSheetRouteName = 'UserProfileBottomSheet'; export const UserProfileAvatarModalRouteName = 'UserProfileAvatarModal'; export const KeyserverSelectionListRouteName = 'KeyserverSelectionList'; export const AddKeyserverRouteName = 'AddKeyserver'; export const KeyserverSelectionBottomSheetRouteName = 'KeyserverSelectionBottomSheet'; export const AccountDoesNotExistRouteName = 'AccountDoesNotExist'; export const FarcasterAccountSettingsRouteName = 'FarcasterAccountSettings'; export const ConnectFarcasterBottomSheetRouteName = 'ConnectFarcasterBottomSheet'; export const TagFarcasterChannelNavigatorRouteName = 'TagFarcasterChannelNavigator'; export const TagFarcasterChannelRouteName = 'TagFarcasterChannel'; export const TagFarcasterChannelByNameRouteName = 'TagFarcasterChannelByName'; export type RootParamList = { +LoggedOutModal: void, +App: void, +ThreadPickerModal: ThreadPickerModalParams, +AddUsersModal: AddUsersModalParams, +CustomServerModal: CustomServerModalParams, +ColorSelectorModal: ColorSelectorModalParams, +ComposeSubchannelModal: ComposeSubchannelModalParams, +SidebarListModal: SidebarListModalParams, +ImagePasteModal: ImagePasteModalParams, +TermsAndPrivacyModal: TermsAndPrivacyModalParams, +SubchannelsListModal: SubchannelListModalParams, +MessageReactionsModal: MessageReactionsModalParams, +Registration: void, +CommunityCreation: void, +InviteLinkModal: InviteLinkModalParams, +InviteLinkNavigator: void, +RolesNavigator: void, +QRCodeSignInNavigator: void, +UserProfileBottomSheetNavigator: void, +TunnelbrokerMenu: void, +KeyserverSelectionBottomSheet: KeyserverSelectionBottomSheetParams, +ConnectFarcasterBottomSheet: void, +TagFarcasterChannelNavigator: void, +CreateMissingSIWEBackupMessage: void, + +RestoreSIWEBackup: RestoreSIWEBackupParams, }; export type MessageTooltipRouteNames = | typeof RobotextMessageTooltipModalRouteName | typeof MultimediaMessageTooltipModalRouteName | typeof TextMessageTooltipModalRouteName; export const PinnableMessageTooltipRouteNames = [ TextMessageTooltipModalRouteName, MultimediaMessageTooltipModalRouteName, ]; export type TooltipModalParamList = { +MultimediaMessageTooltipModal: MultimediaMessageTooltipModalParams, +TextMessageTooltipModal: TextMessageTooltipModalParams, +ThreadSettingsMemberTooltipModal: ThreadSettingsMemberTooltipModalParams, +UserRelationshipTooltipModal: UserRelationshipTooltipModalParams, +RobotextMessageTooltipModal: RobotextMessageTooltipModalParams, }; export type OverlayParamList = { +CommunityDrawerNavigator: void, +ImageModal: ImageModalParams, +ActionResultModal: ActionResultModalParams, +ChatCameraModal: ChatCameraModalParams, +UserAvatarCameraModal: void, +ThreadAvatarCameraModal: ThreadAvatarCameraModalParams, +VideoPlaybackModal: VideoPlaybackModalParams, +TogglePinModal: TogglePinModalParams, ...TooltipModalParamList, }; export type TabParamList = { +Calendar: void, +Chat: void, +Profile: void, +Apps: void, }; export type ChatParamList = { +ChatThreadList: void, +MessageList: MessageListParams, +ComposeSubchannel: ComposeSubchannelParams, +ThreadSettings: ThreadSettingsParams, +EmojiThreadAvatarCreation: EmojiThreadAvatarCreationParams, +DeleteThread: DeleteThreadParams, +FullScreenThreadMediaGallery: FullScreenThreadMediaGalleryParams, +PinnedMessagesScreen: PinnedMessagesScreenParams, +MessageSearch: MessageSearchParams, +ChangeRolesScreen: ChangeRolesScreenParams, }; export type ChatTopTabsParamList = { +HomeChatThreadList: void, +BackgroundChatThreadList: void, }; export type ProfileParamList = { +ProfileScreen: void, +EmojiUserAvatarCreation: void, +EditPassword: void, +DeleteAccount: void, +BuildInfo: void, +DevTools: void, +AppearancePreferences: void, +PrivacyPreferences: void, +DefaultNotifications: void, +FriendList: void, +BlockList: void, +LinkedDevices: void, +SecondaryDeviceQRCodeScanner: void, +BackupMenu: void, +TunnelbrokerMenu: void, +KeyserverSelectionList: void, +AddKeyserver: void, +FarcasterAccountSettings: void, }; export type CommunityDrawerParamList = { +TabNavigator: void }; export type RegistrationParamList = { +CoolOrNerdModeSelection: void, +KeyserverSelection: KeyserverSelectionParams, +ConnectEthereum: ConnectEthereumParams, +ExistingEthereumAccount: ExistingEthereumAccountParams, +ConnectFarcaster: ConnectFarcasterParams, +CreateSIWEBackupMessage: CreateSIWEBackupMessageParams, +UsernameSelection: UsernameSelectionParams, +PasswordSelection: PasswordSelectionParams, +AvatarSelection: AvatarSelectionParams, +EmojiAvatarSelection: EmojiAvatarSelectionParams, +RegistrationUserAvatarCameraModal: void, +RegistrationTerms: RegistrationTermsParams, +AccountDoesNotExist: void, }; export type InviteLinkParamList = { +ViewInviteLinks: ViewInviteLinksScreenParams, +ManagePublicLink: ManagePublicLinkScreenParams, }; export type CommunityCreationParamList = { +CommunityConfiguration: void, +CommunityCreationMembers: CommunityCreationMembersScreenParams, }; export type RolesParamList = { +CommunityRolesScreen: CommunityRolesScreenParams, +CreateRolesScreen: CreateRolesScreenParams, }; export type TagFarcasterChannelParamList = { +TagFarcasterChannel: TagFarcasterChannelParams, +TagFarcasterChannelByName: TagFarcasterChannelByNameParams, }; export type QRCodeSignInParamList = { +QRCodeScreen: void, }; export type UserProfileBottomSheetParamList = { +UserProfileBottomSheet: UserProfileBottomSheetParams, +UserProfileAvatarModal: UserProfileAvatarModalParams, +UserRelationshipTooltipModal: UserRelationshipTooltipModalParams, }; export type ScreenParamList = { ...RootParamList, ...OverlayParamList, ...TabParamList, ...ChatParamList, ...ChatTopTabsParamList, ...ProfileParamList, ...CommunityDrawerParamList, ...RegistrationParamList, ...InviteLinkParamList, ...CommunityCreationParamList, ...RolesParamList, ...QRCodeSignInParamList, ...UserProfileBottomSheetParamList, ...TagFarcasterChannelParamList, }; export type NavigationRoute> = RouteProp; export const accountModals = [ LoggedOutModalRouteName, RegistrationRouteName, QRCodeSignInNavigatorRouteName, ]; export const scrollBlockingModals = [ ImageModalRouteName, MultimediaMessageTooltipModalRouteName, TextMessageTooltipModalRouteName, ThreadSettingsMemberTooltipModalRouteName, UserRelationshipTooltipModalRouteName, RobotextMessageTooltipModalRouteName, VideoPlaybackModalRouteName, ]; export const chatRootModals = [ AddUsersModalRouteName, ColorSelectorModalRouteName, ComposeSubchannelModalRouteName, ]; export const threadRoutes = [ MessageListRouteName, ThreadSettingsRouteName, DeleteThreadRouteName, ComposeSubchannelRouteName, FullScreenThreadMediaGalleryRouteName, PinnedMessagesScreenRouteName, MessageSearchRouteName, EmojiThreadAvatarCreationRouteName, CommunityRolesScreenRouteName, ]; diff --git a/native/profile/backup-menu.react.js b/native/profile/backup-menu.react.js index 131f92327..3f2506ec0 100644 --- a/native/profile/backup-menu.react.js +++ b/native/profile/backup-menu.react.js @@ -1,148 +1,188 @@ // @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, restoreBackupProtocol } = useClientBackup(); + const { + uploadBackupProtocol, + restorePasswordUserBackupProtocol, + retrieveLatestSIWEBackupData, + } = 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 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 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;