import { useCallback } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { IDevice } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import log from '../../shared/logging'; import { capitalizeEveryWord } from '../../shared/string-helpers'; import { useAppContext } from '../context'; import { transitions, useHistory } from '../lib/history'; import { formatHtml } from '../lib/html-formatter'; import { RoutePath } from '../lib/routes'; import { useBoolean } from '../lib/utilityHooks'; import { useSelector } from '../redux/store'; import * as AppButton from './AppButton'; import * as Cell from './cell'; import { bigText, measurements, normalText, tinyText } from './common-styles'; import CustomScrollbars from './CustomScrollbars'; import { Brand, HeaderBarSettingsButton } from './HeaderBar'; import ImageView from './ImageView'; import { Footer, Header, Layout, SettingsContainer } from './Layout'; import List from './List'; import { ModalAlert, ModalAlertType, ModalContainer, ModalMessage } from './Modal'; const StyledCustomScrollbars = styled(CustomScrollbars)({ flex: 1, }); const StyledContainer = styled(SettingsContainer)({ paddingTop: '14px', minHeight: '100%', }); const StyledBody = styled.div({ display: 'flex', flexDirection: 'column', flex: 1, paddingBottom: 'auto', }); const StyledStatusIcon = styled.div({ alignSelf: 'center', width: '60px', height: '60px', marginBottom: '18px', }); const StyledTitle = styled.span(bigText, { lineHeight: '38px', margin: `0 ${measurements.viewMargin} 8px`, color: colors.white, }); const StyledLabel = styled.span({ fontFamily: 'Open Sans', fontSize: '12px', fontWeight: 600, lineHeight: '20px', color: colors.white, margin: `0 ${measurements.viewMargin} 18px`, }); const StyledSpacer = styled.div({ flex: '1', }); const StyledDeviceInfo = styled(Cell.Label)({ display: 'flex', flexDirection: 'column', marginTop: '9px', marginBottom: '9px', }); const StyledDeviceName = styled.span(normalText, { fontWeight: 'normal', lineHeight: '20px', textTransform: 'capitalize', }); const StyledDeviceDate = styled.span(tinyText, { fontSize: '10px', lineHeight: '10px', color: colors.white60, }); const StyledRemoveDeviceButton = styled.button({ cursor: 'default', padding: 0, marginLeft: 8, backgroundColor: 'transparent', border: 'none', }); export default function TooManyDevices() { const history = useHistory(); const { removeDevice, login, cancelLogin } = useAppContext(); const accountToken = useSelector((state) => state.account.accountToken)!; const devices = useSelector((state) => state.account.devices); const loginState = useSelector((state) => state.account.status); const onRemoveDevice = useCallback( async (deviceId: string) => { await removeDevice({ accountToken, deviceId }); }, [removeDevice, accountToken], ); const continueLogin = useCallback(() => { void login(accountToken); history.reset(RoutePath.login, { transition: transitions.pop }); }, [login, accountToken]); const cancel = useCallback(() => { cancelLogin(); history.reset(RoutePath.login, { transition: transitions.pop }); }, [history.reset, cancelLogin]); const iconSource = getIconSource(devices); const title = getTitle(devices); const subtitle = getSubtitle(devices); const continueButtonDisabled = devices.length === 5 || loginState.type !== 'too many devices'; return (
{devices !== undefined && ( <> {title} {subtitle} )} {devices !== undefined && ( )}
); } interface IDeviceListProps { devices: Array; onRemoveDevice: (deviceId: string) => Promise; } function DeviceList(props: IDeviceListProps) { return ( {(device) => } ); } const getDeviceKey = (device: IDevice): string => device.id; interface IDeviceProps { device: IDevice; onRemove: (deviceId: string) => Promise; } function Device(props: IDeviceProps) { const { fetchDevices } = useAppContext(); const accountToken = useSelector((state) => state.account.accountToken)!; const [confirmationVisible, showConfirmation, hideConfirmation] = useBoolean(false); const [deleting, setDeleting, unsetDeleting] = useBoolean(false); const [error, setError, resetError] = useBoolean(false); const handleError = useCallback( async (error: Error) => { log.error(`Failede to remove device: ${error.message}`); let devices: Array | undefined = undefined; try { devices = await fetchDevices(accountToken); } catch { /* no-op */ } if (devices === undefined || devices.find((device) => device.id === props.device.id)) { hideConfirmation(); unsetDeleting(); setError(); } }, [fetchDevices, accountToken, hideConfirmation, setError], ); const onRemove = useCallback(async () => { setDeleting(); hideConfirmation(); try { await props.onRemove(props.device.id); } catch (e) { await handleError(e as Error); } }, [props.onRemove, props.device.id, hideConfirmation, setDeleting, handleError]); const capitalizedDeviceName = capitalizeEveryWord(props.device.name); const createdDate = props.device.created.toISOString().split('T')[0]; return ( <> {props.device.name} {sprintf( // TRANSLATORS: Label informing the user when a device was created. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(createdDate)s - The creation date of the device. messages.pgettext('device-management', 'Created: %(createdDate)s'), { createdDate, }, )} {deleting ? ( ) : ( )} { // TRANSLATORS: Confirmation button when logging out other device. messages.pgettext('device-management', 'Yes, log out device') } , {messages.gettext('Back')} , ]} close={hideConfirmation}> {formatHtml( sprintf( // TRANSLATORS: Text displayed above button which logs out another device. // TRANSLATORS: The text enclosed in "" will appear bold. // TRANSLATORS: Available placeholders: // TRANSLATORS: %(deviceName)s - The name of the device to log out. messages.pgettext( 'device-management', 'Are you sure you want to log %(deviceName)s out?', ), { deviceName: capitalizedDeviceName }, ), )} {messages.gettext('Close')} , ]} close={resetError} message={messages.pgettext('device-management', 'Failed to remove device')} /> ); } function getIconSource(devices?: Array): string { if (devices) { if (devices.length === 5) { return 'icon-fail'; } else { return 'icon-success'; } } else { return 'icon-spinner'; } } function getTitle(devices?: Array): string | undefined { if (devices) { if (devices.length === 5) { // TRANSLATORS: Page title informing user that the login failed due to too many registered // TRANSLATORS: devices on account. return messages.pgettext('device-management', 'Too many devices'); } else { // TRANSLATORS: Page title informing user that enough devices has been removed to continue // TRANSLATORS: login process. return messages.pgettext('device-management', 'Super!'); } } else { return undefined; } } function getSubtitle(devices?: Array): string | undefined { if (devices) { if (devices.length === 5) { return messages.pgettext( 'device-management', 'Please log out of at least one by removing it from the list below. You can find the corresponding device name under the device’s Account settings.', ); } else { return messages.pgettext( 'device-management', 'You can now continue logging in on this device.', ); } } else { return undefined; } }