From f9d966458b4ac15bf0f4d770a80d01f5a0665d1a Mon Sep 17 00:00:00 2001 From: thewh1teagle <61390950+thewh1teagle@users.noreply.github.com> Date: Wed, 8 May 2024 01:13:48 +0300 Subject: [PATCH] feat: user friendly permission modal on macos --- Cargo.lock | 73 +++++++++++++++++---- desktop/package-lock.json | 9 +++ desktop/package.json | 1 + desktop/src-tauri/Cargo.toml | 7 +- desktop/src-tauri/capabilities/default.json | 4 +- desktop/src-tauri/src/cmd.rs | 8 +++ desktop/src-tauri/src/main.rs | 12 +++- desktop/src-tauri/src/permissions.rs | 27 ++++++++ desktop/src/App.tsx | 9 +++ desktop/src/components/PermissionModal.tsx | 30 +++++++++ desktop/src/lib/utils.ts | 3 + desktop/src/useAccessibilityPermission.tsx | 43 ++++++++++++ web/tailwind.config.js | 71 +++++++++++++++++--- 13 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 desktop/src-tauri/src/permissions.rs create mode 100644 desktop/src/components/PermissionModal.tsx create mode 100644 desktop/src/lib/utils.ts create mode 100644 desktop/src/useAccessibilityPermission.tsx diff --git a/Cargo.lock b/Cargo.lock index ff2d71c..586eb55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "accessibility-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf09354dda54177da27bcb8b9b83d8cab947db1dc1538a310a5e9da1c57fd4c2" +dependencies = [ + "core-foundation-sys", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -1018,6 +1027,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1768,15 +1787,17 @@ dependencies = [ name = "mobslide" version = "0.0.2" dependencies = [ + "accessibility-sys", + "core-foundation-sys", "enigo", "env_logger", "log", "serde_json", "tauri", "tauri-build", + "tauri-plugin-os", "tauri-plugin-shell", "webkit2gtk", - "window-shadows", ] [[package]] @@ -1971,6 +1992,17 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "os_info" +version = "3.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +dependencies = [ + "log", + "serde", + "windows-sys 0.52.0", +] + [[package]] name = "os_pipe" version = "1.1.5" @@ -2975,6 +3007,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3173,6 +3214,24 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-os" +version = "2.0.0-beta.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653c0dbf5f954c7614a5e93f5b548bbe4417b3e1aa553b31ed0b6a2656949459" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror", +] + [[package]] name = "tauri-plugin-shell" version = "2.0.0-beta.4" @@ -3991,18 +4050,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "window-shadows" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ff424735b1ac21293b0492b069394b0a189c8a463fb015a16dea7c2e221c08" -dependencies = [ - "cocoa", - "objc", - "raw-window-handle 0.5.2", - "windows-sys 0.48.0", -] - [[package]] name = "window-vibrancy" version = "0.5.0" diff --git a/desktop/package-lock.json b/desktop/package-lock.json index ea6ad64..b26ac28 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.2", "dependencies": { "@tauri-apps/api": "2.0.0-beta.11", + "@tauri-apps/plugin-os": "^2.0.0-beta.3", "@tauri-apps/plugin-shell": "^2.0.0-beta.3", "@uidotdev/usehooks": "^2.4.1", "peerjs": "^1.5.2", @@ -1326,6 +1327,14 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-os": { + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-beta.3.tgz", + "integrity": "sha512-kuTfns6z7z/RKAqij3293fnSsgRHlogO/SzNWziFDGHpijGUm/peH70Cv45LKvHw+7kEMX+nfHFWOZm8UEVy/w==", + "dependencies": { + "@tauri-apps/api": "2.0.0-beta.11" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.0.0-beta.3", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-beta.3.tgz", diff --git a/desktop/package.json b/desktop/package.json index bf32b33..e2fcf79 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tauri-apps/api": "2.0.0-beta.11", + "@tauri-apps/plugin-os": "^2.0.0-beta.3", "@tauri-apps/plugin-shell": "^2.0.0-beta.3", "@uidotdev/usehooks": "^2.4.1", "peerjs": "^1.5.2", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 64e972c..ce276dc 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -19,13 +19,16 @@ env_logger = "0.10" log = "0.4.21" tauri-plugin-shell = "2.0.0-beta.4" serde_json = "1.0.116" +tauri-plugin-os = "2.0.0-beta.4" [target.'cfg(target_os = "linux")'.dependencies] webkit2gtk = "*" -[target.'cfg(any(windows, target_os = "macos"))'.dependencies] -window-shadows = "0.2.2" +[target.'cfg(target_os = "macos")'.dependencies] +accessibility-sys = { version = "0.1.3" } +core-foundation-sys = { version = "0.8.6" } + [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index 2cf1a07..4ef944a 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -1,4 +1,5 @@ { + "$schema": "../gen/schemas/capabilities.json", "identifier": "default", "description": "default permissions", "local": true, @@ -14,6 +15,7 @@ "menu:default", "tray:default", "shell:allow-open", - "shell:default" + "shell:default", + "os:allow-platform" ] } \ No newline at end of file diff --git a/desktop/src-tauri/src/cmd.rs b/desktop/src-tauri/src/cmd.rs index 0b4495b..416c131 100644 --- a/desktop/src-tauri/src/cmd.rs +++ b/desktop/src-tauri/src/cmd.rs @@ -35,3 +35,11 @@ pub async fn package_info() -> Result { serde_json::json!({"semver": env!("CARGO_PKG_VERSION"), "commit": env!("COMMIT_HASH")}); Ok(info) } + +#[cfg(target_os = "macos")] +#[tauri::command] +pub fn check_accessibility_permission(show_prompt: bool) -> Result { + use crate::permissions; + log::debug!("show_prompt: {}", show_prompt); + permissions::check_accessibility(show_prompt).map_err(|e| e.to_string()) +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 71664ba..2db512a 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -5,16 +5,26 @@ mod cmd; #[cfg(target_os = "linux")] mod linux_webrtc; +#[cfg(target_os = "macos")] +mod permissions; + fn main() { env_logger::init(); tauri::Builder::default() + .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_shell::init()) .setup(|_app| { #[cfg(target_os = "linux")] linux_webrtc::enable_webrtc(_app.app_handle()); + Ok(()) }) - .invoke_handler(tauri::generate_handler![cmd::press, cmd::package_info]) + .invoke_handler(tauri::generate_handler![ + cmd::press, + cmd::package_info, + #[cfg(target_os = "macos")] + cmd::check_accessibility_permission + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/desktop/src-tauri/src/permissions.rs b/desktop/src-tauri/src/permissions.rs new file mode 100644 index 0000000..c6dd576 --- /dev/null +++ b/desktop/src-tauri/src/permissions.rs @@ -0,0 +1,27 @@ +use accessibility_sys::{kAXTrustedCheckOptionPrompt, AXIsProcessTrustedWithOptions}; +use core_foundation_sys::base::{CFRelease, TCFTypeRef}; +use core_foundation_sys::dictionary::{CFDictionaryAddValue, CFDictionaryCreateMutable}; +use core_foundation_sys::number::{kCFBooleanFalse, kCFBooleanTrue}; +use std::{error::Error, ptr}; + +pub fn check_accessibility(ask_if_not_allowed: bool) -> Result> { + let is_allowed; + unsafe { + let options = + CFDictionaryCreateMutable(ptr::null_mut(), 0, std::ptr::null(), std::ptr::null()); + let key = kAXTrustedCheckOptionPrompt; + let value = if ask_if_not_allowed { + kCFBooleanTrue + } else { + kCFBooleanFalse + }; + if !options.is_null() { + CFDictionaryAddValue(options, key.as_void_ptr(), value.as_void_ptr()); + is_allowed = AXIsProcessTrustedWithOptions(options); + CFRelease(options as *const _); + } else { + return Err("options is null".into()); + } + } + Ok(is_allowed) +} diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index d662ee1..3457ecf 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -4,9 +4,11 @@ import { useLocalStorage } from "@uidotdev/usehooks"; import { useEffect, useRef, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import successSvg from "./assets/success.svg"; +import PermissionModal from "./components/PermissionModal"; import { BASE_URL } from "./lib/config"; import { createQR } from "./lib/qr"; import { Action, usePeer } from "./lib/usePeer"; +import { useAccessibiltyPermission } from "./useAccessibilityPermission"; interface PkgInfo { semver: string; @@ -19,6 +21,7 @@ function App() { console.log("localstorage id => ", id); const { message, status } = usePeer(id, true); const qrDiv = useRef(null); + const { isAllowed, checkPermission } = useAccessibiltyPermission(); async function getVersion() { console.log("getting version"); @@ -72,6 +75,12 @@ function App() { return (
+ {!isAllowed && ( + + )} Ready to connect
{status === "INIT" && ( diff --git a/desktop/src/components/PermissionModal.tsx b/desktop/src/components/PermissionModal.tsx new file mode 100644 index 0000000..8f4fc64 --- /dev/null +++ b/desktop/src/components/PermissionModal.tsx @@ -0,0 +1,30 @@ +import { cx } from "../lib/utils"; + +interface PermissionModalProps { + allowed: boolean; + checkPermission: (showPrompt: boolean) => void; +} +export default function PermissionModal({ + allowed: granted, + checkPermission, +}: PermissionModalProps) { + return ( + +
+

Accessibility

+

+ Mobslide app lets you use your phone as a remote for presentations. + Just grant accessibility permission. +

+
+ +
+
+
+ ); +} diff --git a/desktop/src/lib/utils.ts b/desktop/src/lib/utils.ts new file mode 100644 index 0000000..a96cb5c --- /dev/null +++ b/desktop/src/lib/utils.ts @@ -0,0 +1,3 @@ +export function cx(...cns: (boolean | string | undefined)[]): string { + return cns.filter(Boolean).join(" "); +} diff --git a/desktop/src/useAccessibilityPermission.tsx b/desktop/src/useAccessibilityPermission.tsx new file mode 100644 index 0000000..6291348 --- /dev/null +++ b/desktop/src/useAccessibilityPermission.tsx @@ -0,0 +1,43 @@ +import { invoke } from "@tauri-apps/api/core"; +import * as os from "@tauri-apps/plugin-os"; +import { useEffect, useRef, useState } from "react"; + +export function useAccessibiltyPermission() { + const [isAllowed, setIsAllowed] = useState(true); + const isAllowedOnce = useRef(); + const intervalIdRef = useRef(); + + async function checkPermission(showPrompt = true) { + const platform = await os.platform(); + if (platform === "macos") { + const isAllowed = await await invoke("check_accessibility_permission", { + showPrompt, + }); + if (isAllowed) { + isAllowedOnce.current = true; + clearInterval(intervalIdRef.current); + } + setIsAllowed(isAllowed as boolean); + } + } + + async function init() { + const platform = await os.platform(); + + // update permission state every 1 sec without prompt + if (platform === "macos") { + checkPermission(false); + intervalIdRef.current = setInterval( + async () => checkPermission(false), + 1000 + ); + } + } + + useEffect(() => { + init(); + return () => clearInterval(intervalIdRef.current); + }, []); + + return { isAllowed, checkPermission }; +} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index e1a3afb..2fe485f 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,14 +1,65 @@ -import daisyui from 'daisyui' - /** @type {import('tailwindcss').Config} */ export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + darkMode: ["class", '[data-theme="dark"]'], theme: { - extend: {}, + extend: { + transitionProperty: { + height: "height", + }, + }, }, - plugins: [daisyui], -} - + plugins: [require("daisyui")], + daisyui: { + themes: [ + { + dark: { + "color-scheme": "dark", + primary: "#1565c0", + "primary-content": "#fff", + secondary: "#7b1fa2", + "secondary-content": "#fff", + accent: "#1F262E", + "accent-content": "#fff", + neutral: "#556474", + "neutral-content": "#B0B8C4", + "base-100": "#111419", + "base-200": "#1E272E", + "base-300": "#1F272A", + "base-content": "#ffffff", + info: "#01579b", + "info-content": "#ffffff", + success: "#1b5e20", + "success-content": "#ffffff", + warning: "#e65100", + "warning-content": "#ffffff", + error: "#c62828", + "error-content": "#ffffff", + }, + light: { + "color-scheme": "light", + primary: "#2A8DF1", + "primary-content": "#D9E9F8", + secondary: "#7b1fa2", + "secondary-content": "#D9E9F8", + accent: "#0B6BCB", + "accent-content": "#D9E9F8", + neutral: "#303740", + "neutral-content": "#434D5B", + "base-100": "#FEFEFF", + "base-200": "#F5FAFF", + "base-300": "#E5F6FD", + "base-content": "#111419", + info: "#03a9f4", + "info-content": "#ffffff", + success: "#4caf50", + "success-content": "#ffffff", + warning: "#ff9800", + "warning-content": "#ffffff", + error: "#ef5350", + "error-content": "#ffffff", + }, + }, + ], + }, +};