From c6d3fd39fbf23208f546a58c138b58e746ef083c Mon Sep 17 00:00:00 2001 From: Dmitriy Petrov Date: Wed, 15 Jul 2026 15:25:10 +0300 Subject: [PATCH] Add stop confirmation to client power action --- src/shared/versions.js | 4 ++-- src/web/components/ClientOverviewPage.jsx | 25 +++++++++++++++++++++-- test/web/rule-editor-contract.test.js | 9 ++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/shared/versions.js b/src/shared/versions.js index 593f07c..331e761 100644 --- a/src/shared/versions.js +++ b/src/shared/versions.js @@ -1,6 +1,6 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.8.10', - gatewayClient: '0.8.9', + macClient: '0.8.11', + gatewayClient: '0.8.10', gatewayBackend: '0.8.0', }); diff --git a/src/web/components/ClientOverviewPage.jsx b/src/web/components/ClientOverviewPage.jsx index 147fadf..a509032 100644 --- a/src/web/components/ClientOverviewPage.jsx +++ b/src/web/components/ClientOverviewPage.jsx @@ -644,6 +644,7 @@ export function ClientOverviewPage({ const [localRulesRevision, setLocalRulesRevision] = useState(state?.route?.localRulesRevision || 0); const [confirmingLocalRulesClose, setConfirmingLocalRulesClose] = useState(false); const [confirmingDelete, setConfirmingDelete] = useState(false); + const [confirmingStop, setConfirmingStop] = useState(false); const [openInstructionId, setOpenInstructionId] = useState(''); const subscriptionInputRef = useRef(null); const subscriptionRef = useRef(null); @@ -899,13 +900,21 @@ export function ClientOverviewPage({ return () => document.removeEventListener('pointerdown', closeInstructionsOutside); }, [instructionsOpen]); - async function toggleConnection() { + function toggleConnection() { const action = connectionAction({ connected, selectedServerId, configExists: state?.configExists }); - if (action?.type === 'stop') return onStop(); + if (action?.type === 'stop') { + setConfirmingStop(true); + return; + } if (action?.type === 'apply') return onApply(action.serverId); if (action?.type === 'restart') return onRestart(); } + async function stopConnection() { + if (!await onStop()) return; + setConfirmingStop(false); + } + function selectServer(serverId) { setPendingServerId(serverId); if (connected && serverId) onApply(serverId); @@ -1443,6 +1452,18 @@ export function ClientOverviewPage({ onClose={requestCloseLocalRules} onSave={saveLocalRules} />} + setConfirmingStop(false)} + onConfirm={stopConnection} + /> }); test('critical confirmations share one accessible blocking popup', () => { + assert.match(component, /id="stop-connection"/); assert.match(component, /id="discard-local-rules"/); assert.match(component, /id="delete-subscription"/); assert.doesNotMatch(component, /client-local-rules-discard|client-delete-confirmation/); @@ -47,9 +48,17 @@ test('critical confirmations share one accessible blocking popup', () => { assert.match(popup, /aria-modal="true"/); assert.match(popup, /querySelectorAll\(FOCUSABLE\)/); assert.match(popup, /element\.inert = true/); + assert.match(popup, /requestAnimationFrame\(\(\) => cancelRef\.current\?\.focus\(\)\)/); assert.match(styles, /\.client-confirmation-popup\.is-open[\s\S]*backdrop-filter: blur\(18px\)/); }); +test('power click and native Enter cannot stop VPN without a separate confirmation', () => { + assert.match(component, /className="client-power"[\s\S]*type="button"[\s\S]*onClick=\{toggleConnection\}/); + assert.match(component, /if \(action\?\.type === 'stop'\) \{[\s\S]*setConfirmingStop\(true\);[\s\S]*return;[\s\S]*\}/); + assert.doesNotMatch(component, /if \(action\?\.type === 'stop'\) return onStop\(\)/); + assert.match(component, /id="stop-connection"[\s\S]*cancelLabel="Оставить включённым"[\s\S]*confirmLabel="Отключить VPN"[\s\S]*onConfirm=\{stopConnection\}/); +}); + test('copy feedback, drawers and Gateway access actions expose complete semantics', () => { assert.match(component, /className="client-live-region" role="status" aria-live="polite" aria-atomic="true"/); assert.match(component, /Не удалось скопировать/);