During this pandemic, scheduling a meeting has become more important than ever, be it for a conference call, a consultation, or just a friendly chat.

In this guide, we'll explore how to use Google Calendar's API to make your own calendar booking app (yes, it's a platform, so other people can log in and share their own calendar link!).

Build a Calendar Booking App with Cotter, Cloudflare Workers, and Google API

Try out the live demo!

Calendar Booking App by Cotter
Calendar Booking App by Cotter

We'll use:

  • Cotter for logging in users and connecting to their Google Account.
  • Cloudflare Workers as a backend that will call Google Calendar API.
  • Google Calendar API for booking time slots in your users' calendar.
  • We'll also use React in this tutorial.

Why Cloudflare Workers?

Cloudflare Workersยฎ
Build your next application with Cloudflare Workers

Cloudflare workers allows you to deploy JavaScript code on Cloudflare's Edge network. This means you can write serverless code in Cloudflare workers and have it deployed across the globe. Your users will connect to the nearest Cloudflare network.

What about a database?

Cloudflare workers offers a Key-Value store that can keep consistent data across their locations. Note that currently, it's "eventually consistent" which means it might take some time (max 60s) for an update to be available globally.

Why do we need a backend?

The biggest reason why we need a backend in this tutorial is that we need a secure environment to store our API Secret Key which is used to get Google's Access Tokens from Cotter.

We don't need a database or a key-value store for our use case, so Cloudflare Workers is perfect for deploying a simple backend code that authenticates your users and calls Google's API

TLDR

To try it out right now, you can use our CodeSandbox and follow the steps below to set up the Cloudflare Backend:

1. Create a project at Cotter, then add Sign in with Google to your Login Form

2. Use this CodeSandbox to get the calendar-booking React App. Update src/constants.js with your Cotter API KEY ID. You can also clone the project from the Github Repo.

3. Setup Cloudflare Worker, make 3 workers with subdomains:

  • checkconnection.YOURSUBDOMAIN.workers.dev
  • createevent.YOURSUBDOMAIN.workers.dev
  • disconnect.YOURSUBDOMAIN.workers.dev

Copy the code from our Github for your Cloudflare Worker.

4. Enable Google Calendar API in your Google project

Enable Google Calendar API to the project that you used to create Google OAuth 2.0 Credentials.

How it works

Here's the outline of the things that we needed to do to build this platform:

  • Users will be using "Sign in with Google" to get an access token from Google. This access token is accessible via Cotter's API.
  • Make API endpoints at your Cloudflare Worker which would 1) call Cotter's API to get the user's Google Access Token, and 2) call Google's API using that Google access token.

Logging In

Getting Google's Access Token via Google Sign In

To access Google's Calendar API, you'll need a Google Access Token for the user to modify their calendar. This means the user needs to connect their Google Account. Using Cotter as your authentication service, this can be done in 2 ways:

  • If the user logged-in using Google Sign In, you can automatically access their Google Access Token by calling Cotter's API.
  • If the user logged-in with their email, you can provide a button on the dashboard and ask them to connect their Google Account. After that, you can access their Google Access Token by calling Cotter's API.
When your user logged-in using Cotter, you'll get a Cotter Access Token. We'll use this access token to protect our API Endpoints that we're going to make in Cloudflare Workers.

1) Adding Sign in with Google to your Login Form

  1. Follow these instructions to set up Google OAuth 2.0 Credentials and connect it to Cotter. Make sure you added https://www.googleapis.com/auth/calendar for the scope.
  2. Enable Google for your form: Go to Branding > Magic Link Form > click the checkmark to enable Google Sign In
  3. Add Cotter's Login Form to your website

Using Cotter's React SDK, you can easily use React context provider to keep track of the authentication state (i.e. is the user logged-in, etc).

yarn add cotter-react
import React from "react";
import { Router } from "@reach/router";
import LoginPage from "../login";
import { CotterProvider, LoginForm } from "cotter-react"; // ๐Ÿ‘ˆ  Import Cotter Provider

