Skip to content

Commit 6c49baf

Browse files
committed
feat: add app lock system unlock auto prompt setting
1 parent 46716f1 commit 6c49baf

22 files changed

Lines changed: 414 additions & 32 deletions

application/i18n/locales/en/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ export const enCoreMessages: Messages = {
160160
'settings.appLock.systemUnlock.unavailableDesc': '{label} is not available on this device right now.',
161161
'settings.appLock.systemUnlock.unavailable': 'System unlock is not available on this device.',
162162
'settings.appLock.systemUnlock.locked': 'Unlock Netcatty before changing system unlock.',
163+
'settings.appLock.systemUnlock.autoPrompt.label': 'Open {label} automatically',
164+
'settings.appLock.systemUnlock.autoPrompt.desc': 'Show the {label} prompt automatically when Netcatty is waiting to unlock.',
163165
'settings.appLock.currentPassword': 'Current password',
164166
'settings.appLock.currentPasswordPlaceholder': 'Required to disable or replace',
165167
'settings.appLock.currentPasswordForDisablePlaceholder': 'Current password to turn off App Lock',

application/i18n/locales/ru/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ export const ruCoreMessages: Messages = {
160160
'settings.appLock.systemUnlock.unavailableDesc': '{label} сейчас недоступен на этом устройстве.',
161161
'settings.appLock.systemUnlock.unavailable': 'Системная разблокировка недоступна на этом устройстве.',
162162
'settings.appLock.systemUnlock.locked': 'Разблокируйте Netcatty перед изменением системной разблокировки.',
163+
'settings.appLock.systemUnlock.autoPrompt.label': 'Открывать {label} автоматически',
164+
'settings.appLock.systemUnlock.autoPrompt.desc': 'Автоматически показывать запрос {label}, когда Netcatty ожидает разблокировки.',
163165
'settings.appLock.currentPassword': 'Текущий пароль',
164166
'settings.appLock.currentPasswordPlaceholder': 'Нужен для отключения или замены',
165167
'settings.appLock.currentPasswordForDisablePlaceholder': 'Текущий пароль для отключения блокировки',

application/i18n/locales/settingsLocales.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ test("all app lock strings are translated in every supported locale", () => {
102102
"settings.appLock.systemUnlock.unavailableDesc",
103103
"settings.appLock.systemUnlock.unavailable",
104104
"settings.appLock.systemUnlock.locked",
105+
"settings.appLock.systemUnlock.autoPrompt.label",
106+
"settings.appLock.systemUnlock.autoPrompt.desc",
105107
"settings.appLock.currentPassword",
106108
"settings.appLock.currentPasswordPlaceholder",
107109
"settings.appLock.newPassword",

application/i18n/locales/zh-CN/core.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export const zhCNCoreMessages: Messages = {
144144
'settings.appLock.systemUnlock.unavailableDesc': '此设备当前无法使用 {label}。',
145145
'settings.appLock.systemUnlock.unavailable': '此设备当前无法使用系统解锁。',
146146
'settings.appLock.systemUnlock.locked': '请先解锁 Netcatty,再更改系统解锁。',
147+
'settings.appLock.systemUnlock.autoPrompt.label': '自动弹出 {label}',
148+
'settings.appLock.systemUnlock.autoPrompt.desc': '当 Netcatty 等待解锁时,自动显示 {label} 验证提示。',
147149
'settings.appLock.currentPassword': '当前密码',
148150
'settings.appLock.currentPasswordPlaceholder': '禁用或替换时需要',
149151
'settings.appLock.currentPasswordForDisablePlaceholder': '用于关闭应用锁定的当前密码',

application/state/appLockSettingsStorage.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ test("applyAppLockEnabledChange enables only when a verifier exists", async () =
1717
enabled: false,
1818
timeoutMinutes: 15,
1919
systemUnlockEnabled: false,
20+
systemUnlockAutoPromptEnabled: false,
2021
passwordVerifier: verifier,
2122
};
2223

@@ -26,6 +27,7 @@ test("applyAppLockEnabledChange enables only when a verifier exists", async () =
2627
enabled: true,
2728
timeoutMinutes: 15,
2829
systemUnlockEnabled: false,
30+
systemUnlockAutoPromptEnabled: false,
2931
passwordVerifier: verifier,
3032
},
3133
);
@@ -36,6 +38,7 @@ test("applyAppLockEnabledChange enables only when a verifier exists", async () =
3638
enabled: false,
3739
timeoutMinutes: 15,
3840
systemUnlockEnabled: false,
41+
systemUnlockAutoPromptEnabled: false,
3942
passwordVerifier: null,
4043
},
4144
);
@@ -47,6 +50,7 @@ test("applyAppLockEnabledChange requires current password before disabling an ex
4750
enabled: true,
4851
timeoutMinutes: 30,
4952
systemUnlockEnabled: true,
53+
systemUnlockAutoPromptEnabled: true,
5054
passwordVerifier: verifier,
5155
};
5256

@@ -61,6 +65,7 @@ test("applyAppLockEnabledChange requires current password before disabling an ex
6165
enabled: false,
6266
timeoutMinutes: 30,
6367
systemUnlockEnabled: false,
68+
systemUnlockAutoPromptEnabled: false,
6469
passwordVerifier: null,
6570
},
6671
);
@@ -72,6 +77,7 @@ test("replaceAppLockPassword requires current password when replacing an existin
7277
enabled: true,
7378
timeoutMinutes: 5,
7479
systemUnlockEnabled: true,
80+
systemUnlockAutoPromptEnabled: true,
7581
passwordVerifier: verifier,
7682
};
7783

@@ -99,6 +105,7 @@ test("replaceAppLockPassword requires current password when replacing an existin
99105
assert.equal(replaced.enabled, true);
100106
assert.equal(replaced.timeoutMinutes, 5);
101107
assert.equal(replaced.systemUnlockEnabled, true);
108+
assert.equal(replaced.systemUnlockAutoPromptEnabled, true);
102109
assert.notEqual(replaced.passwordVerifier?.hash, verifier.hash);
103110
assert.equal(await verifyAppLockPassword("new secret", replaced.passwordVerifier), true);
104111
});
@@ -108,6 +115,7 @@ test("replaceAppLockPassword rejects empty new passwords", async () => {
108115
enabled: false,
109116
timeoutMinutes: 60,
110117
systemUnlockEnabled: false,
118+
systemUnlockAutoPromptEnabled: false,
111119
passwordVerifier: null,
112120
};
113121

@@ -124,6 +132,7 @@ test("replaceAppLockPassword enables app lock when creating the first password",
124132
enabled: false,
125133
timeoutMinutes: 15,
126134
systemUnlockEnabled: false,
135+
systemUnlockAutoPromptEnabled: false,
127136
passwordVerifier: null,
128137
};
129138

@@ -135,5 +144,6 @@ test("replaceAppLockPassword enables app lock when creating the first password",
135144
assert.equal(saved.enabled, true);
136145
assert.equal(saved.timeoutMinutes, 15);
137146
assert.equal(saved.systemUnlockEnabled, false);
147+
assert.equal(saved.systemUnlockAutoPromptEnabled, false);
138148
assert.equal(await verifyAppLockPassword("first secret", saved.passwordVerifier), true);
139149
});

application/state/appLockSettingsStorage.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,18 @@ export async function applyAppLockEnabledChange(
2323
): Promise<AppLockSettingsChangeResult> {
2424
const timeoutMinutes = normalizeAppLockTimeoutMinutes(settings.timeoutMinutes);
2525
const passwordVerifier = normalizeAppLockPasswordVerifier(settings.passwordVerifier);
26-
const systemUnlockEnabled = settings.systemUnlockEnabled === true && passwordVerifier !== null;
26+
const systemUnlockEnabled = settings.systemUnlockEnabled === true && settings.enabled === true && passwordVerifier !== null;
27+
const systemUnlockAutoPromptEnabled = settings.systemUnlockAutoPromptEnabled === true && systemUnlockEnabled;
2728
if (enabled) {
2829
return passwordVerifier
29-
? { enabled: true, timeoutMinutes, systemUnlockEnabled, passwordVerifier }
30-
: { enabled: false, timeoutMinutes, systemUnlockEnabled: false, passwordVerifier: null };
30+
? { enabled: true, timeoutMinutes, systemUnlockEnabled, systemUnlockAutoPromptEnabled, passwordVerifier }
31+
: {
32+
enabled: false,
33+
timeoutMinutes,
34+
systemUnlockEnabled: false,
35+
systemUnlockAutoPromptEnabled: false,
36+
passwordVerifier: null,
37+
};
3138
}
3239

3340
if (passwordVerifier) {
@@ -40,6 +47,7 @@ export async function applyAppLockEnabledChange(
4047
enabled: false,
4148
timeoutMinutes,
4249
systemUnlockEnabled: false,
50+
systemUnlockAutoPromptEnabled: false,
4351
passwordVerifier: null,
4452
};
4553
}
@@ -64,6 +72,7 @@ export async function replaceAppLockPassword(
6472
enabled: normalized.enabled || !normalized.passwordVerifier,
6573
timeoutMinutes: normalized.timeoutMinutes,
6674
systemUnlockEnabled: normalized.systemUnlockEnabled,
75+
systemUnlockAutoPromptEnabled: normalized.systemUnlockAutoPromptEnabled,
6776
passwordVerifier: await createAppLockPasswordVerifier(input.nextPassword),
6877
};
6978
}

application/state/useAppLockState.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ test("shouldLockOnStartup locks only when enabled with a verifier", async () =>
1818
enabled: true,
1919
timeoutMinutes: 15,
2020
systemUnlockEnabled: false,
21+
systemUnlockAutoPromptEnabled: false,
2122
passwordVerifier: verifier,
2223
};
2324

@@ -32,6 +33,7 @@ test("shouldLockAfterIdle honors the configured timeout", async () => {
3233
enabled: true,
3334
timeoutMinutes: 5,
3435
systemUnlockEnabled: false,
36+
systemUnlockAutoPromptEnabled: false,
3537
passwordVerifier: verifier,
3638
};
3739

@@ -48,6 +50,7 @@ test("getIdleLockDelayMs schedules the next check after remaining idle time", as
4850
enabled: true,
4951
timeoutMinutes: 5,
5052
systemUnlockEnabled: false,
53+
systemUnlockAutoPromptEnabled: false,
5154
passwordVerifier: verifier,
5255
};
5356

application/state/useSettingsState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
194194
enabled: false,
195195
timeoutMinutes: 15,
196196
systemUnlockEnabled: false,
197+
systemUnlockAutoPromptEnabled: false,
197198
passwordVerifier: null,
198199
});
199200
const [terminalSettings, setTerminalSettingsState] = useState<TerminalSettings>(() => {
@@ -522,6 +523,7 @@ export const useSettingsState = (options: { enableSettingsSync?: boolean; enable
522523
const setAppLockSystemUnlockEnabled = useCallback(async (input: {
523524
enabled: boolean;
524525
currentPassword?: string;
526+
autoPromptEnabled?: boolean;
525527
}) => {
526528
const next = await netcattyBridge.get()?.setAppLockSystemUnlockEnabled?.(input);
527529
if (next && !('ok' in next)) {

components/AppLockGate.runtime.test.tsx

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ test("startup-locked gate reveals children after successful system unlock", asyn
116116
enabled: true,
117117
timeoutMinutes: 15,
118118
systemUnlockEnabled: true,
119+
systemUnlockAutoPromptEnabled: true,
119120
passwordVerifier: {
120121
version: 1,
121122
algorithm: "PBKDF2-SHA256",
@@ -152,6 +153,76 @@ test("startup-locked gate reveals children after successful system unlock", asyn
152153
}
153154
});
154155

156+
test("startup-locked gate does not automatically system unlock when auto prompt is disabled", async () => {
157+
const dom = installDomEnvironment();
158+
const renderer = await createDomRenderer(dom.document);
159+
const bridgeHarness = createAppLockBridgeHarness({
160+
runtimeState: {
161+
initialized: true,
162+
locked: true,
163+
reason: "startup",
164+
version: 1,
165+
lastLockedAt: 1_000,
166+
lastUnlockedAt: null,
167+
lastActivityAt: 1_000,
168+
},
169+
systemUnlockStatus: {
170+
supported: true,
171+
available: true,
172+
enabled: true,
173+
platform: "darwin",
174+
label: "Touch ID",
175+
reason: null,
176+
},
177+
});
178+
const AppLockGate = createAppLockGate({
179+
useSettingsState: () => ({
180+
uiLanguage: "en",
181+
appLockSettings: {
182+
enabled: true,
183+
timeoutMinutes: 15,
184+
systemUnlockEnabled: true,
185+
systemUnlockAutoPromptEnabled: false,
186+
passwordVerifier: {
187+
version: 1,
188+
algorithm: "PBKDF2-SHA256",
189+
iterations: 210000,
190+
salt: "AAAAAAAAAAAAAAAAAAAAAA==",
191+
hash: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
192+
},
193+
},
194+
}) as ReturnType<typeof import("../application/state/useSettingsState.ts").useSettingsState>,
195+
useAppLockState,
196+
useAppLockBridge,
197+
});
198+
199+
const previousWindowNetcatty = dom.window.netcatty;
200+
dom.window.netcatty = bridgeHarness.bridge;
201+
202+
try {
203+
await renderer.render(
204+
React.createElement(AppLockGate, {
205+
notifyRendererReady: false,
206+
children: () => React.createElement("div", { id: "unlocked-content" }, "Unlocked"),
207+
}),
208+
);
209+
await flushEffects();
210+
await flushEffects();
211+
212+
assert.equal(bridgeHarness.getSystemUnlockCount(), 0);
213+
assert.equal(bridgeHarness.getRuntimeState().locked, true);
214+
assert.equal(dom.document.getElementById("unlocked-content"), null);
215+
216+
const button = [...dom.document.querySelectorAll("button")]
217+
.find((candidate) => /Unlock with Touch ID/i.test(candidate.textContent ?? ""));
218+
assert.ok(button);
219+
} finally {
220+
dom.window.netcatty = previousWindowNetcatty;
221+
await renderer.unmount();
222+
dom.cleanup();
223+
}
224+
});
225+
155226
test("background-locked gate waits for reopen before automatic system unlock", async () => {
156227
const dom = installDomEnvironment();
157228
const renderer = await createDomRenderer(dom.document);
@@ -181,6 +252,7 @@ test("background-locked gate waits for reopen before automatic system unlock", a
181252
enabled: true,
182253
timeoutMinutes: 15,
183254
systemUnlockEnabled: true,
255+
systemUnlockAutoPromptEnabled: true,
184256
passwordVerifier: {
185257
version: 1,
186258
algorithm: "PBKDF2-SHA256",
@@ -256,6 +328,7 @@ test("background-locked gate retries automatic system unlock on each reopen whil
256328
enabled: true,
257329
timeoutMinutes: 15,
258330
systemUnlockEnabled: true,
331+
systemUnlockAutoPromptEnabled: true,
259332
passwordVerifier: {
260333
version: 1,
261334
algorithm: "PBKDF2-SHA256",

components/AppLockGate.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export function createAppLockGate(deps: AppLockGateDeps): React.FC<AppLockGatePr
124124
systemUnlockStatus={appLock.systemUnlockStatus}
125125
onSystemUnlock={appLock.unlockWithSystemAuth}
126126
onResetAppLock={appLock.reset}
127+
autoPromptSystemUnlock={settings.appLockSettings.systemUnlockAutoPromptEnabled}
127128
reopenSignal={reopenSignal}
128129
/>
129130
</TooltipProvider>

0 commit comments

Comments
 (0)