diff --git a/native/backup/backup-handler.js b/native/backup/backup-handler.js index 75cb418ce..b9eb7f25d 100644 --- a/native/backup/backup-handler.js +++ b/native/backup/backup-handler.js @@ -1,54 +1,44 @@ // @flow import * as React from 'react'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; -import { accountHasPassword } from 'lib/shared/account-utils.js'; import { commCoreModule } from '../native-modules.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function BackupHandler(): null { const isBackupEnabled = useSelector( state => state.localSettings.isBackupEnabled, ); const loggedIn = useSelector(isLoggedIn); const staffCanSee = useStaffCanSee(); - const isAccountWithPassword = useSelector(state => - accountHasPassword(state.currentUserInfo), - ); const isBackground = useSelector( state => state.lifecycleState === 'background', ); React.useEffect(() => { - if (!staffCanSee || !isAccountWithPassword) { + if (!staffCanSee) { return; } if (isBackupEnabled && loggedIn && !isBackground) { try { commCoreModule.startBackupHandler(); } catch (err) { console.log('Error starting backup handler:', err); } } else { try { commCoreModule.stopBackupHandler(); } catch (err) { console.log('Error stopping backup handler:', err); } } - }, [ - isBackupEnabled, - staffCanSee, - loggedIn, - isAccountWithPassword, - isBackground, - ]); + }, [isBackupEnabled, staffCanSee, loggedIn, isBackground]); return null; } export default BackupHandler; diff --git a/native/backup/use-client-backup.js b/native/backup/use-client-backup.js index 861101f28..35741667a 100644 --- a/native/backup/use-client-backup.js +++ b/native/backup/use-client-backup.js @@ -1,76 +1,98 @@ // @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 ClientBackup = { +uploadBackupProtocol: () => Promise, +restoreBackupProtocol: () => 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(); - const backupSecret = await getBackupSecret(); - await commCoreModule.createNewBackup(backupSecret); + + 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]); + }, [ + currentUserID, + loggedIn, + setMockCommServicesAuthMetadata, + currentUserInfo, + ]); const restoreBackupProtocol = React.useCallback(async () => { if (!loggedIn || !currentUserID) { throw new Error('Attempt to restore backup for not logged in user.'); } console.info('Start restoring backup...'); await setMockCommServicesAuthMetadata(); const backupSecret = await getBackupSecret(); await commCoreModule.restoreBackup(backupSecret); console.info('Backup restored.'); }, [currentUserID, loggedIn, setMockCommServicesAuthMetadata]); return { uploadBackupProtocol, restoreBackupProtocol }; } export { getBackupSecret, useClientBackup }; diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js index 33d35ce7d..ba47e612f 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,476 +1,476 @@ // @flow import * as React from 'react'; import { View, Text, Platform, ScrollView } from 'react-native'; import { logOutActionTypes, useLogOut } from 'lib/actions/user-actions.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { useDispatchActionPromise, type DispatchActionPromise, } from 'lib/utils/redux-promise-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import { deleteNativeCredentialsFor } from '../account/native-credentials.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Action from '../components/action-row.react.js'; import Button from '../components/button.react.js'; import EditSettingButton from '../components/edit-setting-button.react.js'; import SingleLine from '../components/single-line.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { EditPasswordRouteName, DeleteAccountRouteName, BuildInfoRouteName, DevToolsRouteName, AppearancePreferencesRouteName, FriendListRouteName, BlockListRouteName, PrivacyPreferencesRouteName, DefaultNotificationsPreferencesRouteName, LinkedDevicesRouteName, BackupMenuRouteName, KeyserverSelectionListRouteName, TunnelbrokerMenuRouteName, FarcasterAccountSettingsRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type Colors, useColors, useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, }; function ProfileRow(props: ProfileRowProps): React.Node { const { content, onPress, danger } = props; return ( ); } const unboundStyles = { avatarSection: { alignItems: 'center', paddingVertical: 16, }, container: { flex: 1, }, content: { flex: 1, }, deleteAccountButton: { paddingHorizontal: 24, paddingVertical: 12, }, editPasswordButton: { paddingTop: Platform.OS === 'android' ? 3 : 2, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, paddingRight: 12, }, loggedInLabel: { color: 'panelForegroundTertiaryLabel', fontSize: 16, }, logOutText: { color: 'link', fontSize: 16, paddingLeft: 6, }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', }, scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, paddedRow: { flex: 1, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 24, paddingVertical: 10, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, paddingVertical: 1, }, unpaddedSection: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, }, username: { color: 'panelForegroundLabel', flex: 1, }, value: { color: 'panelForegroundLabel', fontSize: 16, textAlign: 'right', }, }; type BaseProps = { +navigation: ProfileNavigationProp<'ProfileScreen'>, +route: NavigationRoute<'ProfileScreen'>, }; type Props = { ...BaseProps, +currentUserInfo: ?CurrentUserInfo, +logOutLoading: boolean, +colors: Colors, +styles: $ReadOnly, +dispatchActionPromise: DispatchActionPromise, +logOut: () => Promise, +staffCanSee: boolean, +stringForUser: ?string, +isAccountWithPassword: boolean, }; class ProfileScreen extends React.PureComponent { get loggedOutOrLoggingOut(): boolean { return ( !this.props.currentUserInfo || this.props.currentUserInfo.anonymous || this.props.logOutLoading ); } render(): React.Node { let developerTools, defaultNotifications, keyserverSelection, tunnelbrokerMenu; - const { staffCanSee, isAccountWithPassword } = this.props; + const { staffCanSee } = this.props; if (staffCanSee) { developerTools = ( ); defaultNotifications = ( ); keyserverSelection = ( ); tunnelbrokerMenu = ( ); } let backupMenu; - if (staffCanSee && isAccountWithPassword) { + if (staffCanSee) { backupMenu = ( ); } let passwordEditionUI; if (accountHasPassword(this.props.currentUserInfo)) { passwordEditionUI = ( Password •••••••••••••••• ); } let linkedDevices; if (__DEV__) { linkedDevices = ( ); } let farcasterAccountSettings; if (usingCommServicesAccessToken || __DEV__) { farcasterAccountSettings = ( ); } return ( USER AVATAR ACCOUNT Logged in as {this.props.stringForUser} {passwordEditionUI} PREFERENCES {defaultNotifications} {backupMenu} {tunnelbrokerMenu} {farcasterAccountSettings} {linkedDevices} {keyserverSelection} {developerTools} ); } onPressLogOut = () => { if (this.loggedOutOrLoggingOut) { return; } if (!this.props.isAccountWithPassword) { Alert.alert( 'Log out', 'Are you sure you want to log out?', [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); return; } const alertTitle = Platform.OS === 'ios' ? 'Keep Login Info in Keychain' : 'Keep Login Info'; const alertDescription = 'We will automatically fill out log-in forms with your credentials ' + 'in the app.'; Alert.alert( alertTitle, alertDescription, [ { text: 'Cancel', style: 'cancel' }, { text: 'Keep', onPress: this.logOutWithoutDeletingNativeCredentialsWrapper, }, { text: 'Remove', onPress: this.logOutAndDeleteNativeCredentialsWrapper, style: 'destructive', }, ], { cancelable: true }, ); }; logOutWithoutDeletingNativeCredentialsWrapper = () => { if (this.loggedOutOrLoggingOut) { return; } this.logOut(); }; logOutAndDeleteNativeCredentialsWrapper = async () => { if (this.loggedOutOrLoggingOut) { return; } await this.deleteNativeCredentials(); this.logOut(); }; logOut() { void this.props.dispatchActionPromise( logOutActionTypes, this.props.logOut(), ); } async deleteNativeCredentials() { await deleteNativeCredentialsFor(); } onPressEditPassword = () => { this.props.navigation.navigate({ name: EditPasswordRouteName }); }; onPressDeleteAccount = () => { this.props.navigation.navigate({ name: DeleteAccountRouteName }); }; onPressFaracsterAccount = () => { this.props.navigation.navigate({ name: FarcasterAccountSettingsRouteName }); }; onPressDevices = () => { this.props.navigation.navigate({ name: LinkedDevicesRouteName }); }; onPressBuildInfo = () => { this.props.navigation.navigate({ name: BuildInfoRouteName }); }; onPressDevTools = () => { this.props.navigation.navigate({ name: DevToolsRouteName }); }; onPressAppearance = () => { this.props.navigation.navigate({ name: AppearancePreferencesRouteName }); }; onPressPrivacy = () => { this.props.navigation.navigate({ name: PrivacyPreferencesRouteName }); }; onPressDefaultNotifications = () => { this.props.navigation.navigate({ name: DefaultNotificationsPreferencesRouteName, }); }; onPressFriendList = () => { this.props.navigation.navigate({ name: FriendListRouteName }); }; onPressBlockList = () => { this.props.navigation.navigate({ name: BlockListRouteName }); }; onPressBackupMenu = () => { this.props.navigation.navigate({ name: BackupMenuRouteName }); }; onPressTunnelbrokerMenu = () => { this.props.navigation.navigate({ name: TunnelbrokerMenuRouteName }); }; onPressKeyserverSelection = () => { this.props.navigation.navigate({ name: KeyserverSelectionListRouteName }); }; } const logOutLoadingStatusSelector = createLoadingStatusSelector(logOutActionTypes); const ConnectedProfileScreen: React.ComponentType = React.memo(function ConnectedProfileScreen(props: BaseProps) { const currentUserInfo = useSelector(state => state.currentUserInfo); const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; const colors = useColors(); const styles = useStyles(unboundStyles); const callLogOut = useLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const staffCanSee = useStaffCanSee(); const stringForUser = useStringForUser(currentUserInfo); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); return ( ); }); export default ConnectedProfileScreen;