function App() {
  return (
    // ๐Ÿ‘‡ 1) Wrap CotterProvider around your ROOT COMPONENT
    // Copy paste your Cotter API Key ID below.
    <CotterProvider apiKeyID="<YOUR API KEY ID>">
      <Router>
        <LoginPage path="/" />
      </Router>
    </CotterProvider>
  );
}

function LoginPage() {
  return (
      <div className="LoginPage__form-container">
      {/* ๐Ÿ‘‡  2) Show the login form */}
      <LoginForm
          onSuccess={(response) => console.log(response)}
          onError={(err) => console.log(err)}
      />
      </div>
  );
}

export default App;

2) Checking if the user is logged-in in your dashboard

Using CotterContext you can check if the user is logged-in and get their Cotter Access Token.

import { CotterContext, withAuthenticationRequired } from "cotter-react"; // ๐Ÿ‘ˆ  Import the user context
import React, { useContext, useEffect, useState } from "react";
import "./styles.css";

function DashboardPage() {
  const { user, getAccessToken, isLoggedIn } = useContext(CotterContext); // Get the logged-in user information
  const [accessToken, setaccessToken] = useState(null);

  useEffect(() => {
    if (isLoggedIn) readAccessToken();
  }, [isLoggedIn]);
    
  // Get the Cotter access token to be used for our API call
  // (for now, we'll just display it in the dashboard)
  const readAccessToken = async () => {
    const token = await getAccessToken();
    setaccessToken(token?.token);
  };
  return (
      <div className="container">
      	User ID: {user?.ID} <br />
      	User email: {user?.identifier} <br />
        Cotter Access Token: {accessToken}
      </div>
  );
}

// Protect this page using the `withAuthenticationRequired` HOC
// If user is not logged-in, they'll be redirected to the `loginPagePath`
export default withAuthenticationRequired(DashboardPage, {
  loginPagePath: "/",
});
  • Wrap your component around withAuthenticationRequired to automatically redirect your user to login if the user is not logged-in
  • Use isLoggedIn to check if the user is logged-in, and user to get the logged-in user information.
  • Use getAccessToken to get the Cotter Access Token that we'll use to call our API endpoints.

3) Checking if the user has connected their Google Account

We'll make an endpoint for this on our Cloudflare Worker which will call Cotter's API to see if the user has connected their Google Account.

4) Show a button to connect Google Account from the Dashboard

