wait on screen recording permissions

This commit is contained in:
Arjun 2026-03-28 11:28:20 +05:30
parent 30e1785fe2
commit d30cb88651
4 changed files with 70 additions and 33 deletions

View file

@ -11,18 +11,18 @@ module.exports = {
icon: './icons/icon', // .icns extension added automatically
appBundleId: 'com.rowboat.app',
appCategoryType: 'public.app-category.productivity',
osxSign: {
batchCodesignCalls: true,
optionsForFile: () => ({
entitlements: path.join(__dirname, 'entitlements.plist'),
'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
}),
},
osxNotarize: {
appleId: process.env.APPLE_ID,
appleIdPassword: process.env.APPLE_PASSWORD,
teamId: process.env.APPLE_TEAM_ID
},
// osxSign: {
// batchCodesignCalls: true,
// optionsForFile: () => ({
// entitlements: path.join(__dirname, 'entitlements.plist'),
// 'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
// }),
// },
// osxNotarize: {
// appleId: process.env.APPLE_ID,
// appleIdPassword: process.env.APPLE_PASSWORD,
// teamId: process.env.APPLE_TEAM_ID
// },
// Since we bundle everything with esbuild, we don't need node_modules at all.
// These settings prevent Forge's dependency walker (flora-colossus) from trying
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.

View file

@ -1,4 +1,4 @@
import { ipcMain, BrowserWindow, shell, dialog } from 'electron';
import { ipcMain, BrowserWindow, shell, dialog, systemPreferences } from 'electron';
import { ipc } from '@x/shared';
import path from 'node:path';
import os from 'node:os';
@ -719,6 +719,16 @@ export function setupIpcHandlers() {
return { success: false, error: 'Unknown format' };
},
'meeting:checkScreenPermission': async () => {
if (process.platform !== 'darwin') return { granted: true };
const status = systemPreferences.getMediaAccessStatus('screen');
console.log('[meeting] Screen recording permission status:', status);
return { granted: status === 'granted' };
},
'meeting:openScreenRecordingSettings': async () => {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
return { success: true };
},
'meeting:summarize': async (_event, args) => {
const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson);
return { notes };

View file

@ -3417,9 +3417,9 @@ function App() {
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
const startMeetingAfterPermissions = useCallback(async () => {
setShowMeetingPermissions(false)
localStorage.setItem('meeting-permissions-acknowledged', '1')
const [checkingPermission, setCheckingPermission] = useState(false)
const startMeetingNow = useCallback(async () => {
const calEvent = pendingCalendarEventRef.current
pendingCalendarEventRef.current = undefined
const notePath = await meetingTranscription.start(calEvent)
@ -3429,6 +3429,23 @@ function App() {
}
}, [meetingTranscription, handleVoiceNoteCreated])
const handleCheckPermissionAndRetry = useCallback(async () => {
setCheckingPermission(true)
try {
const { granted } = await window.ipc.invoke('meeting:checkScreenPermission', null)
if (granted) {
setShowMeetingPermissions(false)
await startMeetingNow()
}
} finally {
setCheckingPermission(false)
}
}, [startMeetingNow])
const handleOpenScreenRecordingSettings = useCallback(async () => {
await window.ipc.invoke('meeting:openScreenRecordingSettings', null)
}, [])
const handleToggleMeeting = useCallback(async () => {
if (meetingTranscription.state === 'recording') {
await meetingTranscription.stop()
@ -3477,20 +3494,18 @@ function App() {
meetingNotePathRef.current = null
}
} else if (meetingTranscription.state === 'idle') {
// Show permissions modal on first use (macOS only — Windows works out of the box)
if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) {
setShowMeetingPermissions(true)
return
}
const calEvent = pendingCalendarEventRef.current
pendingCalendarEventRef.current = undefined
const notePath = await meetingTranscription.start(calEvent)
if (notePath) {
meetingNotePathRef.current = notePath
await handleVoiceNoteCreated(notePath)
// On macOS, check screen recording permission before starting
if (isMac) {
const result = await window.ipc.invoke('meeting:checkScreenPermission', null)
console.log('[meeting] Permission check result:', result)
if (!result.granted) {
setShowMeetingPermissions(true)
return
}
}
await startMeetingNow()
}
}, [meetingTranscription, handleVoiceNoteCreated])
}, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow])
handleToggleMeetingRef.current = handleToggleMeeting
// Listen for calendar block "join meeting & take notes" events
@ -4421,23 +4436,25 @@ function App() {
<Dialog open={showMeetingPermissions} onOpenChange={setShowMeetingPermissions}>
<DialogContent showCloseButton={false}>
<DialogHeader>
<DialogTitle>Meeting transcription setup</DialogTitle>
<DialogTitle>Screen recording permission required</DialogTitle>
<DialogDescription>
Rowboat needs <strong>Screen Recording</strong> permission to capture meeting audio from other apps (Zoom, Meet, etc.).
Rowboat needs <strong>Screen Recording</strong> permission to capture meeting audio from other apps (Zoom, Meet, etc.). This feature won't work without it.
</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm text-muted-foreground">
<p>To enable this:</p>
<ol className="list-decimal list-inside space-y-1.5">
<li>Open <strong>System Settings</strong> <strong>Privacy & Security</strong></li>
<li>Click <strong>Screen Recording</strong></li>
<li>Open <strong>System Settings</strong> <strong>Privacy & Security</strong> <strong>Screen Recording</strong></li>
<li>Toggle on <strong>Rowboat</strong></li>
<li>You may need to restart the app after granting permission</li>
</ol>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowMeetingPermissions(false)}>Cancel</Button>
<Button onClick={() => { void startMeetingAfterPermissions() }}>Continue</Button>
<Button variant="outline" onClick={() => { void handleOpenScreenRecordingSettings() }}>Open System Settings</Button>
<Button onClick={() => { void handleCheckPermissionAndRetry() }} disabled={checkingPermission}>
{checkingPermission ? 'Checking...' : 'Check Again'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -501,6 +501,16 @@ const ipcSchemas = {
mimeType: z.string(),
}),
},
'meeting:checkScreenPermission': {
req: z.null(),
res: z.object({
granted: z.boolean(),
}),
},
'meeting:openScreenRecordingSettings': {
req: z.null(),
res: z.object({ success: z.boolean() }),
},
'meeting:summarize': {
req: z.object({
transcript: z.string(),