import { useCallback, useMemo } from 'react'; import { sprintf } from 'sprintf-js'; import styled from 'styled-components'; import { AccessMethodSetting } from '../../shared/daemon-rpc-types'; import { messages } from '../../shared/gettext'; import { RoutePath } from '../../shared/routes'; import { useAppContext } from '../context'; import { useApiAccessMethodTest } from '../lib/api-access-methods'; import { Button, Container, Flex, Spinner } from '../lib/components'; import { Switch } from '../lib/components/switch'; import { colors, spacings } from '../lib/foundations'; import { useHistory } from '../lib/history'; import { generateRoutePath } from '../lib/routeHelpers'; import { useBoolean } from '../lib/utility-hooks'; import { useSelector } from '../redux/store'; import { AppNavigationHeader } from './'; import * as Cell from './cell'; import { ContextMenu, ContextMenuContainer, ContextMenuItem, ContextMenuTrigger, } from './ContextMenu'; import InfoButton from './InfoButton'; import { BackAction } from './KeyboardNavigation'; import { Layout, SettingsContainer, SettingsContent, SettingsNavigationScrollbars } from './Layout'; import { ModalAlert, ModalAlertType } from './Modal'; import { NavigationContainer } from './NavigationContainer'; import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader'; const StyledNameLabel = styled(Cell.Label)({ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }); const StyledTestResultCircle = styled.div<{ $result: boolean }>((props) => ({ width: '10px', height: '10px', borderRadius: '50%', backgroundColor: props.$result ? colors.green : colors.red, marginRight: spacings.small, })); // This component is the topmost component in the API access methods view. export default function ApiAccessMethods() { const history = useHistory(); const methods = useSelector((state) => state.settings.apiAccessMethods); const currentMethod = useSelector((state) => state.settings.currentApiAccessMethod); const navigateToEdit = useCallback( (id?: string) => { const path = generateRoutePath(RoutePath.editApiAccessMethods, { id }); history.push(path); }, [history], ); const navigateToNew = useCallback(() => navigateToEdit(), [navigateToEdit]); return ( {messages.pgettext('navigation-bar', 'API access')} {messages.pgettext( 'api-access-methods-view', 'Manage and add custom methods to access the Mullvad API.', )} {methods.custom.map((method) => ( ))} ); } interface ApiAccessMethodProps { method: AccessMethodSetting; inUse: boolean; custom?: boolean; } function ApiAccessMethod(props: ApiAccessMethodProps) { const { setApiAccessMethod: setApiAccessMethodImpl, updateApiAccessMethod, removeApiAccessMethod, } = useAppContext(); const { push } = useHistory(); const [testing, testResult, testApiAccessMethod] = useApiAccessMethodTest(); // State for delete confirmation dialog. const [removeConfirmationVisible, showRemoveConfirmation, hideRemoveConfirmation] = useBoolean(); const confirmRemove = useCallback(() => { void removeApiAccessMethod(props.method.id); hideRemoveConfirmation(); }, [hideRemoveConfirmation, props.method.id, removeApiAccessMethod]); // Toggle on/off on an access method. const toggle = useCallback( async (value: boolean) => { const updatedMethod = cloneMethod(props.method); updatedMethod.enabled = value; await updateApiAccessMethod(updatedMethod); }, [props.method, updateApiAccessMethod], ); const setApiAccessMethod = useCallback(async () => { const reachable = await testApiAccessMethod(props.method.id); if (reachable) { await setApiAccessMethodImpl(props.method.id); } }, [testApiAccessMethod, props.method.id, setApiAccessMethodImpl]); const menuItems = useMemo>(() => { const items: Array = [ { type: 'item' as const, label: messages.gettext('Use'), disabled: props.inUse, onClick: setApiAccessMethod, }, { type: 'item' as const, label: messages.gettext('Test'), onClick: () => testApiAccessMethod(props.method.id), }, ]; // Edit and Delete shouldn't be available for direct, bridges or encrypted DNS proxy. if (props.custom) { items.push( { type: 'separator' as const }, { type: 'item' as const, label: messages.gettext('Edit'), onClick: () => push(generateRoutePath(RoutePath.editApiAccessMethods, { id: props.method.id })), }, { type: 'item' as const, label: messages.gettext('Delete'), onClick: showRemoveConfirmation, }, ); } return items; }, [ props.inUse, props.custom, props.method.id, setApiAccessMethod, testApiAccessMethod, showRemoveConfirmation, push, ]); return ( {props.method.name} {testing && ( {messages.pgettext('api-access-methods-view', 'Testing...')} )} {!testing && testResult !== undefined && ( {testResult ? messages.pgettext('api-access-methods-view', 'API reachable') : messages.pgettext('api-access-methods-view', 'API unreachable')} )} {!testing && testResult === undefined && props.inUse && ( {messages.pgettext('api-access-methods-view', 'In use')} )} {props.method.type === 'direct' && ( )} {props.method.type === 'bridges' && ( )} {props.method.type === 'encrypted-dns-proxy' && ( )} {/* Confirmation dialog for method removal */} {messages.gettext('Cancel')} , , ]} close={hideRemoveConfirmation} title={sprintf(messages.pgettext('api-access-methods-view', 'Delete %(name)s?'), { name: props.method.name, })} message={ props.inUse ? messages.pgettext( 'api-access-methods-view', 'The in use API access method will change.', ) : undefined } /> ); } function cloneMethod(method: T): T { const clonedMethod = { ...method, }; if ( method.type === 'socks5-remote' && clonedMethod.type === 'socks5-remote' && method.authentication !== undefined ) { clonedMethod.authentication = { ...method.authentication }; } return clonedMethod; }