Skip to content

Commit 7496870

Browse files
author
Christopher Willis-Ford
committed
improve user experience around mic/camera permission
- ask for permission when trying to use a feature, not on startup - if permission is denied, explain the consequence and provide a hint for fixing it
1 parent b26c0b6 commit 7496870

File tree

1 file changed

+110
-18
lines changed

1 file changed

+110
-18
lines changed

src/main/index.js

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,48 @@ const isDevelopment = process.env.NODE_ENV !== 'production';
1818
// global window references prevent them from being garbage-collected
1919
const _windows = {};
2020

21-
const createWindow = ({search = null, url = 'index.html', ...browserWindowOptions}) => {
22-
const window = new BrowserWindow({
23-
useContentSize: true,
24-
show: false,
25-
webPreferences: {
26-
nodeIntegration: true
27-
},
28-
...browserWindowOptions
29-
});
30-
const webContents = window.webContents;
21+
const displayPermissionDeniedWarning = (browserWindow, permissionType) => {
22+
let title;
23+
let message;
24+
switch (permissionType) {
25+
case 'camera':
26+
title = 'Camera Permission Denied';
27+
message = 'Permission to use the camera has been denied. ' +
28+
'Scratch will not be able to take a photo or use video sensing blocks.';
29+
break;
30+
case 'microphone':
31+
title = 'Microphone Permission Denied';
32+
message = 'Permission to use the microphone has been denied. ' +
33+
'Scratch will not be able to record sounds or detect loudness.';
34+
break;
35+
default: // shouldn't ever happen...
36+
title = 'Permission Denied';
37+
message = 'A permission has been denied.';
38+
}
3139

32-
if (isDevelopment) {
33-
webContents.openDevTools({mode: 'detach', activate: true});
40+
let instructions;
41+
switch (process.platform) {
42+
case 'darwin':
43+
instructions = 'To change Scratch permissions, please check "Security & Privacy" in System Preferences.';
44+
break;
45+
default:
46+
instructions = 'To change Scratch permissions, please check your system settings and restart Scratch.';
47+
break;
3448
}
49+
message = `${message}\n\n${instructions}`;
50+
51+
dialog.showMessageBox(browserWindow, {type: 'warning', title, message});
52+
};
3553

36-
const fullUrl = formatUrl(isDevelopment ?
54+
/**
55+
* Build an absolute URL from a relative one, optionally adding search query parameters.
56+
* The base of the URL will depend on whether or not the application is running in development mode.
57+
* @param {string} url - the relative URL, like 'index.html'
58+
* @param {*} search - the optional "search" parameters (the part of the URL after '?'), like "route=about"
59+
* @returns {string} - an absolute URL as a string
60+
*/
61+
const makeFullUrl = (url, search = null) =>
62+
encodeURI(formatUrl(isDevelopment ?
3763
{ // Webpack Dev Server
3864
hostname: 'localhost',
3965
pathname: url,
@@ -47,7 +73,77 @@ const createWindow = ({search = null, url = 'index.html', ...browserWindowOption
4773
search,
4874
slashes: true
4975
}
50-
);
76+
));
77+
78+
const handlePermissionRequest = async (webContents, permission, callback, details) => {
79+
if (webContents !== _windows.main.webContents) {
80+
// deny: request came from somewhere other than the main window's web contents
81+
return callback(false);
82+
}
83+
if (!details.isMainFrame) {
84+
// deny: request came from a subframe of the main window, not the main frame
85+
return callback(false);
86+
}
87+
if (permission !== 'media') {
88+
// deny: request is for some other kind of access like notifications or pointerLock
89+
return callback(false);
90+
}
91+
const requiredBase = makeFullUrl('/');
92+
if (details.requestingUrl.indexOf(requiredBase) !== 0) {
93+
// deny: request came from a URL outside of our "sandbox"
94+
return callback(false);
95+
}
96+
let askForMicrophone = false;
97+
let askForCamera = false;
98+
for (const mediaType of details.mediaTypes) {
99+
switch (mediaType) {
100+
case 'audio':
101+
askForMicrophone = true;
102+
break;
103+
case 'video':
104+
askForCamera = true;
105+
break;
106+
default:
107+
// deny: unhandled media type
108+
return callback(false);
109+
}
110+
}
111+
const parentWindow = _windows.main; // if we ever allow media in non-main windows we'll also need to change this
112+
if (askForMicrophone) {
113+
const microphoneResult = await systemPreferences.askForMediaAccess('microphone');
114+
if (!microphoneResult) {
115+
displayPermissionDeniedWarning(parentWindow, 'microphone');
116+
return callback(false);
117+
}
118+
}
119+
if (askForCamera) {
120+
const cameraResult = await systemPreferences.askForMediaAccess('camera');
121+
if (!cameraResult) {
122+
displayPermissionDeniedWarning(parentWindow, 'camera');
123+
return callback(false);
124+
}
125+
}
126+
return callback(true);
127+
};
128+
129+
const createWindow = ({search = null, url = 'index.html', ...browserWindowOptions}) => {
130+
const window = new BrowserWindow({
131+
useContentSize: true,
132+
show: false,
133+
webPreferences: {
134+
nodeIntegration: true
135+
},
136+
...browserWindowOptions
137+
});
138+
const webContents = window.webContents;
139+
140+
webContents.session.setPermissionRequestHandler(handlePermissionRequest);
141+
142+
if (isDevelopment) {
143+
webContents.openDevTools({mode: 'detach', activate: true});
144+
}
145+
146+
const fullUrl = makeFullUrl(url, search);
51147
window.loadURL(fullUrl);
52148

53149
return window;
@@ -140,10 +236,6 @@ const createMainWindow = () => {
140236
if (process.platform === 'darwin') {
141237
const osxMenu = Menu.buildFromTemplate(MacOSMenu(app));
142238
Menu.setApplicationMenu(osxMenu);
143-
(async () => {
144-
await systemPreferences.askForMediaAccess('microphone');
145-
await systemPreferences.askForMediaAccess('camera');
146-
})();
147239
} else {
148240
// disable menu for other platforms
149241
Menu.setApplicationMenu(null);

0 commit comments

Comments
 (0)