import { useCallback, useMemo } from 'react';
import { sprintf } from 'sprintf-js';
import styled from 'styled-components';
import { colors } from '../../config.json';
import { AccessMethodSetting } from '../../shared/daemon-rpc-types';
import { messages } from '../../shared/gettext';
import { useAppContext } from '../context';
import { useApiAccessMethodTest } from '../lib/api-access-methods';
import { useHistory } from '../lib/history';
import { generateRoutePath } from '../lib/routeHelpers';
import { RoutePath } from '../lib/routes';
import { useBoolean } from '../lib/utilityHooks';
import { useSelector } from '../redux/store';
import * as Cell from './cell';
import {
ContextMenu,
ContextMenuContainer,
ContextMenuItem,
ContextMenuTrigger,
} from './ContextMenu';
import ImageView from './ImageView';
import InfoButton from './InfoButton';
import { BackAction } from './KeyboardNavigation';
import { Layout, SettingsContainer } from './Layout';
import { ModalAlert, ModalAlertType } from './Modal';
import {
NavigationBar,
NavigationContainer,
NavigationInfoButton,
NavigationItems,
TitleBarItem,
} from './NavigationBar';
import SettingsHeader, { HeaderSubTitle, HeaderTitle } from './SettingsHeader';
import { StyledContent, StyledNavigationScrollbars, StyledSettingsContent } from './SettingsStyles';
import { SmallButton, SmallButtonColor, SmallButtonGroup } from './SmallButton';
const StyledContextMenuButton = styled(Cell.Icon)({
marginRight: '8px',
});
const StyledMethodInfoButton = styled(InfoButton)({
marginRight: '11px',
});
const StyledSpinner = styled(ImageView)({
height: '10px',
width: '10px',
marginRight: '6px',
});
const StyledNameLabel = styled(Cell.Label)({
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: '6px',
}));
// 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 (
{
// TRANSLATORS: Title label in navigation bar
messages.pgettext('navigation-bar', 'API access')
}
{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) => (
))}
{messages.gettext('Add')}
);
}
interface ApiAccessMethodProps {
method: AccessMethodSetting;
inUse: boolean;
custom?: boolean;
}
function ApiAccessMethod(props: ApiAccessMethodProps) {
const {
setApiAccessMethod: setApiAccessMethodImpl,
updateApiAccessMethod,
removeApiAccessMethod,
} = useAppContext();
const history = 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();
}, [props.method.id]);
// 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],
);
const setApiAccessMethod = useCallback(async () => {
const reachable = await testApiAccessMethod(props.method.id);
if (reachable) {
await setApiAccessMethodImpl(props.method.id);
}
}, [testApiAccessMethod, props.method.id]);
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 and bridges.
if (props.custom) {
items.push(
{ type: 'separator' as const },
{
type: 'item' as const,
label: messages.gettext('Edit'),
onClick: () =>
history.push(
generateRoutePath(RoutePath.editApiAccessMethods, { id: props.method.id }),
),
},
{
type: 'item' as const,
label: messages.gettext('Delete'),
onClick: showRemoveConfirmation,
},
);
}
return items;
}, [props.method.id, props.inUse, setApiAccessMethod, testApiAccessMethod, history.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' && (
)}
{/* Confirmation dialog for method removal */}
{messages.gettext('Cancel')}
,
{messages.gettext('Delete')}
,
]}
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;
}