n*rd*nc*d

Google One Tap Vue plugin

What is GOT?

It's not Game of Thrones. It is Google One Tap.

One Tap is the new cross-platform sign-in mechanism from Google that supports and streamlines multiple types of credentials.

You can quickly and easily manage user authentication and sign in to your website using One Tap. Users sign into a Google Account, provide their consent and securely share their profile information with your platform.

Many organisations have started using this method to increase their user retention score. After reading this blog post, you will be able to create a Google One Tap package plugin for any of your projects. Let's go!

But of course, create your Google OAuth credential from Google APIs console

Step 1: Create OAuth Client ID

Go to "Credentials" => Click "Create Credentials" then "OAuth Client ID".

Create OAuth Client ID
Credentials => Create Credentials > OAuth Client ID

Step 2: Fill in details

Fill in the details of your project to create Client ID for the Web Application.

IMPORTANT: Include your site's domain in the Authorized JavaScript origins box. Please note that Google One Tap can only be displayed in HTTPS domains.

When you perform local tests or development, you must add both http://localhost and http://localhost:port_number to the Authorized JavaScript origins box. In my case, I added http://localhost and http://localhost:3000.

Step 2 Fill in details
Fill in details

Step 3: Get your Client ID

Yes! you have your Client ID ready to be used! Make sure to save it, we will use this Client ID on our plugin 👍

IMPORTANT: Do not share your Client Secret to anyone.

Step 2 Fill in details
Credentials => Create Credentials > OAuth Client ID

01. Project setup

This step is on: repo [branch: feat/01-setup]

Before we write our code, I'd like setup our project with eslint, jest, and playground. Let's start with setting up our `package.json`.

// File: package.json
{
	"name": "one-tap-login",
	"version": "1.0.0",
	"description": "Google One Tap Login plugin",
	"main": "onetaplogin.js",
	"author": "Ivana Setiawan",
	"license": "MIT",
	"scripts": {
		"test": "jest",
		"test:watch": "jest --watch",
		"lint": "eslint --fix --ext \".js,.vue\" ."
	},
	"devDependencies": {
		"@babel/cli": "^7.17.6",
		"@babel/core": "^7.17.9",
		"@babel/preset-env": "^7.16.11",
		"@vue/test-utils": "^1.3.0",
		"babel-jest": "^27.5.1",
		"eslint": "^8.11.0",
		"eslint-plugin-vue": "^8.5.0",
		"jest": "^27.5.1",
		"jest-junit": "^13.0.0",
		"vue-jest": "^3.0.7"
	}
}

Then install all the dependencies by running npm install on your terminal. Afterwards, we need to setup the Babel, ESLint and Jest config:

// Create a new file .eslintrc.js
module.exports = {
	extends: [
	  // add more generic rulesets here, such as:
	  // 'eslint:recommended',
	  // 'plugin:vue/vue3-recommended',
	  'plugin:vue/recommended' // Use this if you are using Vue.js 2.x.
	],
	rules: {
	  // override/add rules settings here, such as:
	  // 'vue/no-unused-vars': 'error'
	}
}
// Create a new file babel.config.js
module.exports = {
	env: {
		test: {
			presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
		},
	},
};
// Create a new file jest.preset.js
module.exports = {
	moduleFileExtensions: ['js'],
	moduleNameMapper: {
		'^~/(.*)$': '<rootDir>/$1',
		'^~~/(.*)$': '<rootDir>/$1',
		'^@/(.*)$': '<rootDir>/$1',
	},
	transform: {
		'^.+\\.js$': 'babel-jest',
		'.*\\.(vue)$': 'vue-jest',
	},
	reporters: ['default', 'jest-junit'],
};
// Create a new file jest.config.js
module.exports = {
	name: 'onetaplogin',
	displayName: 'one-tap-login',
	...require('./jest.preset'),
	testEnvironment: 'jsdom',
};

When it's done, you see a new `node_modules` folder and `package-lock.json`. To check if everything is OK, run these commands on your terminal without any errors:

npm run lint
npm run test

When the setup is done, we are gonna create the `playground` for our project so we can test our plugin later on. To create a playground, follow this steps.
You can also check out this commit ...fcf76d.

02. Build Google One Tap plugin

This step is on: repo [branch: feat/02-build-got]

Before I start writing code, it helps me tremendously to visualise what would be the best developer experience for my plugin. In my head, I want the flow to work like:

  • To install, run npm install google-one-tap
  • Developers should be able to pass client ID, context and logged in status to determine the visibility of the GOT.

Create `onetaplogin.js` and start with install plugin. See commit ...43b6914c

/*
* options (Object):
*   - isLoggedIn (Boolean): Logged in status
*   - clientId (String): Your Google's credential client ID
* callback (Function)
*
* Usage:
* new Vue.use(OneTapLogin, {
*   id: 'Client ID',
*   isLoggedIn: false,
* }, callback);
*/

export default {
    install(_, options, callback) {
        this.options = options;
        this.callback = callback;
        this.isLoaded = false;

        /**
         * Rules to not execute:
         * User is already logged in, or client ID is not defined.
         */
        const rules =
            this.options?.isLoggedIn || !this.options?.clientId;
        this._checkExecutable(rules);
    },
};

