Workaround for Multiple sign-in Issue
If the issue is not fixed, let's work around it with minimal code changes. Find below "copy paste" code for 2 approaches
1) only notifying the user about the problem;
2) a complete workaround for when possible.
Approach 1 is the simplest: notifying the user about multiple sign-in and giving instructions to sign-out and sign into 1 account only. Much has been written about this in the issue tracker, here and here.
Demo:
This approach is implemented in the Workspace Editor add-on Maps for Sheets.
Step-by-step instructions:
1. In the apps script (.gs) code, replace calls to
HtmlService.createHtmlOutput(...)/createHtmlOutputFromFile(...)
with
HtmlService.createTemplate(...)/createTemplateFromFile(...).evaluate()
2. Copy and paste into the apps script (.gs) code the following function:
function getUser(){
try{ return Session.getEffectiveUser().getEmail() || Session.getActiveUser().getEmail()}catch(e){}
}
3. If you set OAuth scopes manually, then add the following scope to appsscript.json:
https://www.googleapis.com/auth/userinfo.email
4. Copy and paste into your HTML (.html) templates the following script:
/**
* Multiple signed in accounts issue with Google Apps Script
* https://apps.myrout.es/msii
*
* Freely distributable under the MIT License.
* Copyright (c) 2021 Aleksei Lukash
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var google = { bak: google,
script: new Proxy({
bak: google.script,
run: new Proxy({
bak: google.script.run,
scriptId: '<?= ScriptApp.getScriptId() ?>',
userAuth: '<?= ScriptApp.getOAuthToken() ?>'},
{ get: (target, prop, self)=>
prop == 'disabled' || prop == 'bak'?
target[prop]:
!target.disabled && prop == 'withSuccessHandler' && target.scriptId && target.userAuth?
(successHandler)=>(target.successHandler = successHandler, self):
!target.disabled && prop == 'withFailureHandler' && target.scriptId && target.userAuth?
(failureHandler)=>(target.failureHandler = failureHandler, self):
!target.disabled && prop == 'withUserObject' && target.scriptId && target.userAuth?
(userObject)=>(target.userObject = userObject, self):
!target.disabled && target.scriptId && target.userAuth?
prop in target?
target[prop]:
(...params)=>googleScriptRun_(target.scriptId,target.userAuth,target.successHandler,target.failureHandler,prop,params,target.userObject):
target.bak[prop]
})
},
{ get: (target, prop)=> prop in target? target[prop]:target.bak[prop]})
};
async function googleScriptRun_(scriptId,userAuth,onSuccess,onError,func,params,userObject){
let options = {
method: 'POST',
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json',
"Authorization":"Bearer " + userAuth,
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify({ // body data type must match "Content-Type" header
"function": func, // script function name to call
"parameters": params, // function parameters
"devMode": false}), // If true and the user is an owner of the script, the script runs at the most recently saved version rather than the version deployed
}
let url = "https://script.googleapis.com/v1/scripts/"+scriptId+":run";
const response = await fetch(url,options);
response
.json()
.then(data => {
if(data.done == false || data.error){
if(onError)
onError(data.error.message || data.error.details[0].errorMessage);
}
else{
if(onSuccess)
onSuccess(data.response.result,userObject);
}
})
};
/**
* @param {function} [getUserNameFunction='getUser'] Required Google Script function returning effective user email. Example: getUserEmailFunction(){Session.getEffectiveUser().getEmail()}
* @param {function} Optional callback function to call after the initialization of the Google Script Run library is complete. Accepts: true - disabled, false - enabled, undefined - getUserEmailFunction call failed
*/
(function(getUserEmailFunction = 'getUser', onInit = (status)=>{if(!status)alert("This action is not supported when you are signed in to multiple accounts. Sign out and try again.")}){
if(!google.script.run.scriptId || !google.script.run.userAuth){
google.script.run.disabled = true;
if(onInit) onInit(google.script.run.disabled)
}
else
if(getUserEmailFunction)
google.script.run.bak
.withSuccessHandler((username) => { google.script.run.disabled = (username == '<?= Session.getEffectiveUser().getEmail() ?>');
if(onInit) onInit(google.script.run.disabled)})
.withFailureHandler(() => {if(onInit) onInit(google.script.run.disabled)})
[getUserEmailFunction]()
})()
</script>
Approach 2 to provide complete workaround is recommended for developers with some background in Google apps script and Workspace Editor Add-ons.
Synopsis:
In a browser window with multiple signed in Google accounts (u0, u1, ...), the apps script is running under the account (uN). Created in this application with the HtmlService sidebars and dialogs open under active default account (u0). All calls to application functions via google.script.run are also performed under the active default account (u0). This means that there are no mechanisms for communication between the HtmlService window (u0) and the application instance (uN). That is, the connection with app instance is lost.
Workaround:
Let's use the opportunity to execute the application functions NOT through google.script.run, but using the Apps Script API and OAuthToken on behalf of the user (uN) running the application.
Demo:
This approach is implemented in the Workspace Editor add-on Free icons by Icons8.
Requirements:
To read and write data to the active document from the window opened by the HtmlService, you must pass
document ID / document URL,
and other context information about target document like
sheet and range,
paragraph,
slide / layout.
Limitations:
In the apps script functions called from the HtmlService window, you cannot use functions that refer to the current state of the document under the user's session:
getActiveDocument()/getActiveSpreadsheet()/getActivePresentation()/getActiveForm()
You should use
openById(id)/openByUrl(url))
The project OAuth scopes should be updated accordingly from .currentonly document to all documents.
Step-by-step instructions:
4b. In the HTML (.html) templates the following line
(function(getUserEmailFunction = 'getUser', onInit = (status)=>{if(!status)alert("This action is not supported when you are signed in to multiple accounts. Sign out and try again.")}){
to be replaced by
(function(getUserEmailFunction = 'getUser', onInit = (status)=>{/*put your init code here*/}){
5. Review your GS code for compliance with the Requirements and Limitations above
6. Review the project OAuth scopes: remove .currentonly for documents related scopes
7. Enable Apps Script API in the Google Cloud Platform project linked with your app
8. Save new version of the application and deploy it as API executable with 'anyone' access to the script.
9. Optional: Create onInit callback function to process the result returned after initialization.
All application script function calls using google.script.run must be called after the implemented script has finished initializing.
Your comments, suggestions and improvements are welcome.
FAQ
What if after replacing
HtmlService.createHtmlOutput (...) / createHtmlOutputFromFile (...)
with
HtmlService.createTemplate (...) / createTemplateFromFile (...). Evaluate ()
getting an error in the sidebar or window?
This can happen in the case of compiled / minified HTML / JS code. In this case, use another method instead of a template to transfer data. The code is shown below:
GS:
...
cursorContext = JSON.stringify(cursorContext)
const scriptId = ScriptApp.getScriptId()
const oAuthToken = ScriptApp.getOAuthToken()
const userEmail = getUser()
const html = HtmlService
.createHtmlOutputFromFile('build/ui')
.setTitle('Free icons by Icons8')
.append(`<p id="scriptId" data-scriptId='${scriptId}' style="display: none"></p>`)
.append(`<p id="oAuthToken" data-oAuthToken='${oAuthToken}' style="display: none"></p>`)
.append(`<p id="userEmail" data-userEmail='${userEmail}' style="display: none"></p>`)
.append(`<p id="cursorContext" data-cursorContext='${cursorContext}' style="display: none"></p>`)
HTML/JS:
<script>
var cursorContext, scriptId, oAuthToken, userEmail;
window.onload = initParams
function initParams() {
cursorContext = document.getElementById("cursorContext").getAttribute("data-cursorContext")
scriptId = document.getElementById("scriptId").getAttribute("data-scriptId")
oAuthToken = document.getElementById("oAuthToken").getAttribute("data-oAuthToken")
userEmail = document.getElementById("userEmail").getAttribute("data-userEmail")
initGoogleScriptRun_()
}
</script>
<script>
/**
* Multiple signed in accounts issue with Google Apps Script
* https://apps.myrout.es/msii
*
* Freely distributable under the MIT License.
* Copyright (c) 2021 Aleksei Lukash
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
async function googleScriptRun_(scriptId,userAuth,onSuccess,onError,func,params,userObject){
let options = {
method: 'POST',
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'same-origin', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json',
"Authorization":"Bearer " + userAuth,
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
body: JSON.stringify({ // body data type must match "Content-Type" header
"function": func, // script function name to call
"parameters": params, // function parameters
"devMode": false}), // If true and the user is an owner of the script, the script runs at the most recently saved version rather than the version deployed
}
let url = "https://script.googleapis.com/v1/scripts/"+scriptId+":run";
const response = await fetch(url,options);
response
.json()
.then(data => {
if(data.done == false || data.error){
if(onError)
onError(data.error.message || data.error.details[0].errorMessage);
}
else{
if(onSuccess)
onSuccess(data.response.result,userObject);
}
})
};
/**
* @param {function} [getUserNameFunction='getUser'] Required Google Script function returning effective user email. Example: getUserEmailFunction(){Session.getEffectiveUser().getEmail()}
* @param {function} Optional callback function to call after the initialization of the Google Script Run library is complete. Accepts: true - disabled, false - enabled, undefined - getUserEmailFunction call failed
*/
function initGoogleScriptRun_(getUserEmailFunction = 'getUser', onInit = (status)=>{/*put your init code here*/}){
google = { bak: google,
script: new Proxy({
bak: google.script,
run: new Proxy({
bak: google.script.run,
scriptId: scriptId,
userAuth: oAuthToken},
{ get: (target, prop, self)=>
prop == 'disabled' || prop == 'bak'?
target[prop]:
!target.disabled && prop == 'withSuccessHandler' && target.scriptId && target.userAuth?
(successHandler)=>(target.successHandler = successHandler, self):
!target.disabled && prop == 'withFailureHandler' && target.scriptId && target.userAuth?
(failureHandler)=>(target.failureHandler = failureHandler, self):
!target.disabled && prop == 'withUserObject' && target.scriptId && target.userAuth?
(userObject)=>(target.userObject = userObject, self):
!target.disabled && target.scriptId && target.userAuth?
prop in target?
target[prop]:
(...params)=>googleScriptRun_(target.scriptId,target.userAuth,target.successHandler,target.failureHandler,prop,params,target.userObject):
target.bak[prop]
})
},
{ get: (target, prop)=> prop in target? target[prop]:target.bak[prop]})
};
if(!google.script.run.scriptId || !google.script.run.userAuth){
google.script.run.disabled = true;
if(onInit) onInit(google.script.run.disabled)
}
else
if(getUserEmailFunction)
google.script.run.bak
.withSuccessHandler((username) => { google.script.run.disabled = (username == userEmail);
if(onInit) onInit(google.script.run.disabled)})
.withFailureHandler(() => {if(onInit) onInit(google.script.run.disabled)})
[getUserEmailFunction]()
}
</script>