Google Workspace
Multi-app support, OAuth scopes, and context detection
ShipAddons supports all major Google Workspace applications from a single codebase.
Supported Applications
| Application | Script Type | Context Detection |
|---|---|---|
| Google Sheets | sheets | SpreadsheetApp.getActiveSpreadsheet() |
| Google Docs | docs | DocumentApp.getActiveDocument() |
| Google Slides | slides | SlidesApp.getActivePresentation() |
| Google Forms | forms | FormApp.getActiveForm() |
OAuth Scopes
The add-on requires specific OAuth scopes defined in appsscript.json:
{
"oauthScopes": [
"openid",
"https://www.googleapis.com/auth/script.container.ui",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/documents.currentonly",
"https://www.googleapis.com/auth/spreadsheets.currentonly",
"https://www.googleapis.com/auth/presentations.currentonly",
"https://www.googleapis.com/auth/forms.currentonly"
]
}Scope Reference
| Scope | Purpose | Required |
|---|---|---|
openid | Enables ScriptApp.getIdentityToken() for authentication | Yes |
script.container.ui | Show sidebar and dialog UI elements | Yes |
userinfo.email | Access user's email address | Yes |
userinfo.profile | Access user's name and profile picture | Yes |
script.external_request | Make HTTP requests to your backend via UrlFetchApp | Yes |
documents.currentonly | Read/write the currently open Google Doc | For Docs |
spreadsheets.currentonly | Read/write the currently open Google Sheet | For Sheets |
presentations.currentonly | Read/write the currently open Google Slides | For Slides |
forms.currentonly | Read/write the currently open Google Form | For Forms |
Minimal vs Full Access
The .currentonly suffix limits access to only the document the user has open. This is a privacy best practice:
✓ spreadsheets.currentonly → Can only access the open spreadsheet
✗ spreadsheets → Can access ALL user's spreadsheetsUsers are more likely to authorize add-ons with minimal permissions.
Multi-App Architecture
Separate Clasp Configurations
Each Google app type requires its own Apps Script project:
addon/
├── .clasp-sheets.json # Sheets project
├── .clasp-docs.json # Docs project
├── .clasp-slides.json # Slides project
└── .clasp-forms.json # Forms projectThe setup script creates these configurations:
yarn run setup
# Prompts for which app types to configureScript Commands
| Command | Description |
|---|---|
yarn run push | Push to all configured apps |
yarn run push:sheets | Push to Sheets only |
yarn run push:docs | Push to Docs only |
yarn run open | Open all projects in browser |
yarn run open:sheets | Open Sheets project |
Shared Codebase
All app types share the same source code. The built output is pushed to each Apps Script project.
src/
├── client/ # React UI (shared)
├── server/ # Apps Script functions (shared)
└── ...Context Detection
The add-on detects which Google app it's running in:
// addon/src/server/utils.ts
export function detectScriptContext(): ScriptContextType {
try {
const ui = SpreadsheetApp.getUi();
if (ui) {
return "sheets";
}
} catch (_eSheet) {
try {
const docUi = DocumentApp.getUi();
if (docUi) {
return "docs";
}
} catch (_eDoc) {
try {
const slideUi = SlidesApp.getUi();
if (slideUi) {
return "slides";
}
} catch (_eSlide) {
try {
const formUi = FormApp.getUi();
if (formUi) {
return "forms";
}
} catch (_eForm) {
throw new Error("Unknown script context");
}
}
}
}
throw new Error("Unknown script context");
}Client-Side Usage
import serverFunctions from "../serverFunctions";
function Home() {
const [context, setContext] = useState<string>("");
useEffect(() => {
serverFunctions.detectScriptContext().then(setContext);
}, []);
// Render context-specific UI
switch (context) {
case "sheets":
return <HomeSheets />;
case "docs":
return <HomeDocs />;
case "slides":
return <HomeSlides />;
case "forms":
return <HomeForms />;
default:
return <HomeGeneric />;
}
}Per-App Home Components
Each Google app can have its own Home component:
components/
├── Home.tsx # Main router
├── sheets/
│ └── HomeSheets.tsx # Sheets-specific UI
├── docs/
│ └── HomeDocs.tsx # Docs-specific UI
├── slides/
│ └── HomeSlides.tsx # Slides-specific UI
└── forms/
└── HomeForms.tsx # Forms-specific UIExample: Sheets-Specific Features
// components/sheets/HomeSheets.tsx
import serverFunctions from "../../serverFunctions";
export function HomeSheets() {
const handleGetRange = async () => {
const data = await serverFunctions.getSelectedRange();
console.log("Selected range:", data);
};
return (
<div>
<h2>Sheets Add-on</h2>
<Button onClick={handleGetRange}>Get Selected Range</Button>
</div>
);
}With corresponding server function:
// server/sheets.ts
export function getSelectedRange(): string[][] {
const sheet = SpreadsheetApp.getActiveSpreadsheet();
const range = sheet.getActiveRange();
return range.getValues();
}Menu Integration
The add-on adds a menu to each Google app:
// server/ui.ts
function getActiveUiForContext(context?: ScriptContextType) {
let ctx = context;
if (!context) {
ctx = detectScriptContext();
}
switch (ctx) {
case "docs":
return DocumentApp.getUi();
case "slides":
return SlidesApp.getUi();
case "sheets":
return SpreadsheetApp.getUi();
case "forms":
return FormApp.getUi();
default:
throw new Error("Unknown script context");
}
}
export const onOpen = () => {
const menu = getActiveUiForContext().createAddonMenu();
menu.addItem("Sidebar", "openSidebar");
menu.addSeparator();
menu.addItem("Dialog", "openDialog");
menu.addToUi();
};UI Elements
Sidebar
Full-height panel on the right side of the document:
export const openSidebar = () => {
const html =
HtmlService.createHtmlOutputFromFile("sidebar").setTitle(EXTENSION_NAME);
getActiveUiForContext().showSidebar(html);
};Sidebar constraints:
- Fixed width (~300px)
- Full document height
- Persists while document is open
Dialog
Modal window centered on screen:
export const openDialog = () => {
const html = HtmlService.createHtmlOutputFromFile("dialog")
.setWidth(1280)
.setHeight(720);
getActiveUiForContext().showModalDialog(html, EXTENSION_NAME);
};Dialog constraints:
- Custom width/height
- Blocks interaction with document
- Closes on user action