Google Apps Script: Creating Stateful Gmail Add-Ons
Hey there!— I’ve typed up a bit of context and story around my discovery of Google Apps Script and why I started using it. If you’re interested, keep reading. Otherwise scroll down for the “Volunteer Registration” demo and tutorial.
I spent the first trimester of 2018 exploring Flutter and Dart, Google’s cross platform solution for native app development. About a month into Flutter, I decided to seek mentorship from local developers. Through outreach to Google’s flutter mailing list, I managed to connect with the local GDG chapter and gain sponsorship for a Portland Flutter Meetup that’s now running monthly and will remain free to the public.
My experience working with Google’s Flutter team was excellent, so I asked my employer for permission to spearhead development on a native app at our company. I was given a task that was, as they say in Thailand, same same but different.
I learned that we would be developing a Gmail Add-On for our appointment scheduling software. This was my first time hearing about Add-ons; they are micro-programs that run inside Gmail and Google Drive services like Sheets, Docs, Slides, and Forms. I was also unfamiliar with the programming language, Google Apps Script, and the IDE that supports it.
“Gaoogle Apps Script is a JavaScript cloud scripting language that provides easy ways to automate tasks across Google products and third party services and build web applications.”
Gmail Add-Ons dev tools are free and come with solid documentation, plus three sample applications to draw from. As a javascript developer, I found Apps Script to be intuitive and easy to use.
PART ONE — Preparations
First Steps to Start Programming for Gmail Add-Ons
If this is your first time using Google Add-Ons, you can learn a lot of fundamentals from the Quick Start Tutorial on their website. You can develop Add-Ons locally or in-browser. If you want to develop locally, you will need Node and Clasp. Check out this project demo for instructions on local Add-On development.
I will be providing instructions for development in-browser, to keep the number of technologies involved to a minimum. You will need a Github account to clone this repository. Once you’ve become familiarized with the basics, follow these simple steps to begin the tutorial.
About the “Volunteer Registration” Demo App
Application Summary: In this demo, the user has signed up for a trade show and will be volunteering for a day or night shift on behalf of their company.
Application Features: This app showcases several Gmail Add-Ons features, including multi-card navigation, form input retrieval with persistent state (user props storage), and a “confirm” button that prints user data as a canned email response.
Application UI: The application consists of two cards (shown below) with a Universal Settings icon (triple dot) in the header for navigation.
Application UX: selects a company that they work for, which causes an employee list to appear in the dropdown menu. These settings are saved and the user navigates to a new card where they can choose to volunteer for a day or night shift. When finished, they confirm and the Add-On will create an email reply with canned response.
How To Install a Gmail Add-On Demo without Clasp
Step 1) Clone this “Gmail Add-On Stateful Demo” Github Repo
Step 2) Click here to: Create a new Google Script
Step 3) Within the IDE, click on “View” and select “Show manifest file” to reveal your current appscript.json file.
Step 4) Copy the appscript.json content from cloned repo and save it in this Google IDE’s appscript.json manifest file.
Step 5) Within appscript.json, copy and locate the oAuthScopes property. These scopes play an important role in application security. Google makes it clear that the scopes should be restricted to those that are of absolute necessity.
In this demo, the chosen oAuthScopes enable persistent state management (script.storage) and allow the app to create an email response. The enabledAdvancedServices expose the gmail addon libraries.
{
"timeZone": "GMT",
"dependencies": {
"enabledAdvancedServices": [{
"userSymbol": "Gmail",
"serviceId": "gmail",
"version": "v1"
}]
},
"oauthScopes": [ "https://www.googleapis.com/auth/script.storage", "https://www.googleapis.com/auth/gmail.addons.execute", "https://www.googleapis.com/auth/gmail.addons.current.message.metadata","https://www.googleapis.com/auth/gmail.addons.current.message.action","https://www.googleapis.com/auth/gmail.addons.current.action.compose"]...}
Step 6) Next, use File > New > Script File to create eight new script files, named after each of the .js files in your cloned repository (e.g. code.js, helpers.js, etc). Notice that the IDE uses a .gs extension rather than .js.
Step 7) Copy the .js code from each file into the corresponding .gs file. Save all of the files. Your interface should look something like this:
Step 8) Now that your app is configured, go to Publish > Deploy from manifest and fetch your Deployment ID from the Head Deployment. The following screenshots will walk you through it:
As shown below, you can simply click “Get ID” and then copy the Deployment ID. Hit the close buttons when you successfully copied the ID.
Step 9) Click here to: Open your Google Add-Ons settings
This infographic displays the Google Add-Ons setting screen. Follow along one step at a time; enable the developer add-ons, then paste the deployment ID and install the app. It should show up
Success!
If you followed all of the steps correctly, the app should install correctly and you are ready to begin using it. Head back to your Gmail account and open up an email from somebody. After a few seconds, an icon should open to the right of your message. This is your Add-On sidebar!
PART TWO — Understanding the Code
Now that we’ve got the application installed, we can start looking at the application itself. Let’s return to the appscript.json file:
"gmail": {
"name": "Volunteer Registration App",
"logoUrl": "https://www.gstatic.com/images/icons/material/system/1x/receipt_black_24dp.png",
"contextualTriggers": [{
"unconditional": {
},
"onTriggerFunction": "startApp"
}],...
The key property here is onTriggerfunction. Its value is set to the function that starts our whole app. You can call it anything, so I named it startApp. Open the code.gs file to locate the startApp function. If you want to view the Logger’s log files at any time, simple select View > Logs:
Throughout this tutorial, I’ve made my comments directly in the code. Please follow along to understand the logic behind each function:
function startApp () { // First, we establish the organizationID. There are two options.
// If organizationID already exists as a property in the state's storage, then it gets that value.
// Otherwise, we get the first organization object in the jsonData array.
var organizationID = userProperties.getProperty('organizationID') || organizationSample[0].id;
// Now that the organizationID var is established, we use setProperty to store that ID in our state.
userProperties.setProperty('organizationID', organizationID);
// Next we gather the array of members who share the organizationID of our current selected org.
var members = memberSample.filter(function(member){
return member.organization_id === organizationID
});
// logs are used throughout these docs for testing purposes.
Logger.log("first member on startApp: " + members[0].name)
// We get a current Member ID, either from the state or from the first member in our members array.
var memberID = userProperties.getProperty('currentMemberID') || members[0].id;
userProperties.setProperty('currentMemberID', memberID);
Logger.log("memberID on startApp: " + memberID)
// Finally, the scheduleOptions from appscript.json are filtered by organization id to provide our schedule array.
var schedules = scheduleOptions.filter(function(schedule){
return schedule.organization_id === organizationID
});
var scheduleID = userProperties.getProperty('currentScheduledID') || schedules[0].id;
userProperties.setProperty('currentScheduleID', scheduleID);
Logger.log("schedules[0].name on startApp: " + schedules[0].name);
Logger.log("schedules[1].name on startApp: " + schedules[1].name);
// return the function located in buildSettingsCard.gs, which loads the first card based on our stateful data
return buildSettingsCard()
}
This startApp function loads each time the app is booted up. It does not run when we navigate between pages. As you saw, we test for existing state in our userProperties store. If there is none, we set property defaults. At the end, we return a function called buildSettingsCard(), located in the .gs file of the same name.
// Build Settings Cardfunction buildSettingsCard() {
// create a new card
var card = CardService.newCardBuilder();
// Set name and header title on card
card.setName("SettingsCard").setHeader(CardService.newCardHeader().setTitle('Company Registration'));
// newCardSections are a that Widgets can be painted onto
var sectionSettings = CardService.newCardSection();
// get organization object
var organization = getObjectByID(organizationSample, userProperties.getProperty('organizationID'));
Logger.log("Current Organization on buildSettingsCard: " + organization.name);// get scheduleID
var schedules = scheduleOptions.filter(function(schedule){
return schedule.organization_id === organization.id
});
var scheduleID = userProperties.getProperty('currentScheduledID') || schedules[0].id;
userProperties.setProperty('currentScheduleID', scheduleID);
// get the current member associated with current organization
var currentMember = getObjectByID(memberSample, userProperties.getProperty('currentMemberID'));
Logger.log("Current Member Name on buildSettingsCard: " + currentMember.name);
// get currentMembers to be displayed on the member select menu
var currentMembers = memberSample.filter(function(member){
return member.organization_id === userProperties.getProperty('organizationID');
});
Logger.log("name of member 1 associated with current organization name on buildSettingsCard: " + currentMembers[0].name);
Logger.log("name of member 2 associated with current organization name on buildSettingsCard: " + currentMembers[1].name);
// Add widgets to the Settings card section
// Add the organization select menu widget to the card
sectionSettings.addWidget(getOrganizationSelectMenu(organizationSample, organization || organizationSample[0]));
// check that there are members associated with the current organization. If so, display the member select menu widget on the card
if (currentMembers.length > 0) {
sectionSettings.addWidget(getMembersSelectMenu(currentMembers, currentMember || memberSample[0]));
} else {
sectionSettings.addWidget(CardService.newTextParagraph().setText("No Members Available"));
};
// btnSaveSettings is a button located at the bottom of the card
// actSaveSettings a newAction that triggers a function called buildScheduleTypes.
// buildScheduleTypes is located in the corresponding buildScheduleTypes.gs file
var actSaveSettings = CardService.newAction()
.setFunctionName('buildScheduleTypes');
var btnSaveSettings = CardService.newTextButton()
.setText("Save")
.setOnClickAction(actSaveSettings);
// add the button
sectionSettings.addWidget(btnSaveSettings);// Return and build the card
return card.addSection(sectionSettings).build();
}
This is the first time where you see a new card built and widgets added to one of the card’s sections. CardService widgets are a core feature of Google Add-Ons. The final widget on this card is btnSaveSettings, which is configured to run the function buildScheduleTypes, located in the .gs file of the same name:
// Build Schedule Typesfunction buildScheduleTypes(e) {
var currentMemberID = userProperties.getProperty('currentMemberID');
Logger.log("current member id: " + currentMemberID);
// re-establish state variables for local scopevar currentSchedules = scheduleOptions.filter(function(schedule){
return schedule.organization_id === userProperties.getProperty('organizationID')
});
Logger.log("first schedule on currentSchedules: " + currentSchedules[0].name);
// create card and give it header title, establish section
var cardScheduleTypes = CardService.newCardBuilder()
.setName("ScheduleCard")
.setHeader(CardService.newCardHeader().setTitle("Schedule Types"));
var sectionSchedules = CardService.newCardSection();
// add widgets to card section for Schedule types
if (currentSchedules.length != 0) {
sectionSchedules.addWidget(getScheduleSelectMenu(currentSchedules));
} else {
sectionSchedules.addWidget(CardService.newTextParagraph().setText("No Schedule Types Available"));
}
// Print-to-Email Button (see createReply.gs)
var action = CardService.newAction().setFunctionName('createReply');
sectionSchedules.addWidget(CardService
.newTextButton()
.setText('Confirm')
.setComposeAction(action, CardService.ComposedEmailType.REPLY_AS_DRAFT));
return cardScheduleTypes.addSection(sectionSchedules).build();
}
Test Driven Development
Each action in the application is connected to a function with a series of data logs. You can test the app during a variety of stages: initial boot-up, selecting from dropdown menus, navigating to a page, using the universal settings link, and hitting the “confirm” button.
Try keeping your Gmail account open in one tab while you look at the Logs in your App Script IDE. This way you can tab back and forward. If you want to see how different methods are behaving and where they are getting called, these Logger.log() tests can be very helpful.
Wrapping it up:
The main purpose of this article has been to introduce you to Google Add-Ons and give you a working demo that showcases a combination of Select Forms with their REPLY_AS_DRAFT feature. If you want to get an even more granular understanding of the app, check out all the files in the application.
You can find more Google Add-On sample apps on Github here. If you enjoyed this article and want to read more by the author, please see the following Flutter Articles:
Thanks again for reading, and please feel free to leave comments below with any thoughts, feedback, suggestions, questions, or favorite Add-On!