Skip to content

Commit 08afa27

Browse files
committed
Merge branch 'main' into feat/editor-transcription-export
2 parents 2171ac9 + d8e5fe0 commit 08afa27

20 files changed

Lines changed: 1455 additions & 984 deletions

File tree

apps/desktop/src-tauri/src/permissions.rs

Lines changed: 336 additions & 118 deletions
Large diffs are not rendered by default.

apps/desktop/src/routes/(window-chrome)/new-main/useRequestPermission.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useQueryClient } from "@tanstack/solid-query";
22
import { getCurrentWindow } from "@tauri-apps/api/window";
33
import { devicesSnapshot } from "~/utils/devices";
4+
import { requestAndVerifyPermission } from "~/utils/os-permissions";
45
import { commands, type OSPermissionStatus } from "~/utils/tauri";
56

67
export default function useRequestPermission() {
@@ -11,22 +12,10 @@ export default function useRequestPermission() {
1112
currentStatus?: OSPermissionStatus,
1213
) {
1314
try {
14-
if (currentStatus === "denied") {
15-
await commands.openPermissionSettings(type);
16-
return;
17-
}
18-
1915
const window = getCurrentWindow();
2016
await window.setAlwaysOnTop(false);
2117
try {
22-
await commands.requestPermission(type);
23-
24-
const check = await commands.doPermissionsCheck(false);
25-
const status = type === "camera" ? check.camera : check.microphone;
26-
27-
if (status !== "granted") {
28-
await commands.openPermissionSettings(type);
29-
}
18+
await requestAndVerifyPermission(commands, type, currentStatus);
3019
} finally {
3120
await window.setAlwaysOnTop(true);
3221
}

apps/desktop/src/routes/(window-chrome)/onboarding.tsx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import {
1818
} from "solid-js";
1919
import { createStore } from "solid-js/store";
2020
import { generalSettingsStore } from "~/store";
21+
import {
22+
isPermissionGranted as isPermitted,
23+
requestAndVerifyPermission,
24+
} from "~/utils/os-permissions";
2125
import {
2226
commands,
2327
type OSPermission,
@@ -108,10 +112,6 @@ const modes: ModeDetail[] = [
108112
},
109113
];
110114

111-
function isPermitted(status?: OSPermissionStatus): boolean {
112-
return status === "granted" || status === "notNeeded";
113-
}
114-
115115
type SetupPermission = {
116116
name: string;
117117
key: OSPermission;
@@ -2001,19 +2001,16 @@ function PermissionsStep(props: {
20012001
if (requestingPermission()) return;
20022002
setRequestingPermission(true);
20032003
try {
2004-
await commands.requestPermission(permission);
2004+
const status = check()?.[permission] as OSPermissionStatus | undefined;
20052005
setInitialCheck(false);
2006-
const result = await commands.doPermissionsCheck(false);
2007-
setCheck(result as unknown as Record<string, OSPermissionStatus>);
2008-
const notYetPermitted =
2009-
(permission === "screenRecording" &&
2010-
!isPermitted(result.screenRecording)) ||
2011-
(permission === "accessibility" && !isPermitted(result.accessibility));
2012-
if (notYetPermitted) {
2013-
await commands.openPermissionSettings(permission);
2014-
if (permission === "screenRecording") {
2015-
await maybePromptRestartForScreenRecording();
2016-
}
2006+
const result = await requestAndVerifyPermission(
2007+
commands,
2008+
permission,
2009+
status,
2010+
);
2011+
setCheck(result.check as unknown as Record<string, OSPermissionStatus>);
2012+
if (result.openedSettings && permission === "screenRecording") {
2013+
await maybePromptRestartForScreenRecording();
20172014
}
20182015
} catch (err) {
20192016
console.error(`Error requesting permission: ${err}`);
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import {
4+
isPermissionGranted,
5+
permissionStatusFor,
6+
requestAndVerifyPermission,
7+
} from "~/utils/os-permissions";
8+
9+
describe("os-permissions", () => {
10+
it("treats only granted and not-needed statuses as permitted", () => {
11+
expect(isPermissionGranted("granted")).toBe(true);
12+
expect(isPermissionGranted("notNeeded")).toBe(true);
13+
expect(isPermissionGranted("empty")).toBe(false);
14+
expect(isPermissionGranted("denied")).toBe(false);
15+
});
16+
17+
it("maps a permission key to the matching OS permission status", () => {
18+
const check = {
19+
screenRecording: "granted",
20+
microphone: "empty",
21+
camera: "denied",
22+
accessibility: "notNeeded",
23+
} as const;
24+
25+
expect(permissionStatusFor(check, "screenRecording")).toBe("granted");
26+
expect(permissionStatusFor(check, "microphone")).toBe("empty");
27+
expect(permissionStatusFor(check, "camera")).toBe("denied");
28+
expect(permissionStatusFor(check, "accessibility")).toBe("notNeeded");
29+
});
30+
31+
it("does not open settings after a successful permission request", async () => {
32+
const client = {
33+
requestPermission: vi.fn().mockResolvedValue(undefined),
34+
openPermissionSettings: vi.fn().mockResolvedValue(undefined),
35+
doPermissionsCheck: vi.fn().mockResolvedValue({
36+
screenRecording: "empty",
37+
microphone: "granted",
38+
camera: "empty",
39+
accessibility: "empty",
40+
}),
41+
};
42+
43+
const result = await requestAndVerifyPermission(client, "microphone");
44+
45+
expect(client.requestPermission).toHaveBeenCalledWith("microphone");
46+
expect(client.openPermissionSettings).not.toHaveBeenCalled();
47+
expect(result.status).toBe("granted");
48+
expect(result.openedSettings).toBe(false);
49+
});
50+
51+
it("opens settings when the OS still reports the permission as ungranted", async () => {
52+
const client = {
53+
requestPermission: vi.fn().mockResolvedValue(undefined),
54+
openPermissionSettings: vi.fn().mockResolvedValue(undefined),
55+
doPermissionsCheck: vi.fn().mockResolvedValue({
56+
screenRecording: "denied",
57+
microphone: "empty",
58+
camera: "empty",
59+
accessibility: "empty",
60+
}),
61+
};
62+
63+
const result = await requestAndVerifyPermission(client, "screenRecording");
64+
65+
expect(client.requestPermission).toHaveBeenCalledWith("screenRecording");
66+
expect(client.openPermissionSettings).toHaveBeenCalledWith(
67+
"screenRecording",
68+
);
69+
expect(result.status).toBe("denied");
70+
expect(result.openedSettings).toBe(true);
71+
});
72+
73+
it("skips the native request and goes straight to settings for denied permissions", async () => {
74+
const client = {
75+
requestPermission: vi.fn().mockResolvedValue(undefined),
76+
openPermissionSettings: vi.fn().mockResolvedValue(undefined),
77+
doPermissionsCheck: vi.fn().mockResolvedValue({
78+
screenRecording: "empty",
79+
microphone: "empty",
80+
camera: "empty",
81+
accessibility: "denied",
82+
}),
83+
};
84+
85+
const result = await requestAndVerifyPermission(
86+
client,
87+
"accessibility",
88+
"denied",
89+
);
90+
91+
expect(client.requestPermission).not.toHaveBeenCalled();
92+
expect(client.openPermissionSettings).toHaveBeenCalledWith("accessibility");
93+
expect(result.status).toBe("denied");
94+
expect(result.openedSettings).toBe(true);
95+
});
96+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import type {
2+
OSPermission,
3+
OSPermissionStatus,
4+
OSPermissionsCheck,
5+
} from "~/utils/tauri";
6+
7+
export function isPermissionGranted(status?: OSPermissionStatus): boolean {
8+
return status === "granted" || status === "notNeeded";
9+
}
10+
11+
export function permissionStatusFor(
12+
check: OSPermissionsCheck,
13+
permission: OSPermission,
14+
): OSPermissionStatus {
15+
switch (permission) {
16+
case "screenRecording":
17+
return check.screenRecording;
18+
case "microphone":
19+
return check.microphone;
20+
case "camera":
21+
return check.camera;
22+
case "accessibility":
23+
return check.accessibility;
24+
}
25+
}
26+
27+
type PermissionClient = {
28+
requestPermission: (permission: OSPermission) => Promise<void>;
29+
openPermissionSettings: (permission: OSPermission) => Promise<void>;
30+
doPermissionsCheck: (initialCheck: boolean) => Promise<OSPermissionsCheck>;
31+
};
32+
33+
export type PermissionRequestResult = {
34+
check: OSPermissionsCheck;
35+
status: OSPermissionStatus;
36+
openedSettings: boolean;
37+
};
38+
39+
export async function requestAndVerifyPermission(
40+
client: PermissionClient,
41+
permission: OSPermission,
42+
currentStatus?: OSPermissionStatus,
43+
): Promise<PermissionRequestResult> {
44+
if (currentStatus === "denied") {
45+
await client.openPermissionSettings(permission);
46+
const check = await client.doPermissionsCheck(false);
47+
return {
48+
check,
49+
status: permissionStatusFor(check, permission),
50+
openedSettings: true,
51+
};
52+
}
53+
54+
await client.requestPermission(permission);
55+
56+
const check = await client.doPermissionsCheck(false);
57+
const status = permissionStatusFor(check, permission);
58+
59+
if (isPermissionGranted(status)) {
60+
return {
61+
check,
62+
status,
63+
openedSettings: false,
64+
};
65+
}
66+
67+
await client.openPermissionSettings(permission);
68+
69+
return {
70+
check,
71+
status,
72+
openedSettings: true,
73+
};
74+
}

0 commit comments

Comments
 (0)