diff --git a/native/profile/profile-screen.react.js b/native/profile/profile-screen.react.js index d7dfa3c72..d2d798a65 100644 --- a/native/profile/profile-screen.react.js +++ b/native/profile/profile-screen.react.js @@ -1,626 +1,631 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, Platform, ScrollView } from 'react-native'; import uuid from 'uuid'; import { logOutActionTypes, useLogOut, usePrimaryDeviceLogOut, useSecondaryDeviceLogOut, } from 'lib/actions/user-actions.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import { getOwnPrimaryDeviceID } from 'lib/selectors/user-selectors.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { type OutboundDMOperationSpecification, dmOperationSpecificationTypes, } from 'lib/shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from 'lib/shared/dm-ops/process-dm-ops.js'; import type { LogOutResult } from 'lib/types/account-types.js'; import type { DMCreateThreadOperation } from 'lib/types/dm-ops'; import { thickThreadTypes } from 'lib/types/thread-types-enum.js'; import { type CurrentUserInfo } from 'lib/types/user-types.js'; import { getContentSigningKey } from 'lib/utils/crypto-utils.js'; import { useCurrentUserFID } from 'lib/utils/farcaster-utils.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 { useShowVersionUnsupportedAlert } from '../utils/hooks.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, +primaryDeviceID: ?string, +logOutLoading: boolean, +colors: Colors, +styles: $ReadOnly, +dispatchActionPromise: DispatchActionPromise, +logOut: () => Promise, +logOutPrimaryDevice: () => Promise, +logOutSecondaryDevice: () => Promise, +staffCanSee: boolean, +stringForUser: ?string, +isAccountWithPassword: boolean, +onCreateDMThread: () => Promise, +currentUserFID: ?string, }; 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 } = this.props; if (staffCanSee) { developerTools = ( ); defaultNotifications = ( ); keyserverSelection = ( ); tunnelbrokerMenu = ( ); } let backupMenu; if (staffCanSee) { backupMenu = ( ); } let passwordEditionUI; if (accountHasPassword(this.props.currentUserInfo)) { passwordEditionUI = ( Password •••••••••••••••• ); } let linkedDevices; if (__DEV__) { linkedDevices = ( ); } let farcasterAccountSettings; if (usingCommServicesAccessToken || __DEV__) { farcasterAccountSettings = ( ); } let experimentalLogoutActions; if (__DEV__) { experimentalLogoutActions = ( <> ); } let dmActions; if (staffCanSee) { dmActions = ( <> ); } return ( USER AVATAR ACCOUNT Logged in as {this.props.stringForUser} {passwordEditionUI} PREFERENCES {defaultNotifications} {backupMenu} {tunnelbrokerMenu} {farcasterAccountSettings} {linkedDevices} {keyserverSelection} {developerTools} {experimentalLogoutActions} {dmActions} ); } 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 }, ); }; onPressNewLogout = () => { void (async () => { if (this.loggedOutOrLoggingOut) { return; } const { primaryDeviceID } = this.props; const currentDeviceID = await getContentSigningKey(); const isPrimaryDevice = currentDeviceID === primaryDeviceID; let alertTitle, alertMessage, onPressAction; if (isPrimaryDevice) { alertTitle = 'Log out all devices?'; alertMessage = 'This device is your primary device, ' + 'so logging out will cause all of your other devices to log out too.'; onPressAction = this.logOutPrimaryDevice; } else { alertTitle = 'Log out?'; alertMessage = 'Are you sure you want to log out of this device?'; onPressAction = this.logOutSecondaryDevice; } Alert.alert( alertTitle, alertMessage, [ { text: 'No', style: 'cancel' }, { text: 'Yes', onPress: onPressAction, 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(), ); } logOutPrimaryDevice = async () => { if (this.loggedOutOrLoggingOut) { return; } void this.props.dispatchActionPromise( logOutActionTypes, this.props.logOutPrimaryDevice(), ); }; logOutSecondaryDevice = async () => { if (this.loggedOutOrLoggingOut) { return; } void this.props.dispatchActionPromise( logOutActionTypes, this.props.logOutSecondaryDevice(), ); }; 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 }); }; onPressCreateThread = () => { void this.props.onCreateDMThread(); }; } const logOutLoadingStatusSelector = createLoadingStatusSelector(logOutActionTypes); const ConnectedProfileScreen: React.ComponentType = React.memo(function ConnectedProfileScreen(props: BaseProps) { const currentUserInfo = useSelector(state => state.currentUserInfo); const primaryDeviceID = useSelector(getOwnPrimaryDeviceID); const logOutLoading = useSelector(logOutLoadingStatusSelector) === 'loading'; const colors = useColors(); const styles = useStyles(unboundStyles); - const callLogOut = useLogOut(); const callPrimaryDeviceLogOut = usePrimaryDeviceLogOut(); const callSecondaryDeviceLogOut = useSecondaryDeviceLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const staffCanSee = useStaffCanSee(); const stringForUser = useStringForUser(currentUserInfo); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const currentUserID = useCurrentUserFID(); + const showVersionUnsupportedAlert = useShowVersionUnsupportedAlert(false); + const callLogOut = useLogOut({ + handleUseNewFlowResponse: showVersionUnsupportedAlert, + }); + const userID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const onCreateDMThread = React.useCallback(async () => { invariant(userID, 'userID should be set'); const op: DMCreateThreadOperation = { type: 'create_thread', threadID: uuid.v4(), creatorID: userID, time: Date.now(), threadType: thickThreadTypes.LOCAL, memberIDs: [], roleID: uuid.v4(), newMessageID: uuid.v4(), }; const specification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'self_devices', }, }; await processAndSendDMOperation(specification); }, [processAndSendDMOperation, userID]); return ( ); }); export default ConnectedProfileScreen; diff --git a/web/settings/account-settings.react.js b/web/settings/account-settings.react.js index 460a9e4aa..28abe8225 100644 --- a/web/settings/account-settings.react.js +++ b/web/settings/account-settings.react.js @@ -1,356 +1,365 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import uuid from 'uuid'; import { useLogOut, logOutActionTypes, useSecondaryDeviceLogOut, } from 'lib/actions/user-actions.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/swmansion-icon.react.js'; import { useStringForUser } from 'lib/hooks/ens-cache.js'; import { accountHasPassword } from 'lib/shared/account-utils.js'; import { dmOperationSpecificationTypes, type OutboundDMOperationSpecification, } from 'lib/shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from 'lib/shared/dm-ops/process-dm-ops.js'; import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import { useTunnelbroker } from 'lib/tunnelbroker/tunnelbroker-context.js'; import type { DMCreateThreadOperation } from 'lib/types/dm-ops.js'; import { thickThreadTypes } from 'lib/types/thread-types-enum.js'; import { createOlmSessionsWithOwnDevices, getContentSigningKey, } from 'lib/utils/crypto-utils.js'; import { isDev } from 'lib/utils/dev-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './account-settings.css'; import AppearanceChangeModal from './appearance-change-modal.react.js'; import BackupTestRestoreModal from './backup-test-restore-modal.react.js'; import PasswordChangeModal from './password-change-modal.js'; import BlockListModal from './relationship/block-list-modal.react.js'; import FriendListModal from './relationship/friend-list-modal.react.js'; import TunnelbrokerMessagesScreen from './tunnelbroker-message-list.react.js'; import TunnelbrokerTestScreen from './tunnelbroker-test.react.js'; import EditUserAvatar from '../avatars/edit-user-avatar.react.js'; import Button from '../components/button.react.js'; +import VersionUnsupportedModal from '../modals/version-unsupported-modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function AccountSettings(): React.Node { - const sendLogoutRequest = useLogOut(); + const { pushModal, popModal } = useModalContext(); + + const logOutOptions = React.useMemo(() => { + const showVersionUnsupportedModal = () => { + pushModal(); + }; + return { handleUseNewFlowResponse: showVersionUnsupportedModal }; + }, [pushModal]); + const sendLogoutRequest = useLogOut(logOutOptions); + const sendSecondaryDeviceLogoutRequest = useSecondaryDeviceLogOut(); const dispatchActionPromise = useDispatchActionPromise(); const logOutUser = React.useCallback( () => dispatchActionPromise(logOutActionTypes, sendLogoutRequest()), [dispatchActionPromise, sendLogoutRequest], ); const logOutSecondaryDevice = React.useCallback( () => dispatchActionPromise( logOutActionTypes, sendSecondaryDeviceLogoutRequest(), ), [dispatchActionPromise, sendSecondaryDeviceLogoutRequest], ); const identityContext = React.useContext(IdentityClientContext); const userID = useSelector(state => state.currentUserInfo?.id); const [deviceID, setDeviceID] = React.useState(); React.useEffect(() => { void (async () => { const contentSigningKey = await getContentSigningKey(); setDeviceID(contentSigningKey); })(); }, []); - const { pushModal, popModal } = useModalContext(); const showPasswordChangeModal = React.useCallback( () => pushModal(), [pushModal], ); const openFriendList = React.useCallback( () => pushModal(), [pushModal], ); const openBlockList = React.useCallback( () => pushModal(), [pushModal], ); const isAccountWithPassword = useSelector(state => accountHasPassword(state.currentUserInfo), ); const currentUserInfo = useSelector(state => state.currentUserInfo); const stringForUser = useStringForUser(currentUserInfo); const staffCanSee = useStaffCanSee(); const { sendMessageToDevice, socketState, addListener, removeListener } = useTunnelbroker(); const openTunnelbrokerModal = React.useCallback( () => pushModal( , ), [popModal, pushModal, sendMessageToDevice], ); const openTunnelbrokerMessagesModal = React.useCallback( () => pushModal( , ), [addListener, popModal, pushModal, removeListener], ); const onCreateOlmSessions = React.useCallback(async () => { if (!identityContext) { return; } const authMetadata = await identityContext.getAuthMetadata(); try { await createOlmSessionsWithOwnDevices( authMetadata, identityContext.identityClient, sendMessageToDevice, ); } catch (e) { console.log(`Error creating olm sessions with own devices: ${e.message}`); } }, [identityContext, sendMessageToDevice]); const openBackupTestRestoreModal = React.useCallback( () => pushModal(), [popModal, pushModal], ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const onCreateDMThread = React.useCallback(async () => { invariant(userID, 'userID should be set'); const op: DMCreateThreadOperation = { type: 'create_thread', threadID: uuid.v4(), creatorID: userID, time: Date.now(), threadType: thickThreadTypes.LOCAL, memberIDs: [], roleID: uuid.v4(), newMessageID: uuid.v4(), }; const specification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'self_devices', }, }; await processAndSendDMOperation(specification); }, [processAndSendDMOperation, userID]); const showAppearanceModal = React.useCallback( () => pushModal(), [pushModal], ); if (!currentUserInfo || currentUserInfo.anonymous) { return null; } let changePasswordSection; if (isAccountWithPassword) { changePasswordSection = (
  • Password ******
  • ); } let experimentalLogOutSection; if (isDev) { experimentalLogOutSection = (
  • Log out secondary device
  • ); } let preferences; if (staffCanSee) { preferences = (

    Preferences

    • Appearance
    ); } let tunnelbroker; if (staffCanSee) { tunnelbroker = (

    Tunnelbroker menu

    • Connected {socketState.connected.toString()}
    • Send message to device
    • Trace received messages
    • Create session with own devices
    ); } let backup; if (staffCanSee) { backup = (

    Backup menu

    • Test backup restore
    ); } let deviceData; if (staffCanSee) { deviceData = (

    Device ID

    • {deviceID}

    User ID

    • {userID}
    ); } let dms; if (staffCanSee) { dms = (

    DMs menu

    • Create a new local DM thread
    ); } return (

    My Account

    • {'Logged in as '} {stringForUser}

    • {experimentalLogOutSection} {changePasswordSection}
    • Friend List
    • Block List
    {preferences} {tunnelbroker} {backup} {deviceData} {dms}
    ); } export default AccountSettings;