If the user has not connected their Google Account (they didn't log in using Google), show a button to connect Google Account. Call this function with your button to connect their Google Account:

import { CotterContext } from "cotter-react"; // ๐Ÿ‘ˆ  Import the user context

function DashboardPage() {
  ...
  const { getCotter } = useContext(CotterContext);

  // If the user hasn't connected their Google Account,
  // show a button to call this function to connect
  const connectToGoogle = async () => {
    var cotter = getCotter();
    cotter.connectSocialLogin("GOOGLE", accessToken);
  };
    
  return (
    <div className="DashboardPage__container">
     ...
      <button onClick={connectToGoogle}>Connect Google Account</button>
    </div>
  );
}

Cloudflare Worker Endpoints

Making API endpoints at Cloudflare Workers to call Google's API

Once you got the login flow working, you can start making the endpoints at Cloudflare Workers that would call Google's API. Specifically, you'll need these endpoints:

  1. An API to check if the user has connected their Google Account
  2. An API to modify their Google Calendar

Create a Cloudflare Account and Setup Workers

  1. Go to Cloudflare and sign up with your email and password. You don't need to add a domain. When asked to add a domain, if you don't have a domain, you can skip this by clicking on the Cloudflare logo.
  2. Click Workers on the right side of your dashboard, and add a subdomain that you want for your backend API endpoint. Choose the Free Plan and verify your email.

Create a Test Worker to see how this works

  1. Press Create a Worker. You can see that you have a handler script on the left side, and a playground to test your API endpoint. Click Send and you should see hello world as the response.
  2. That's it, you've just created an endpoint! To save this endpoint, press Save and Deploy.

Creating Endpoints

Notice that you can only change the sub-subdomain, not the path.

To be able to handle different paths, you'll need to use their CLI and use a router. That's too advanced for this tutorial, so we'll just have our endpoints on different subdomains.

Endpoints

Endpoint #1: checkconnection

Check if the user has Google Connected.

Create a worker, and change the name to checkconnection . We'll define our endpoint as the following:

$ curl --request GET \
    --header 'Authorization: Bearer <COTTER_ACCESS_TOKEN>' \
    --url 'https://checkconnection.YOUR_SUBDOMAIN.workers.dev?userid=<COTTER_USER_ID>'
// If Google account is connected:
{"connected": true}

// If not:
{"connected": false}

Here are the things that we'll need to do:

Code & Explanations

Copy the code from our Github repo.

Click here to see the code and explanations

Add the Cotter Api Keys as an environment variables

Go to the Settings tab, and press Add Variable. Add both the API_KEY_ID and API_SECRET_KEY from Cotter, then press Save.
Screen-Shot-2020-11-02-at-3.40.14-AM

Helper Codes

Because we can't import packages, copy these code and paste it at the top of your script:

// ๐Ÿ‘‡ PASTE THE HELPER CODE TO VALIDATE JWT TOKEN
// ๐Ÿ‘‡ PASTE THE HELPER CODE TO ADD CORS HEADERS

async function handleWithCorsHeader(request) {
  const resp = await handleRequest(request)
  resp.headers.set('Access-Control-Allow-Origin', '*')
  return resp
}


addEventListener('fetch', (event) => {
  const request = event.request
  if (request.method === 'OPTIONS') {
    event.respondWith(handleOptions(request))
  } else {
    event.respondWith(handleWithCorsHeader(request))
  }
})

    
/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  // 1๏ธโƒฃ Check if the access token is valid
  try {
    const valid = await checkJWT(request, API_KEY_ID)
    if (!valid) throw new Error("Access token is invalid");
  } catch (e) {
    return new Response(e.message, {
      status: 401,
    })
  }
  
  // 2๏ธโƒฃ Call Cotter's API to see if the user has connected Google
  // get the Cotter User ID from the query parameter
  const { searchParams } = new URL(request.url)
  let userID = searchParams.get('user_id')
  const config = {
    headers: {
      'Content-Type': 'application/json',
      'API_KEY_ID': API_KEY_ID,
      'API_SECRET_KEY': API_SECRET_KEY,
    }
  }
  try {
    const response = await fetch(`https://www.cotter.app/api/v0/oauth/token/GOOGLE/${userID}`, config)
    if (response.status != 200) throw new Error("Token doesn't exist")
  } catch(e) {
    console.log(e)
    const body = JSON.stringify({ connected: false })
    return new Response(body, {
      status: 500,
    });
  }
  
  const body = JSON.stringify({ connected: true })
  return new Response(body, {status: 200})
}
  • The function checkJWT is included in the helper code, and it'll check if the Authorization header is valid and if the access token is valid
  • Variables API_KEY_ID and API_SECRET_KEY are globally available because you added it as environment variables.

Endpoint #2: createevent

Create a Google Calendar Event

Before we start, you'll need to Enable Google Calendar API to the project that you used to create Google OAuth 2.0 Credentials.

Create another worker, and change the name to createevent . We'll define our endpoint as the following:

