Skip to content

Design Document: Google Wallet Ticket Generation

kvn edited this page Sep 21, 2022 · 1 revision

Reference: https://developers.google.com/wallet/generic/web

SocialPass allows users to generate tickets if they are able to prove ownership of certain required digital assets (such as NFTs) set by the event organizers. One useful feature after proving ownership and generating the ticket is saving the ticket to the user’s Google Wallet. This page is meant to document the required interactions with the Google API in order to allow users to accomplish the above stated goal.

Overview

The overview of the flow is fairly simple and consists of the following steps:

  1. Authenticate (with the required credentials)
  2. Generate a pass object
  3. Save the pass object (in the user’s Google wallet)

Steps in detail

The above steps are documented in detail in the following sections:

1. Authenticate

The first step is to install the required Google client library in Python using the following command:

pip install --upgrade google-api-python-client

Once that’s done, the following code can be run to authenticate the client:

import os, re, datetime

from google.auth.transport.requests import AuthorizedSession
from google.oauth2 import service_account
from google.auth import jwt, crypt

# Path to service account key file obtained from Google CLoud Console.
service_account_file = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "/path/to/key.json")

# Issuer ID obtained from Google Pay Business Console.
issuer_id = os.environ.get("WALLET_ISSUER_ID", "<issuer ID>")

# Developer defined ID for the wallet class.
class_id = os.environ.get("WALLET_CLASS_ID", "test-generic-class-id")

# Developer defined ID for the user, eg an email address.
user_id = os.environ.get("WALLET_USER_ID", "[email protected]")

# ID for the wallet object, must be in the form `issuer_id.user_id` where user_id is alphanumeric.
object_id = "%s.%s-%s" % (issuer_id, re.sub(r"[^\w.-]", "_", user_id), class_id)

# Authenticate the client
credentials = service_account.Credentials.from_service_account_file(service_account_file, 
  scopes=["https://www.googleapis.com/auth/wallet_object.issuer"])
http_client = AuthorizedSession(credentials)

Getting the <issuer ID>

In order to get the <issuer ID>, the following steps must be taken:

Getting the service_account_file

The service_account_file is a JSON file storing credentials required to access the API. This file can be generated for this use case using the following steps:

Once this is done, you should be able to authenticate successfully and move on to the next step.

2. Generate a pass object

Creating a pass class

Before a pass can be generated, a class needs to be created to hold the pass. This can be done following the steps in this link: https://developers.google.com/wallet/tickets/events/web/prerequisites#5.-create-a-class

Please take note of the following things when creating the class:

Important note For our use case in production, we may need to create a class every time an event is created/updated. This means hitting the Google API from a Django signal post save from the event model. Further discussions need to be had on this matter before anything is finalized.

Generating a pass

Once this is done, get the class_id (from the created class) to generate a pass using the following code:

object_url = "https://walletobjects.googleapis.com/walletobjects/v1/eventTicketObject/"
object_payload = {
  "id": object_id,
  "classId": "%s.%s" % (issuer_id, class_id),
  "heroImage": {
    "sourceUri": {
      "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg",
      "description": "Test heroImage description"
    }
  },
  "textModulesData": [
    {
      "header": "Test text module header",
      "body": "Test text module body"
    }
  ],
  "linksModuleData": {
    "uris": [
      {
        "kind": "walletobjects#uri",
        "uri": "http://maps.google.com/",
        "description": "Test link module uri description"
      },
      {
        "kind": "walletobjects#uri",
        "uri": "tel:6505555555",
        "description": "Test link module tel description"
      }
    ]
  },
  "imageModulesData": [
    {
      "mainImage": {
        "kind": "walletobjects#image",
        "sourceUri": {
          "kind": "walletobjects#uri",
          "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg",
          "description": "Test image module description"
        }
      }
    }
  ],
  "barcode": {
    "kind": "walletobjects#barcode",
    "type": "qrCode",
    "value": "Test QR Code"
  },
  "state": "active",
  "seatInfo": {
    "kind": "walletobjects#eventSeat",
    "seat": {
      "kind": "walletobjects#localizedString",
      "defaultValue": {
        "kind": "walletobjects#translatedString",
        "language": "en-us",
        "value": "42"
      }
    },
    "row": {
      "kind": "walletobjects#localizedString",
      "defaultValue": {
        "kind": "walletobjects#translatedString",
        "language": "en-us",
        "value": "G3"
      }
    },
    "section": {
      "kind": "walletobjects#localizedString",
      "defaultValue": {
        "kind": "walletobjects#translatedString",
        "language": "en-us",
        "value": "5"
      }
    },
    "gate": {
      "kind": "walletobjects#localizedString",
      "defaultValue": {
        "kind": "walletobjects#translatedString",
        "language": "en-us",
        "value": "A"
      }
    }
  },
  "ticketHolderName": "Test ticket holder name",
  "ticketNumber": "Test ticket number",
  "locations": [
    {
      "kind": "walletobjects#latLongPoint",
      "latitude": 37.424015499999996,
      "longitude": -122.09259560000001
    }
  ]
}

# Retrieve the object, or create it if it doesn't exist.
object_response = http_client.get(object_url + object_id)
if object_response.status_code == 404:
  object_response = http_client.post(object_url, json=object_payload)
print("object GET or POST response:", object_response.text)

If everything works properly, you should see a JSON response. This is the JSON representation of the pass that was just generated. Save the id from this response and move to the next step.

3. Save the pass object

In order to allow users to save the passes we generate, we need to take the following steps:

  • Get a signed JWT (token) for the pass object
  • Prompt the user to go the appropriate link using the generated token: https://pay.google.com/gp/v/save/{token}

When a user goes to this link, they will be automatically asked to save the pass in to their Google Wallet. This link can be shared via email, or a button inside our product on the success page.

Getting the token

The signed JWT (token) can be generated by running the following code:

claims = {
  "iss": http_client.credentials.service_account_email, # `client_email` in service account file.
  "aud": "google",
  "origins": ["socialpass.io"],
  "typ": "savetowallet",
  "payload": {
    "eventTicketObjects": [
      {
        "id": object_id # the id of the generated pass
      }
    ]
  }
}

signer = crypt.RSASigner.from_service_account_file(service_account_file)
token = jwt.encode(signer, claims).decode("utf-8")
save_url = "https://pay.google.com/gp/v/save/%s" % token
print(save_url)