Check if the pluging is executable based on the `rules`. See commit ...d5434f

export default {
	...

	/**
     * Check if the plugin is executable based on the `rules`.
     */
    _checkExecutable(rules) {
        if (rules) return;
        this._load();
    },
};

Load method to check if the plugin is not yet loaded. See commit ...215e7

export default {
	...

    /**
     * Load method to check if the plugin is not yet loaded.
     * If not yet, load the library,
     * If already loaded, do not load the library
     */
    _load() {
        return !this.isLoaded ? this._loadLibrary() : null;
    },
};

Load method to check if the plugin is not yet loaded. See commit ...4d88f0

export default {
	...

    /**
     * Load the library method.
     * Based on this: https://developers.google.com/identity/gsi/web/guides/client-library
     */
    _loadLibrary() {
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = 'https://accounts.google.com/gsi/client';
        script.async = true;
        script.defer = true;
        document.body.appendChild(script);
        script.addEventListener('load', () => {
            this.isLoaded = true;
        });
        this._init();
    },
};

Initialise the Google One Tap. See commit ...9183f9

export default {
	...

    /**
     * Initialise the Google One Tap
     */
    _init() {
        const ctx = this.options?.context || 'signin'; // eslint-disable-line
        const context = ['signin', 'signup', 'use'].includes(ctx)
            ? ctx
            : 'signin';
        const callback = this.callback;
        /* eslint-disable */
        const client_id = this.options?.clientId; // eslint-disable-line
        window.onload = function () {
            window.google.accounts.id.initialize({
                client_id,
                cancel_on_tap_outside: false,
                context,
                callback,
            });
            window.google.accounts.id.prompt();
        };
        /* eslint-enable */
    },
};

Test your Google One Tap plugin on the plaground. See commit ...0a66e

import Vue from "vue";
import OneTapLogin from 'one-tap-login';
const callback = () => {
    const redirectUrl = 'https://www.venopi.com';
    window.location.assign(redirectUrl);
};
new Vue.use(OneTapLogin, {
	// Get the Client Id from your OAuth credential
	clientId: '410358638264-egs2qtqe6i9mn19c56lhnscblfnuols4.apps.googleusercontent.com',

	// Should set dynamically, depending on your user logged in status
	isLoggedIn: false,
}, callback);

Let's run npm run playground and go to localhost:8080. You should see One Tap login shows up like this:

Google One Tap on your website
Google One Tap on your website

Check your "Authorized JavaScript origins" on your Google API Console if you see this error on your browser console => [GSI_LOGGER]: The given origin is not allowed for the given client ID.

03. Unit Testing

This step is on: repo [branch: feat/03-test]

Based on our `onetaplogin.js`, I think we need create a test that covers:

  • 🧪 The plugin should be installed properly
  • 🧪 The plugin should check the rules condition before loading the client library
  • 🧪 The plugin should not load the client library from google if rules to not execute is true
  • 🧪 The plugin should load the client library from google if rules to not execute is false
  • 🧪 The plugin should load the client library from google
  • 🧪 The plugin should append cookie law script on the document body

Let's write this test!

import { createLocalVue } from '@vue/test-utils';
import OneTapLogin from './onetaplogin';

describe('One Tap Login', () => {
    const localVue = createLocalVue();
    let _loadSpy;
    let _loadLibrarySpy;
    let _checkExecutableSpy;
    let appendChild;

    beforeEach(() => {
        document.body.innerHTML = '<body><script></script></body>';
        _checkExecutableSpy = jest.spyOn(OneTapLogin, '_checkExecutable');
        _loadSpy = jest.spyOn(OneTapLogin, '_load');
        _loadLibrarySpy = jest.spyOn(OneTapLogin, '_loadLibrary');
        appendChild = jest.spyOn(document.body, 'appendChild');
        localVue.use(OneTapLogin);
    });

    test('should be installed properly', () => {
        expect(typeof localVue._installedPlugins[0].install).toBe('function');
    });

    test('should check the rules condition before loading the client library', () => {
        const rulesMock = true;
        expect(_checkExecutableSpy).toBeCalledWith(rulesMock);
    });

    test('should not load the client library from google if rules to not execute is true', () => {
        const rulesMock = true;
        OneTapLogin._checkExecutable(rulesMock);
        expect(_loadSpy).not.toBeCalled();
    });

    test('should load the client library from google if rules to not execute is false', () => {
        const rulesMock = false;
        OneTapLogin._checkExecutable(rulesMock);
        expect(_loadSpy).toBeCalled();
    });

    test('should load the client library from google', () => {
        expect(_loadLibrarySpy).toBeCalled();
    });

    test('should append cookie law script on the document body', () => {
        expect(appendChild).toBeCalledWith(
            expect.objectContaining({
                type: 'text/javascript',
                src: 'https://accounts.google.com/gsi/client',
                async: true,
                defer: true,
            })
        );
    });
});

Let's run npm run test on our terminal. You should see the tests are passing, like this:

Unit testing for our Google One Tap plugin
Unit testing for our Google One Tap plugin

04. Alternative plugin

Coming soon: Alternative way to write the plugin.

Any questions or suggestions for better implementations? Write to me 😀