$ curl --request POST \
  --header "Content-Type: application/json" \
  --data '{"start_time":"2020-11-02T10:00:00-08:00","end_time":"2020-11-02T10:15:00-08:00","attendee_email":"[email protected]","meeting_title":"John Doe <> Jane Doe"}' \
  --url 'https://createevent.YOUR_SUBDOMAIN.workers.dev?userid=<COTTER_USER_ID>'
  • start_time & end_time should be in formatted according to RFC3339
  • attendee_email is the email of the attendee (just 1 email)
  • meeting_title is the title of the meeting
{
    "success": true,
    "event": { ... Google's Event Obj }
}
Success Response
{
    "success": false,
    "error": "Error message"
}
Error Response

This URL doesn't require an authorization header because we want anyone to be able to book an appointment without having to log in to your platform first.

Here are the things that we'll need to do:

Code & Explanations

Copy the code from our Github repo.

Click here to see the code & explanations

Add the Cotter Api Keys and Google Credentials as an environment variables

  • Again, we need to add the API_KEY_ID and API_SECRET_KEY environment variables from Cotter API Keys.
  • We also need to add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variable from Google OAuth Credentials.

Screen-Shot-2020-11-02-at-5.36.21-PM

Helper Codes

// ๐Ÿ‘‡ PASTE THE HELPER CODE TO ADD CORS HEADERS
// ๐Ÿ‘‡ PASTE THE HELPER CODE TO ADD REFRESH TOKEN FUNCTION

async function handleWithCorsHeader(request) {
  const resp = await handleRequest(request)
  resp.headers.set('Access-Control-Allow-Origin', '*')
  return resp
}


addEventListener('fetch', (event) => {
  const request = event.request
  if (request.method === 'OPTIONS') {
    event.respondWith(handleOptions(request))
  } else {
    event.respondWith(handleWithCorsHeader(request))
  }
})

/**
 * Respond to the request
 * @param {Request} request
 */
async function handleRequest(request) {
  //1๏ธโƒฃ Call Cotter's API to see if the user has connected Google
  // get the Cotter User ID from the query parameter
  const { searchParams } = new URL(request.url)
  let userID = searchParams.get('user_id')
  let googleTokens;
  const config = {
    headers: {
      'Content-Type': 'application/json',
      'API_KEY_ID': API_KEY_ID,
      'API_SECRET_KEY': API_SECRET_KEY,
    }
  }
  try {
    const response = await fetch(`https://www.cotter.app/api/v0/oauth/token/GOOGLE/${userID}`, config)
    if (response.status != 200) throw new Error("Token doesn't exist")
    const resp = await response.json()
    googleTokens = resp.tokens;
  } catch(e) {
    console.log(e)
    const body = JSON.stringify({ success: false, error: "Google Account is not connected properly" })
    return new Response(body, {
      status: 500,
    });
  }
  
  // 2๏ธโƒฃ Call Google Event Insert API
  const req = await request.json();
  const eventRequest = {
      start: { dateTime: req.start_time },
      end: { dateTime: req.end_time },
      attendees: [{ email: req.attendee_email }],
      summary: req.meeting_title
  }
  const eventConfig = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${googleTokens.access_token}`,
    },
    body: JSON.stringify(eventRequest)
  }
  let eventResponse
    
  try {
    let eventResp = await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all", eventConfig)
    
    // If unauthorized, try refreshing the access token
    if (eventResp.status === 401) {
        googleTokens = await refreshGoogleToken(googleTokens.refresh_token)
        // Then calling the API again
        eventConfig.headers.Authorization = `Bearer ${googleTokens.access_token}`
        eventResp = await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events?sendUpdates=all", eventConfig)
    }
    
    eventResponse = await eventResp.json()
    if (eventResp.status != 200) throw new Error("Fail calling Google's Event API")
  } catch(e) {
    console.log(e)
    const body = JSON.stringify({ success: false, error: "Fail calling Google's event insert API" })
    return new Response(body, {
      status: 500,
    });
  }
  
  const body = JSON.stringify({ success: true, event: eventResponse })
  return new Response(body, {status: 200})
}

Endpoint #3: disconnect

Disconnect the user's Google Account

Create another worker, and change the name to disconnect . We'll define our endpoint as the following:

$ curl --request DELETE \
    --header 'Authorization: Bearer <COTTER_ACCESS_TOKEN>' \
    --url 'https://disconnect.YOUR_SUBDOMAIN.workers.dev?userid=<COTTER_USER_ID>'
// If disconnected successfully
{"success": true}

Here are the things that we'll need to do:

Code

Copy the code from our Github repo.

Add the Cotter Api Keys as environment variables

  • Again, we need to add the API_KEY_ID and API_SECRET_KEY environment variables from Cotter API Keys.

That's it!

You should now have your backend API set up and Google Sign In set up, all you need to do is connect them in your frontend. If you need some references on how to do that, check out our Github repo:

Try the live demo, it's fully functional

Calendar Booking App by Cotter
Calendar Booking App by Cotter

Questions & Feedback

Come and talk to the founders of Cotter and other developers who are using Cotter on Cotter's Slack Channel.

Ready to use Cotter?

If you enjoyed this tutorial and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.

If you need help, ping us on our Slack channel or email us at [email protected]