2026-04-02 13:44:57 +02:00
"use client" ;
import { useRouter } from "next/navigation" ;
2026-04-07 05:55:39 +05:30
import { useEffect , useState } from "react" ;
2026-04-02 13:44:57 +02:00
import { Logo } from "@/components/Logo" ;
import { Button } from "@/components/ui/button" ;
import { Spinner } from "@/components/ui/spinner" ;
2026-04-07 00:43:40 -07:00
import { useElectronAPI } from "@/hooks/use-platform" ;
2026-04-02 13:44:57 +02:00
type PermissionStatus = "authorized" | "denied" | "not determined" | "restricted" | "limited" ;
interface PermissionsStatus {
accessibility : PermissionStatus ;
2026-04-03 19:57:48 +02:00
screenRecording : PermissionStatus ;
2026-04-02 13:44:57 +02:00
}
2026-04-03 19:57:48 +02:00
const STEPS = [
{
id : "screen-recording" ,
title : "Screen Recording" ,
2026-04-07 05:55:39 +05:30
description :
2026-04-27 19:25:39 +02:00
"Lets SurfSense capture a region of your screen, full display, or browser (where supported) to attach to chat in Screenshot Assist, or to capture the full display from the composer." ,
2026-04-03 19:57:48 +02:00
action : "requestScreenRecording" ,
field : "screenRecording" as const ,
} ,
{
id : "accessibility" ,
title : "Accessibility" ,
2026-04-27 19:25:39 +02:00
description :
"Lets SurfSense bring the app to the foreground and work with the active application (for example Quick Assist) when you use desktop shortcuts." ,
2026-04-03 19:57:48 +02:00
action : "requestAccessibility" ,
field : "accessibility" as const ,
} ,
] ;
2026-04-02 13:44:57 +02:00
function StatusBadge ( { status } : { status : PermissionStatus } ) {
if ( status === "authorized" ) {
return (
< span className = "inline-flex items-center gap-1.5 text-xs font-medium text-green-700 dark:text-green-400" >
< span className = "h-2 w-2 rounded-full bg-green-500" / >
Granted
< / span >
) ;
}
if ( status === "denied" ) {
return (
< span className = "inline-flex items-center gap-1.5 text-xs font-medium text-amber-700 dark:text-amber-400" >
< span className = "h-2 w-2 rounded-full bg-amber-500" / >
Denied
< / span >
) ;
}
return (
< span className = "inline-flex items-center gap-1.5 text-xs font-medium text-muted-foreground" >
< span className = "h-2 w-2 rounded-full bg-muted-foreground/40" / >
Pending
< / span >
) ;
}
export default function DesktopPermissionsPage() {
const router = useRouter ( ) ;
2026-04-07 00:43:40 -07:00
const api = useElectronAPI ( ) ;
2026-04-02 13:44:57 +02:00
const [ permissions , setPermissions ] = useState < PermissionsStatus | null > ( null ) ;
useEffect ( ( ) = > {
2026-04-07 00:43:40 -07:00
if ( ! api ) return ;
2026-04-02 13:44:57 +02:00
let interval : ReturnType < typeof setInterval > | null = null ;
2026-04-03 19:57:48 +02:00
const isResolved = ( s : string ) = > s === "authorized" || s === "restricted" ;
2026-04-02 13:44:57 +02:00
const poll = async ( ) = > {
2026-04-07 00:43:40 -07:00
const status = await api . getPermissionsStatus ( ) ;
2026-04-02 13:44:57 +02:00
setPermissions ( status ) ;
2026-04-03 19:57:48 +02:00
if ( isResolved ( status . accessibility ) && isResolved ( status . screenRecording ) ) {
2026-04-02 13:44:57 +02:00
if ( interval ) clearInterval ( interval ) ;
}
} ;
poll ( ) ;
interval = setInterval ( poll , 2000 ) ;
2026-04-07 03:10:06 -07:00
return ( ) = > {
if ( interval ) clearInterval ( interval ) ;
} ;
2026-04-07 00:43:40 -07:00
} , [ api ] ) ;
2026-04-02 13:44:57 +02:00
2026-04-07 00:43:40 -07:00
if ( ! api ) {
2026-04-02 13:44:57 +02:00
return (
< div className = "h-screen flex items-center justify-center bg-background" >
< p className = "text-muted-foreground" > This page is only available in the desktop app . < / p >
< / div >
) ;
}
if ( ! permissions ) {
return (
< div className = "h-screen flex items-center justify-center bg-background" >
< Spinner size = "lg" / >
< / div >
) ;
}
2026-04-07 05:55:39 +05:30
const allGranted =
permissions . accessibility === "authorized" && permissions . screenRecording === "authorized" ;
2026-04-02 13:44:57 +02:00
2026-04-03 19:57:48 +02:00
const handleRequest = async ( action : string ) = > {
if ( action === "requestScreenRecording" ) {
2026-04-07 00:43:40 -07:00
await api . requestScreenRecording ( ) ;
2026-04-03 19:57:48 +02:00
} else if ( action === "requestAccessibility" ) {
2026-04-07 00:43:40 -07:00
await api . requestAccessibility ( ) ;
2026-04-03 19:57:48 +02:00
}
2026-04-02 13:44:57 +02:00
} ;
const handleContinue = ( ) = > {
if ( allGranted ) {
2026-04-07 00:43:40 -07:00
api . restartApp ( ) ;
2026-04-02 13:44:57 +02:00
}
} ;
const handleSkip = ( ) = > {
router . push ( "/dashboard" ) ;
} ;
return (
< div className = "h-screen flex flex-col items-center p-4 bg-background dark:bg-neutral-900 select-none overflow-hidden" >
< div className = "w-full max-w-lg flex flex-col min-h-0 h-full gap-6 py-8" >
{ /* Header */ }
< div className = "text-center space-y-3 shrink-0" >
< Logo className = "w-12 h-12 mx-auto" / >
< div className = "space-y-1" >
< h1 className = "text-2xl font-semibold tracking-tight" > System Permissions < / h1 >
< p className = "text-sm text-muted-foreground" >
2026-04-30 18:42:38 -07:00
SurfSense needs two macOS permissions for Screenshot Assist and for desktop features
that require focusing the app or the active application .
2026-04-02 13:44:57 +02:00
< / p >
< / div >
< / div >
2026-04-03 19:57:48 +02:00
{ /* Steps */ }
2026-04-02 13:44:57 +02:00
< div className = "rounded-xl border bg-background dark:bg-neutral-900 flex-1 min-h-0 overflow-y-auto px-6 py-6 space-y-6" >
2026-04-03 19:57:48 +02:00
{ STEPS . map ( ( step , index ) = > {
const status = permissions [ step . field ] ;
const isGranted = status === "authorized" ;
return (
< div
key = { step . id }
className = { ` rounded-lg border p-4 transition-colors ${
isGranted
? "border-green-200 bg-green-50/50 dark:border-green-900 dark:bg-green-950/20"
: "border-border"
} ` }
>
< div className = "flex items-start justify-between gap-3" >
< div className = "flex items-start gap-3" >
< span className = "flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-sm font-medium text-primary" >
{ isGranted ? "\u2713" : index + 1 }
< / span >
< div className = "space-y-1" >
< h3 className = "text-sm font-medium" > { step . title } < / h3 >
< p className = "text-xs text-muted-foreground" > { step . description } < / p >
< / div >
< / div >
< StatusBadge status = { status } / >
2026-04-03 16:10:52 +02:00
< / div >
2026-04-03 19:57:48 +02:00
{ ! isGranted && (
< div className = "mt-3 pl-10 space-y-2" >
< Button
size = "sm"
variant = "outline"
onClick = { ( ) = > handleRequest ( step . action ) }
className = "text-xs"
>
Open System Settings
< / Button >
{ status === "denied" && (
< p className = "text-xs text-amber-700 dark:text-amber-400" >
Toggle SurfSense on in System Settings to continue .
< / p >
) }
< p className = "text-xs text-muted-foreground" >
2026-04-07 05:55:39 +05:30
If SurfSense doesn & apos ; t appear in the list , click < strong > + < / strong > and
select it from Applications .
2026-04-03 19:57:48 +02:00
< / p >
< / div >
2026-04-02 13:44:57 +02:00
) }
< / div >
2026-04-03 19:57:48 +02:00
) ;
} ) }
2026-04-02 13:44:57 +02:00
< / div >
{ /* Footer */ }
< div className = "text-center space-y-3 shrink-0" >
{ allGranted ? (
< >
< Button onClick = { handleContinue } className = "text-sm h-9 min-w-[180px]" >
Restart & amp ; Get Started
< / Button >
< p className = "text-xs text-muted-foreground" >
A restart is needed for permissions to take effect .
< / p >
< / >
) : (
< >
< Button disabled className = "text-sm h-9 min-w-[180px]" >
2026-04-03 19:57:48 +02:00
Grant permissions to continue
2026-04-02 13:44:57 +02:00
< / Button >
< button
2026-04-07 03:10:06 -07:00
type = "button"
2026-04-02 13:44:57 +02:00
onClick = { handleSkip }
className = "block mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors"
>
Skip for now
< / button >
< / >
) }
< / div >
< / div >
< / div >
) ;
}