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;
}