Skip to content

Commit

Permalink
feat: user friendly permission modal on macos
Browse files Browse the repository at this point in the history
  • Loading branch information
thewh1teagle committed May 7, 2024
1 parent 7c4fd3f commit f9d9664
Show file tree
Hide file tree
Showing 13 changed files with 270 additions and 27 deletions.
73 changes: 60 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"$schema": "../gen/schemas/capabilities.json",
"identifier": "default",
"description": "default permissions",
"local": true,
Expand All @@ -14,6 +15,7 @@
"menu:default",
"tray:default",
"shell:allow-open",
"shell:default"
"shell:default",
"os:allow-platform"
]
}
8 changes: 8 additions & 0 deletions desktop/src-tauri/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@ pub async fn package_info() -> Result<Value, String> {
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<bool, String> {
use crate::permissions;
log::debug!("show_prompt: {}", show_prompt);
permissions::check_accessibility(show_prompt).map_err(|e| e.to_string())
}
12 changes: 11 additions & 1 deletion desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
27 changes: 27 additions & 0 deletions desktop/src-tauri/src/permissions.rs
Original file line number Diff line number Diff line change
@@ -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<bool, Box<dyn Error>> {
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)
}
9 changes: 9 additions & 0 deletions desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +21,7 @@ function App() {
console.log("localstorage id => ", id);
const { message, status } = usePeer(id, true);
const qrDiv = useRef<HTMLDivElement>(null);
const { isAllowed, checkPermission } = useAccessibiltyPermission();

async function getVersion() {
console.log("getting version");
Expand Down Expand Up @@ -72,6 +75,12 @@ function App() {

return (
<div className="flex flex-col w-[100vw] h-[100vh] items-center justify-center">
{!isAllowed && (
<PermissionModal
checkPermission={checkPermission}
allowed={isAllowed}
/>
)}
<span className="text-3xl mb-5">Ready to connect</span>
<div ref={qrDiv} />
{status === "INIT" && (
Expand Down
30 changes: 30 additions & 0 deletions desktop/src/components/PermissionModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<dialog className={cx("modal", !granted && "modal-open")}>
<div className="modal-box">
<h3 className="font-bold text-lg">Accessibility</h3>
<p className="py-4">
Mobslide app lets you use your phone as a remote for presentations.
Just grant accessibility permission.
</p>
<div className="modal-action">
<button
className="btn btn-primary"
onClick={() => checkPermission(true)}
>
Continue
</button>
</div>
</div>
</dialog>
);
}
3 changes: 3 additions & 0 deletions desktop/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function cx(...cns: (boolean | string | undefined)[]): string {
return cns.filter(Boolean).join(" ");
}
43 changes: 43 additions & 0 deletions desktop/src/useAccessibilityPermission.tsx
Original file line number Diff line number Diff line change
@@ -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<boolean>();
const intervalIdRef = useRef<number>();

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 };
}
Loading

0 comments on commit f9d9664

Please sign in to comment.