Google Apps Script: Creating Stateful Gmail Add-Ons

Ezra Sandzer-Bell
ITNEXT
Published in
9 min readMay 21, 2018

--

Photo by rawpixel on Unsplash

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.

Image from Google Blog

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.

This is the two-card User Interface for the Volunteer Registration App (Google Apps Script Demo)

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.

First card: User has selected Oscar Peterson from Amazon, but did not click save yet.
Second card: User saved settings, selected Night Shift, and clicked Confirm. Email response is generated.

How To Install a Gmail Add-On Demo without Clasp

Step 1) Clone this “Gmail Add-On Stateful Demo” Github Repo

This is the cloned Add-On Demo, viewed in Atom Text Editor

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:

The eight script files (.gs) and appscript.json manifest files have all been created, updated, and saved.

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:

Select Publish > Deploy from manifest

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

Once you click install, Google will ask you to confirm that you trust the Developer of the Add-On.

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!

Click the icon shown above to open the demo app. You can also click “+” to explore other published Add-Ons!

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 scope
var 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:

Creating a Carousel with Flutter (Published by Flutter.IO)

Flutter: Pinterest-Style Photo Grids

Thanks again for reading, and please feel free to leave comments below with any thoughts, feedback, suggestions, questions, or favorite Add-On!

--

--