From e6b07e4f8c8ecefe52431f56667425a828f3b73f Mon Sep 17 00:00:00 2001 From: Stephen McDonald Date: Thu, 15 Jun 2023 10:11:42 +1000 Subject: [PATCH] Go samples --- go/README.md | 78 ++++++ go/demo_eventticket.go | 571 ++++++++++++++++++++++++++++++++++++++++ go/demo_flight.go | 531 +++++++++++++++++++++++++++++++++++++ go/demo_generic.go | 523 +++++++++++++++++++++++++++++++++++++ go/demo_giftcard.go | 500 +++++++++++++++++++++++++++++++++++ go/demo_loyalty.go | 521 +++++++++++++++++++++++++++++++++++++ go/demo_offer.go | 503 +++++++++++++++++++++++++++++++++++ go/demo_transit.go | 578 +++++++++++++++++++++++++++++++++++++++++ go/go.mod | 25 ++ go/go.sum | 37 +++ templates/README.md | 6 + templates/generate.py | 466 +++++++++++++++++++++++++++++++++ templates/template.go | 269 +++++++++++++++++++ 13 files changed, 4608 insertions(+) create mode 100644 go/README.md create mode 100644 go/demo_eventticket.go create mode 100644 go/demo_flight.go create mode 100644 go/demo_generic.go create mode 100644 go/demo_giftcard.go create mode 100644 go/demo_loyalty.go create mode 100644 go/demo_offer.go create mode 100644 go/demo_transit.go create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 templates/README.md create mode 100644 templates/generate.py create mode 100644 templates/template.go diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..4fb1705 --- /dev/null +++ b/go/README.md @@ -0,0 +1,78 @@ +# Google Wallet Go samples + +## Overview + +The files in this directory each implement a demo type for a specific Google +Wallet pass type. Each demo type has methods implemented for performing tasks such as +creating a pass class, updating issuer permissions, and more. + +| Pass type | File | +|----------------------------|------------------------------------------------| +| Event tickets | [`demo_eventticket.go`](./demo_eventticket.go) | +| Flight boarding passes | [`demo_flight.go`](./demo_flight.go) | +| Generic passes | [`demo_generic.go`](./demo_generic.go) | +| Gift cards | [`demo_giftcard.go`](./demo_giftcard.go) | +| Loyalty program membership | [`demo_loyalty.go`](./demo_loyalty.go) | +| Offers and promotions | [`demo_offer.go`](./demo_offer.go) | +| Transit passes | [`demo_transit.go`](./demo_transit.go) | + +## Prerequisites + +* Go 1.20.x +* Follow the steps outlined in the + [Google Wallet prerequisites](https://developers.google.com/wallet/generic/web/prerequisites) + to create the Google Wallet issuer account and Google Cloud service account + +## Environment variables + +The following environment variables must be set. Alternatively, you can update +the code files to set the values directly. + +| Enviroment variable | Description | Example | +|----------------------------------|-------------------------------------------------|---------------------| +| `GOOGLE_APPLICATION_CREDENTIALS` | Path to a Google Cloud service account key file | `/path/to/key.json` | +| `WALLET_ISSUER_ID` | Your Google Wallet Issuer ID | 1234567890 | + +## How to use the code samples + +1. First install the dependencies for the sample you wish to run (this isn't necessary a second time for running subsequent samples) + + ```bash + go install demo_eventticket.go + ``` + +2. Run the sample + + ```bash + go run demo_eventticket.go + ``` + +3. Optionally, you can manually copy the demo type in your own project. An example + can be found below + + ```go + // Create a demo type instance + // Creates the authenticated HTTP client + d := demoEventticket{} + d.auth() + + // Create a pass class + d.createClass(issuerId, classSuffix) + + // Create a pass object + d.createObject(issuerId, classSuffix, objectSuffix) + + // Expire a pass object + d.expireObject(issuerId, objectSuffix) + + // Create an "Add to Google Wallet" link + // that generates a new pass class and object + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + + // Create an "Add to Google Wallet" link + // that references existing pass classes and objects + d.createJwtExistingObjects(issuerId) + + // Create pass objects in batch + d.batchCreateObjects(issuerId, classSuffix) + ``` diff --git a/go/demo_eventticket.go b/go/demo_eventticket.go new file mode 100644 index 0000000..11c33ed --- /dev/null +++ b/go/demo_eventticket.go @@ -0,0 +1,571 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/eventTicketClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/eventTicketObject" +) + +// [END setup] + +type demoEventticket struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoEventticket) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoEventticket) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "eventId": "EVENT_ID", + "eventName": { + "defaultValue": { + "value": "Event name", + "language": "en-US" + } + }, + "issuerName": "Issuer name", + "id": "%s.%s", + "reviewStatus": "UNDER_REVIEW" + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoEventticket) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "ticketHolderName": "Ticket holder name", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "ticketNumber": "Ticket number", + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "seatInfo": { + "gate": { + "defaultValue": { + "value": "A", + "language": "en-US" + } + }, + "section": { + "defaultValue": { + "value": "5", + "language": "en-US" + } + }, + "row": { + "defaultValue": { + "value": "G3", + "language": "en-US" + } + }, + "seat": { + "defaultValue": { + "value": "42", + "language": "en-US" + } + } + }, + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoEventticket) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoEventticket) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "eventId": "EVENT_ID", + "eventName": { + "defaultValue": { + "value": "Event name", + "language": "en-US" + } + }, + "issuerName": "Issuer name", + "id": "%s.%s", + "reviewStatus": "UNDER_REVIEW" + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "ticketHolderName": "Ticket holder name", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "ticketNumber": "Ticket number", + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "seatInfo": { + "gate": { + "defaultValue": { + "value": "A", + "language": "en-US" + } + }, + "section": { + "defaultValue": { + "value": "5", + "language": "en-US" + } + }, + "row": { + "defaultValue": { + "value": "G3", + "language": "en-US" + } + }, + "seat": { + "defaultValue": { + "value": "42", + "language": "en-US" + } + } + }, + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoEventticket) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoEventticket) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "ticketHolderName": "Ticket holder name", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "ticketNumber": "Ticket number", + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "seatInfo": { + "gate": { + "defaultValue": { + "value": "A", + "language": "en-US" + } + }, + "section": { + "defaultValue": { + "value": "5", + "language": "en-US" + } + }, + "row": { + "defaultValue": { + "value": "G3", + "language": "en-US" + } + }, + "seat": { + "defaultValue": { + "value": "42", + "language": "en-US" + } + } + }, + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/eventTicketObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_eventticket.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoEventticket{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/demo_flight.go b/go/demo_flight.go new file mode 100644 index 0000000..0e2f1c0 --- /dev/null +++ b/go/demo_flight.go @@ -0,0 +1,531 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/flightClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/flightObject" +) + +// [END setup] + +type demoFlight struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoFlight) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoFlight) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "origin": { + "terminal": "1", + "gate": "A2", + "airportIataCode": "LAX" + }, + "flightHeader": { + "carrier": { + "carrierIataCode": "LX" + }, + "flightNumber": "123" + }, + "localScheduledDepartureDateTime": "2023-07-02T15:30:00", + "reviewStatus": "UNDER_REVIEW", + "issuerName": "Issuer name", + "destination": { + "terminal": "2", + "gate": "C3", + "airportIataCode": "SFO" + }, + "id": "%s.%s" + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoFlight) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "passengerName": "Passenger name", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "boardingAndSeatingInfo": { + "boardingGroup": "B", + "seatNumber": "42" + }, + "reservationInfo": { + "confirmationCode": "Confirmation code" + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoFlight) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoFlight) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "origin": { + "terminal": "1", + "gate": "A2", + "airportIataCode": "LAX" + }, + "flightHeader": { + "carrier": { + "carrierIataCode": "LX" + }, + "flightNumber": "123" + }, + "localScheduledDepartureDateTime": "2023-07-02T15:30:00", + "reviewStatus": "UNDER_REVIEW", + "issuerName": "Issuer name", + "destination": { + "terminal": "2", + "gate": "C3", + "airportIataCode": "SFO" + }, + "id": "%s.%s" + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "passengerName": "Passenger name", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "boardingAndSeatingInfo": { + "boardingGroup": "B", + "seatNumber": "42" + }, + "reservationInfo": { + "confirmationCode": "Confirmation code" + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoFlight) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoFlight) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "passengerName": "Passenger name", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "boardingAndSeatingInfo": { + "boardingGroup": "B", + "seatNumber": "42" + }, + "reservationInfo": { + "confirmationCode": "Confirmation code" + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/flightObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_flight.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoFlight{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/demo_generic.go b/go/demo_generic.go new file mode 100644 index 0000000..0c12c7d --- /dev/null +++ b/go/demo_generic.go @@ -0,0 +1,523 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/genericClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/genericObject" +) + +// [END setup] + +type demoGeneric struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoGeneric) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoGeneric) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "id": "%s.%s" + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoGeneric) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "cardTitle": { + "defaultValue": { + "value": "Generic card title", + "language": "en-US" + } + }, + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "header": { + "defaultValue": { + "value": "Generic header", + "language": "en-US" + } + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "logo": { + "contentDescription": { + "defaultValue": { + "value": "Generic card logo", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://storage.googleapis.com/wallet-lab-tools-codelab-artifacts-public/pass_google_logo.jpg" + } + }, + "hexBackgroundColor": "#4285f4", + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoGeneric) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoGeneric) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "id": "%s.%s" + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "cardTitle": { + "defaultValue": { + "value": "Generic card title", + "language": "en-US" + } + }, + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "header": { + "defaultValue": { + "value": "Generic header", + "language": "en-US" + } + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "logo": { + "contentDescription": { + "defaultValue": { + "value": "Generic card logo", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://storage.googleapis.com/wallet-lab-tools-codelab-artifacts-public/pass_google_logo.jpg" + } + }, + "hexBackgroundColor": "#4285f4", + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoGeneric) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoGeneric) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "cardTitle": { + "defaultValue": { + "value": "Generic card title", + "language": "en-US" + } + }, + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "header": { + "defaultValue": { + "value": "Generic header", + "language": "en-US" + } + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "logo": { + "contentDescription": { + "defaultValue": { + "value": "Generic card logo", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://storage.googleapis.com/wallet-lab-tools-codelab-artifacts-public/pass_google_logo.jpg" + } + }, + "hexBackgroundColor": "#4285f4", + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/genericObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_generic.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoGeneric{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/demo_giftcard.go b/go/demo_giftcard.go new file mode 100644 index 0000000..48031b7 --- /dev/null +++ b/go/demo_giftcard.go @@ -0,0 +1,500 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/giftCardClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/giftCardObject" +) + +// [END setup] + +type demoGiftcard struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoGiftcard) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoGiftcard) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "issuerName": "Issuer name", + "id": "%s.%s", + "reviewStatus": "UNDER_REVIEW" + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoGiftcard) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "pin": "1234", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "balanceUpdateTime": { + "date": "2020-04-12T16:20:50.52-04:00" + }, + "state": "ACTIVE", + "cardNumber": "Card number", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "balance": { + "micros": 20000000, + "currencyCode": "USD" + }, + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoGiftcard) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoGiftcard) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "issuerName": "Issuer name", + "id": "%s.%s", + "reviewStatus": "UNDER_REVIEW" + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "pin": "1234", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "balanceUpdateTime": { + "date": "2020-04-12T16:20:50.52-04:00" + }, + "state": "ACTIVE", + "cardNumber": "Card number", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "balance": { + "micros": 20000000, + "currencyCode": "USD" + }, + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoGiftcard) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoGiftcard) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "pin": "1234", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "balanceUpdateTime": { + "date": "2020-04-12T16:20:50.52-04:00" + }, + "state": "ACTIVE", + "cardNumber": "Card number", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "balance": { + "micros": 20000000, + "currencyCode": "USD" + }, + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/giftCardObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_giftcard.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoGiftcard{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/demo_loyalty.go b/go/demo_loyalty.go new file mode 100644 index 0000000..660e5c5 --- /dev/null +++ b/go/demo_loyalty.go @@ -0,0 +1,521 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/loyaltyClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/loyaltyObject" +) + +// [END setup] + +type demoLoyalty struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoLoyalty) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoLoyalty) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "programName": "Program name", + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "id": "%s.%s", + "programLogo": { + "contentDescription": { + "defaultValue": { + "value": "Logo description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm8.staticflickr.com/7340/11177041185_a61a7f2139_o.jpg" + } + } + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoLoyalty) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "accountName": "Account name", + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s", + "loyaltyPoints": { + "balance": { + "int": 800 + }, + "label": "Points" + }, + "accountId": "Account id" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoLoyalty) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoLoyalty) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "programName": "Program name", + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "id": "%s.%s", + "programLogo": { + "contentDescription": { + "defaultValue": { + "value": "Logo description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm8.staticflickr.com/7340/11177041185_a61a7f2139_o.jpg" + } + } + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "accountName": "Account name", + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s", + "loyaltyPoints": { + "balance": { + "int": 800 + }, + "label": "Points" + }, + "accountId": "Account id" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoLoyalty) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoLoyalty) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "accountName": "Account name", + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s", + "loyaltyPoints": { + "balance": { + "int": 800 + }, + "label": "Points" + }, + "accountId": "Account id" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/loyaltyObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_loyalty.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoLoyalty{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/demo_offer.go b/go/demo_offer.go new file mode 100644 index 0000000..8c3c055 --- /dev/null +++ b/go/demo_offer.go @@ -0,0 +1,503 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/offerClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/offerObject" +) + +// [END setup] + +type demoOffer struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoOffer) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoOffer) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "redemptionChannel": "ONLINE", + "reviewStatus": "UNDER_REVIEW", + "title": "Offer title", + "issuerName": "Issuer name", + "provider": "Provider name", + "id": "%s.%s" + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoOffer) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "validTimeInterval": { + "start": { + "date": "2023-06-12T23:20:50.52Z" + }, + "end": { + "date": "2023-12-12T23:20:50.52Z" + } + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoOffer) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoOffer) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "redemptionChannel": "ONLINE", + "reviewStatus": "UNDER_REVIEW", + "title": "Offer title", + "issuerName": "Issuer name", + "provider": "Provider name", + "id": "%s.%s" + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "validTimeInterval": { + "start": { + "date": "2023-06-12T23:20:50.52Z" + }, + "end": { + "date": "2023-12-12T23:20:50.52Z" + } + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoOffer) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoOffer) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "validTimeInterval": { + "start": { + "date": "2023-06-12T23:20:50.52Z" + }, + "end": { + "date": "2023-12-12T23:20:50.52Z" + } + }, + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/offerObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_offer.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoOffer{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/demo_transit.go b/go/demo_transit.go new file mode 100644 index 0000000..49379ab --- /dev/null +++ b/go/demo_transit.go @@ -0,0 +1,578 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/transitClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/transitObject" +) + +// [END setup] + +type demoTransit struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demoTransit) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demoTransit) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(` + { + "logo": { + "contentDescription": { + "defaultValue": { + "value": "Logo description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://live.staticflickr.com/65535/48690277162_cd05f03f4d_o.png" + } + }, + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "id": "%s.%s", + "transitType": "BUS" + } + `, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demoTransit) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "passengerNames": "Passenger names", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "ticketLeg": { + "destinationStationCode": "SFO", + "destinationName": { + "defaultValue": { + "value": "Destination name", + "language": "en-US" + } + }, + "arrivalDateTime": "2020-04-12T20:20:50.52Z", + "originStationCode": "LA", + "originName": { + "defaultValue": { + "value": "Origin name", + "language": "en-US" + } + }, + "departureDateTime": "2020-04-12T16:20:50.52Z", + "fareName": { + "defaultValue": { + "value": "Fare name", + "language": "en-US" + } + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "passengerType": "SINGLE_PASSENGER", + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "tripType": "ONE_WAY", + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demoTransit) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demoTransit) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(` + { + "logo": { + "contentDescription": { + "defaultValue": { + "value": "Logo description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://live.staticflickr.com/65535/48690277162_cd05f03f4d_o.png" + } + }, + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "id": "%s.%s", + "transitType": "BUS" + } + `, issuerId, classSuffix) + + newObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "passengerNames": "Passenger names", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "ticketLeg": { + "destinationStationCode": "SFO", + "destinationName": { + "defaultValue": { + "value": "Destination name", + "language": "en-US" + } + }, + "arrivalDateTime": "2020-04-12T20:20:50.52Z", + "originStationCode": "LA", + "originName": { + "defaultValue": { + "value": "Origin name", + "language": "en-US" + } + }, + "departureDateTime": "2020-04-12T16:20:50.52Z", + "fareName": { + "defaultValue": { + "value": "Fare name", + "language": "en-US" + } + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "passengerType": "SINGLE_PASSENGER", + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "tripType": "ONE_WAY", + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demoTransit) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demoTransit) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(` + { + "classId": "%s.%s", + "passengerNames": "Passenger names", + "heroImage": { + "contentDescription": { + "defaultValue": { + "value": "Hero image description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + } + }, + "ticketLeg": { + "destinationStationCode": "SFO", + "destinationName": { + "defaultValue": { + "value": "Destination name", + "language": "en-US" + } + }, + "arrivalDateTime": "2020-04-12T20:20:50.52Z", + "originStationCode": "LA", + "originName": { + "defaultValue": { + "value": "Origin name", + "language": "en-US" + } + }, + "departureDateTime": "2020-04-12T16:20:50.52Z", + "fareName": { + "defaultValue": { + "value": "Fare name", + "language": "en-US" + } + } + }, + "barcode": { + "type": "QR_CODE", + "value": "QR code" + }, + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "passengerType": "SINGLE_PASSENGER", + "state": "ACTIVE", + "linksModuleData": { + "uris": [ + { + "id": "LINK_MODULE_URI_ID", + "uri": "http://maps.google.com/", + "description": "Link module URI description" + }, + { + "id": "LINK_MODULE_TEL_ID", + "uri": "tel:6505555555", + "description": "Link module tel description" + } + ] + }, + "imageModulesData": [ + { + "id": "IMAGE_MODULE_ID", + "mainImage": { + "contentDescription": { + "defaultValue": { + "value": "Image module description", + "language": "en-US" + } + }, + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + } + } + } + ], + "textModulesData": [ + { + "body": "Text module body", + "header": "Text module header", + "id": "TEXT_MODULE_ID" + } + ], + "tripType": "ONE_WAY", + "id": "%s.%s" + } + `, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/transitObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_transit.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demoTransit{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +} diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..4f106f9 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,25 @@ +module example.com/wallet + +go 1.20 + +require golang.org/x/oauth2 v0.8.0 + +require ( + cloud.google.com/go/compute/metadata v0.2.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.8 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/yuin/goldmark v1.4.13 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.6.0 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.0 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..3a5e828 --- /dev/null +++ b/go/go.sum @@ -0,0 +1,37 @@ +cloud.google.com/go/compute/metadata v0.2.0 h1:nBbNSZyDpkNlo3DepaaLKVuO7ClyifSAmNloSCZrHnQ= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= diff --git a/templates/README.md b/templates/README.md new file mode 100644 index 0000000..14aed48 --- /dev/null +++ b/templates/README.md @@ -0,0 +1,6 @@ +# Code Generation Code Only + +This directory contains files for generating the demos +found in the parent directory. It does not contain +functioning samples for Google Wallet, so do not +use it as such. \ No newline at end of file diff --git a/templates/generate.py b/templates/generate.py new file mode 100644 index 0000000..be4f21a --- /dev/null +++ b/templates/generate.py @@ -0,0 +1,466 @@ +# +# Copyright 2022 Google Inc. All rights reserved. +# +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# + +import os, json + +default_class_payload = { + "id": "$class_id", +} + +default_object_payload = { + "id": "$object_id", + "classId": "$class_id", + "state": "ACTIVE", + "heroImage": { + "sourceUri": { + "uri": "https://farm4.staticflickr.com/3723/11177041115_6e6a3b6f49_o.jpg" + }, + "contentDescription": { + "defaultValue": { + "language": "en-US", + "value": "Hero image description" + } + } + }, + "textModulesData": [ + { + "header": "Text module header", + "body": "Text module body", + "id": "TEXT_MODULE_ID" + } + ], + "linksModuleData": { + "uris": [ + { + "uri": "http://maps.google.com/", + "description": "Link module URI description", + "id": "LINK_MODULE_URI_ID" + }, + { + "uri": "tel:6505555555", + "description": "Link module tel description", + "id": "LINK_MODULE_TEL_ID" + } + ] + }, + "imageModulesData": [ + { + "mainImage": { + "sourceUri": { + "uri": "http://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg" + }, + "contentDescription": { + "defaultValue": { + "language": "en-US", + "value": "Image module description" + } + } + }, + "id": "IMAGE_MODULE_ID" + } + ], + "barcode": { + "type": "QR_CODE", + "value": "QR code" + } +} + +payloads = {} + +for object_type in ["generic", "offer", "loyalty", "giftCard", "eventTicket", "flight", "transit"]: + payloads[object_type] = { + "$class_payload": dict(default_class_payload), + "$object_payload": dict(default_object_payload), + } + +################# +# Generic +################# + +payloads["generic"]["$object_payload"].update({ + "cardTitle": { + "defaultValue": { + "language": "en-US", + "value": "Generic card title" + } + }, + "header": { + "defaultValue": { + "language": "en-US", + "value": "Generic header" + } + }, + "hexBackgroundColor": "#4285f4", + "logo": { + "sourceUri": { + "uri": "https://storage.googleapis.com/wallet-lab-tools-codelab-artifacts-public/pass_google_logo.jpg" + }, + "contentDescription": { + "defaultValue": { + "language": "en-US", + "value": "Generic card logo" + } + } + } +}) + +################# +# Offer +################# + +payloads["offer"]["$class_payload"].update({ + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "provider": "Provider name", + "title": "Offer title", + "redemptionChannel": "ONLINE" +}) + +payloads["offer"]["$object_payload"].update({ + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "validTimeInterval": { + "start": { + "date": "2023-06-12T23:20:50.52Z" + }, + "end": { + "date": "2023-12-12T23:20:50.52Z" + } + } +}) + +################# +# Loyalty +################# + +payloads["loyalty"]["$class_payload"].update({ + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "programName": "Program name", + "programLogo": { + "sourceUri": { + "uri": "http://farm8.staticflickr.com/7340/11177041185_a61a7f2139_o.jpg" + }, + "contentDescription": { + "defaultValue": { + "language": "en-US", + "value": "Logo description" + } + } + } +}) + +payloads["loyalty"]["$object_payload"].update({ + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "accountId": "Account id", + "accountName": "Account name", + "loyaltyPoints": { + "label": "Points", + "balance": { + "int": 800 + } + } +}) + +################# +# GiftCard +################# + +payloads["giftCard"]["$class_payload"].update({ + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", +}) + +payloads["giftCard"]["$object_payload"].update({ + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "cardNumber": "Card number", + "pin": "1234", + "balance": { + "micros": 20000000, + "currencyCode": "USD" + }, + "balanceUpdateTime": { + "date": "2020-04-12T16:20:50.52-04:00" + } +}) + +################# +# Eventticket +################# + +payloads["eventTicket"]["$class_payload"].update({ + "eventId": "EVENT_ID", + "eventName": { + "defaultValue": { + "language": "en-US", + "value": "Event name" + } + }, + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW" +}) + +payloads["eventTicket"]["$object_payload"].update({ + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "seatInfo": { + "seat": { + "defaultValue": { + "language": "en-US", + "value": "42" + } + }, + "row": { + "defaultValue": { + "language": "en-US", + "value": "G3" + } + }, + "section": { + "defaultValue": { + "language": "en-US", + "value": "5" + } + }, + "gate": { + "defaultValue": { + "language": "en-US", + "value": "A" + } + } + }, + "ticketHolderName": "Ticket holder name", + "ticketNumber": "Ticket number" +}) + +################# +# Flight +################# + +payloads["flight"]["$class_payload"].update({ + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "localScheduledDepartureDateTime": "2023-07-02T15:30:00", + "flightHeader": { + "carrier": { + "carrierIataCode": "LX" + }, + "flightNumber": "123" + }, + "origin": { + "airportIataCode": "LAX", + "terminal": "1", + "gate": "A2" + }, + "destination": { + "airportIataCode": "SFO", + "terminal": "2", + "gate": "C3" + } +}) + +payloads["flight"]["$object_payload"].update({ + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "passengerName": "Passenger name", + "boardingAndSeatingInfo": { + "boardingGroup": "B", + "seatNumber": "42" + }, + "reservationInfo": { + "confirmationCode": "Confirmation code" + } +}) + +################# +# Transit +################# + +payloads["transit"]["$class_payload"].update({ + "issuerName": "Issuer name", + "reviewStatus": "UNDER_REVIEW", + "logo": { + "sourceUri": { + "uri": "https://live.staticflickr.com/65535/48690277162_cd05f03f4d_o.png" + }, + "contentDescription": { + "defaultValue": { + "language": "en-US", + "value": "Logo description" + } + } + }, + "transitType": "BUS" +}) + +payloads["transit"]["$object_payload"].update({ + "locations": [ + { + "latitude": 37.424015499999996, + "longitude": -122.09259560000001 + } + ], + "passengerType": "SINGLE_PASSENGER", + "passengerNames": "Passenger names", + "tripType": "ONE_WAY", + "ticketLeg": { + "originStationCode": "LA", + "originName": { + "defaultValue": { + "language": "en-US", + "value": "Origin name" + } + }, + "destinationStationCode": "SFO", + "destinationName": { + "defaultValue": { + "language": "en-US", + "value": "Destination name" + } + }, + "departureDateTime": "2020-04-12T16:20:50.52Z", + "arrivalDateTime": "2020-04-12T20:20:50.52Z", + "fareName": { + "defaultValue": { + "language": "en-US", + "value": "Fare name" + } + } + } +}) + +def indent(s, spaces): + return s.replace("\n", "\n" + (" " * spaces)) + +def format_payload_dotnet(payload, _): + output = [] + payload = (payload + .replace(' "', " ") + .replace('": ', " = ") + .replace(" string = ", ' @string = ') + .replace("]", "}")) + for line in payload.split("\n"): + _indent = len(line) - len(line.lstrip(" ")) + if line.endswith("{"): + line = line[:-1] + "new\n%s{" % (" " * _indent) + if line.endswith("["): + line = line[:-1] + "new object[]\n%s{" % (" " * _indent) + output.append(line) + return "\n".join(output) + +def format_payload_go(payload, name): + payload = ("\n" + payload.replace(" ", "\t") + "\n").replace("\n", "\n\t").replace(", \n", ",\n") + if name == "$batch_object_payload": + payload = payload.replace("\n", "\n\t") + return payload + +lang_config = { + "go": { + "ext": "go", + "class_id": '"%s.%s"', + "object_id": '"%s.%s"', + "filename": lambda s: "demo_%s.go" % s.lower(), + "formatter": format_payload_go, + }, + "java": { + "ext": "java", + "class_id": "%s.%s", + "object_id": "%s", + "formatter": lambda s, _: '\n "' + s.replace('"', '\\"').replace("\n", '"\n + "') + '"', + "filename": lambda s: "src/main/java/Demo%s.java" % s.title(), + }, + "python": { + "ext": "py", + "class_id": '"%s.%s" % (issuer_id, class_id)', + "object_id": "object_id", + "filename": lambda s: "demo_%s.py" % s.lower(), + }, + "nodejs": { + "ext": "js", + "class_id": "`${issuerId}.${classId}`", + "object_id": "objectId", + "formatter": lambda s, _: indent(s, 2), + "filename": lambda s: "demo-%s.js" % s.lower(), + }, + "php": { + "ext": "php", + "class_id": '"{$issuerId}.{$classId}"', + "object_id": '"{$objectId}"', + "filename": lambda s: "demo_%s.php" % s.lower(), + }, + "dotnet": { + "ext": "cs", + "class_id": '$"{issuerId}.{classId}"', + "object_id": "objectId", + "formatter": format_payload_dotnet, + "filename": lambda s: "Demo%s.cs.example" % s.title(), + "indent": 4, + }, +} + +path = lambda *s: os.path.join(os.path.dirname(os.path.abspath(__file__)), *s) + +for lang, config in lang_config.items(): + try: + with open(path("template.%s" % config["ext"]), "r") as f: + template = f.read() + except IOError: + continue + for object_type, content in payloads.items(): + output = template + + # JSON payloads + if "$batch_object_payload" not in content: + content["$batch_object_payload"] = content["$object_payload"] + for name, value in content.items(): + payload = json.dumps(value, indent=config.get("indent", 2)) + if "formatter" in config: + payload = config["formatter"](payload, name) + output = output.replace(name, payload) + + # code placeholders + config["object_type"] = object_type + config["object_type_lowercase"] = object_type.lower() + config["object_type_titlecase"] = object_type.title() + for name in ("object_type_titlecase", "object_type_lowercase", "object_type", "class_id", "object_id"): + output = output.replace('"$%s"' % name, config[name]) + output = output.replace('$%s' % name, config[name]) + + with open(path("..", lang, config["filename"](object_type)), "w") as f: + f.write(output) \ No newline at end of file diff --git a/templates/template.go b/templates/template.go new file mode 100644 index 0000000..64dd7d9 --- /dev/null +++ b/templates/template.go @@ -0,0 +1,269 @@ +/* + * Copyright 2023 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// [START setup] +// [START imports] +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/google/uuid" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + oauthJwt "golang.org/x/oauth2/jwt" + "io" + "net/http" + "os" + "strings" +) + +// [END imports] + +const ( + batchUrl = "https://walletobjects.googleapis.com/batch" + classUrl = "https://walletobjects.googleapis.com/walletobjects/v1/$object_typeClass" + objectUrl = "https://walletobjects.googleapis.com/walletobjects/v1/$object_typeObject" +) + +// [END setup] + +type demo$object_type_titlecase struct { + credentials *oauthJwt.Config + httpClient *http.Client + batchUrl, classUrl, objectUrl string +} + +// [START auth] +// Create authenticated HTTP client using a service account file. +func (d *demo$object_type_titlecase) auth() { + b, _ := os.ReadFile(os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")) + credentials, _ := google.JWTConfigFromJSON(b, "https://www.googleapis.com/auth/wallet_object.issuer") + d.credentials = credentials + d.httpClient = d.credentials.Client(oauth2.NoContext) +} + +// [END auth] + +// [START createClass] +// Create a class. +func (d *demo$object_type_titlecase) createClass(issuerId, classSuffix string) { + newClass := fmt.Sprintf(`$class_payload`, issuerId, classSuffix) + + res, err := d.httpClient.Post(classUrl, "application/json", bytes.NewBuffer([]byte(newClass))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Class insert response:\n%s\n", b) + } +} + +// [END createClass] + +// [START createObject] +// Create an object. +func (d *demo$object_type_titlecase) createObject(issuerId, classSuffix, objectSuffix string) { + newObject := fmt.Sprintf(`$object_payload`, issuerId, classSuffix, issuerId, objectSuffix) + + res, err := d.httpClient.Post(objectUrl, "application/json", bytes.NewBuffer([]byte(newObject))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object insert response:\n%s\n", b) + } +} + +// [END createObject] + +// [START expireObject] +// Expire an object. +// +// Sets the object's state to Expired. If the valid time interval is +// already set, the pass will expire automatically up to 24 hours after. +func (d *demo$object_type_titlecase) expireObject(issuerId, objectSuffix string) { + patchBody := `{"state": "EXPIRED"}` + url := fmt.Sprintf("%s/%s.%s", objectUrl, issuerId, objectSuffix) + req, _ := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer([]byte(patchBody))) + res, err := d.httpClient.Do(req) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Object expiration response:\n%s\n", b) + } +} + +// [END expireObject] + +// [START jwtNew] +// Generate a signed JWT that creates a new pass class and object. +// +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass class and object defined in the JWT are +// created. This allows you to create multiple pass classes and objects in +// one API call when the user saves the pass to their wallet. +func (d *demo$object_type_titlecase) createJwtNewObjects(issuerId, classSuffix, objectSuffix string) { + newClass := fmt.Sprintf(`$class_payload`, issuerId, classSuffix) + + newObject := fmt.Sprintf(`$object_payload`, issuerId, classSuffix, issuerId, objectSuffix) + + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "genericClasses": [%s], + "genericObjects": [%s] + } + `, newClass, newObject)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtNew] + +// [START jwtExisting] +// Generate a signed JWT that references an existing pass object. + +// When the user opens the "Add to Google Wallet" URL and saves the pass to +// their wallet, the pass objects defined in the JWT are added to the +// user's Google Wallet app. This allows the user to save multiple pass +// objects in one API call. +func (d *demo$object_type_titlecase) createJwtExistingObjects(issuerId string) { + var payload map[string]interface{} + json.Unmarshal([]byte(fmt.Sprintf(` + { + "eventTicketObjects": [{ + "id": "%s.EVENT_OBJECT_SUFFIX", + "classId": "%s.EVENT_CLASS_SUFFIX" + }], + + "flightObjects": [{ + "id": "%s.FLIGHT_OBJECT_SUFFIX", + "classId": "%s.FLIGHT_CLASS_SUFFIX" + }], + + "genericObjects": [{ + "id": "%s.GENERIC_OBJECT_SUFFIX", + "classId": "%s.GENERIC_CLASS_SUFFIX" + }], + + "giftCardObjects": [{ + "id": "%s.GIFT_CARD_OBJECT_SUFFIX", + "classId": "%s.GIFT_CARD_CLASS_SUFFIX" + }], + + "loyaltyObjects": [{ + "id": "%s.LOYALTY_OBJECT_SUFFIX", + "classId": "%s.LOYALTY_CLASS_SUFFIX" + }], + + "offerObjects": [{ + "id": "%s.OFFER_OBJECT_SUFFIX", + "classId": "%s.OFFER_CLASS_SUFFIX" + }], + + "transitObjects": [{ + "id": "%s.TRANSIT_OBJECT_SUFFIX", + "classId": "%s.TRANSIT_CLASS_SUFFIX" + }] + } + `, issuerId)), &payload) + + claims := jwt.MapClaims{ + "iss": d.credentials.Email, + "aud": "google", + "origins": []string{"www.example.com"}, + "typ": "savetowallet", + "payload": payload, + } + + // The service account credentials are used to sign the JWT + key, _ := jwt.ParseRSAPrivateKeyFromPEM(d.credentials.PrivateKey) + token, _ := jwt.NewWithClaims(jwt.SigningMethodRS256, claims).SignedString(key) + + fmt.Println("Add to Google Wallet link") + fmt.Println("https://pay.google.com/gp/v/save/" + token) +} + +// [END jwtExisting] + +// [START batch] +// Batch create Google Wallet objects from an existing class. +func (d *demo$object_type_titlecase) batchCreateObjects(issuerId, classSuffix string) { + data := "" + for i := 0; i < 3; i++ { + objectSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + + batchObject := fmt.Sprintf(`$batch_object_payload`, issuerId, classSuffix, issuerId, objectSuffix) + + data += "--batch_createobjectbatch\n" + data += "Content-Type: application/json\n\n" + data += "POST /walletobjects/v1/$object_typeObject\n\n" + data += batchObject + "\n\n" + } + data += "--batch_createobjectbatch--" + + res, err := d.httpClient.Post(batchUrl, "multipart/mixed; boundary=batch_createobjectbatch", bytes.NewBuffer([]byte(data))) + + if err != nil { + fmt.Println(err) + } else { + b, _ := io.ReadAll(res.Body) + fmt.Printf("Batch insert response:\n%s\n", b) + } +} + +// [END batch] + +func main() { + if len(os.Args) == 0 { + fmt.Println("Usage: go run demo_$object_type_lowercase.go ") + os.Exit(1) + } + + issuerId := os.Getenv("WALLET_ISSUER_ID") + classSuffix := strings.ReplaceAll(uuid.New().String(), "-", "_") + objectSuffix := fmt.Sprintf("%s-%s", strings.ReplaceAll(uuid.New().String(), "-", "_"), classSuffix) + + d := demo$object_type_titlecase{} + + d.auth() + d.createClass(issuerId, classSuffix) + d.createObject(issuerId, classSuffix, objectSuffix) + d.expireObject(issuerId, objectSuffix) + d.createJwtNewObjects(issuerId, classSuffix, objectSuffix) + d.createJwtExistingObjects(issuerId) + d.batchCreateObjects(issuerId, classSuffix) +}