將票證儲存至 Google Pay

所有票證類別都有共通的使用方式。舉例來說,所有會員卡、禮物卡、優惠、活動票券、登機證和大眾運輸票證都能以多種方式新增至 Google Pay 應用程式。選取下列其中一種方式即可瞭解詳情:


從 Android 應用程式中操作

你可以使用下列方法,將「儲存至 Google Pay」按鈕新增至 Android 應用程式:

使用 Android SDK

您可以透過 Android API 將票證儲存至 Google Pay。只要將「儲存至 Google Pay」按鈕整合至應用程式,您的客戶即可輕鬆將票證儲存至 Google Pay。

下列步驟是以會員票證為例,概略說明如何新增「儲存至 Google Pay」按鈕,但所有票證都適用相同的程序。

1. 建立類別

首先,請定義 LoyaltyClass。下方範例顯示了代表 LoyaltyClass 的 JSON 資源:

{
  "accountIdLabel": "Member Id",
  "accountNameLabel": "Member Name",
  "id": "2945482443380251551.ExampleClass1",
  "issuerName": "Baconrista",
  "kind": "walletobjects#loyaltyClass",
  "textModulesData": [
    {
      "header": "Rewards details",
      "body": "Welcome to Baconrista rewards.  Enjoy your rewards for being a loyal customer. " +
               "10 points for every dollar spent.  Redeem your points for free coffee, bacon and more!"
    }
  ],
  "linksModuleData": {
    "uris": [
      {
        "kind": "walletobjects#uri",
        "uri": "https://maps.google.com/map?q=google",
        "description": "Nearby Locations"
      },
      {
        "kind": "walletobjects#uri",
        "uri": "tel:6505555555",
        "description": "Call Customer Service"
      }
    ]
  },
  "imageModulesData": [
    {
      "mainImage": {
        "kind": "walletobjects#image",
        "sourceUri": {
          "kind": "walletobjects#uri",
          "uri": "https://farm4.staticflickr.com/3738/12440799783_3dc3c20606_b.jpg",
          "description": "Coffee beans"
        }
      }
    }
  ],
  "messages": [{
    "header": "Welcome to Banconrista Rewards!",
    "body": "Featuring our new bacon donuts.",
    "kind": "walletobjects#walletObjectMessage"
  }],
  "locations": [{
    "kind": "walletobjects#latLongPoint",
    "latitude": 37.424015499999996,
    "longitude": -122.09259560000001
    },{
    "kind": "walletobjects#latLongPoint",
    "latitude": 37.424354,
    "longitude": -122.09508869999999
    },{
    "kind": "walletobjects#latLongPoint",
    "latitude": 37.7901435,
    "longitude": -122.39026709999997
    },{
    "kind": "walletobjects#latLongPoint",
    "latitude": 40.7406578,
    "longitude": -74.00208940000002
  }],
  "programLogo": {
    "kind": "walletobjects#image",
    "sourceUri": {
      "kind": "walletobjects#uri",
      "uri": "https://farm8.staticflickr.com/7340/11177041185_a61a7f2139_o.jpg"
    }
  },
  "programName": "Baconrista Rewards",
  "rewardsTier": "Gold",
  "rewardsTierLabel": "Tier",
  "reviewStatus": "underReview",
  "hexBackgroundColor": "#ffffff",
  "heroImage": {
   "kind": "walletobjects#image",
   "sourceUri": {
     "kind": "walletobjects#uri",
     "uri": "https://farm8.staticflickr.com/7302/11177240353_115daa5729_o.jpg"
   }
  }
}

2. 建立物件

建立類別之後,請按照下方程式碼片段所示定義 LoyaltyObject

{
  "classId": "2945482443380251551.ExampleClass1",
  "id": "2945482443380251551.ExampleObject1",
  "accountId": "1234567890",
  "accountName": "Jane Doe",
  "barcode": {
    "alternateText": "12345",
    "type": "qrCode",
    "value": "28343E3"
  },
  "textModulesData": [{
    "header": "Jane's Baconrista Rewards",
    "body": "Save more at your local Mountain View store Jane. " +
              "You get 1 bacon fat latte for every 5 coffees purchased.  " +
              "Also just for you, 10% off all pastries in the Mountain View store."
  }],
  "linksModuleData": {
    "uris": [
      {
        "kind": "walletobjects#uri",
        "uri": "https://www.baconrista.com/myaccount?id=1234567890",
        "description": "My Baconrista Account"
      }]
  },
  "infoModuleData": {
    "labelValueRows": [{
      "columns": [{
        "label": "Next Reward in",
        "value": "2 coffees"
      }, {
        "label": "Member Since",
        "value": "01/15/2013"
      }]
    }, {
      "columns": [{
        "label": "Local Store",
        "value": "Mountain View"
      }]
    }],
    "showLastUpdateTime": "true"
  },
  "loyaltyPoints": {
    "balance": {
      "string": "5000"
    },
    "label": "Points",
      "pointsType": "points"
  },
  "messages": [{
    "header": "Jane, welcome to Banconrista Rewards!",
    "body": "Thanks for joining our program. Show this message to " +
              "our barista for your first free coffee on us!"
  }],
  "state": "active"
}

3. 對未簽署的 JWT 進行編碼

建立物件之後,請透過編碼將 LoyaltyClassLoyaltyObject 編入未簽署的 JWT,如下方程式碼片段所示:

{
  "iss": "example_service_account@developer.gserviceaccount.com",
  "aud": "google",
  "typ": "savetoandroidpay",
  "iat": 1368029586,
  "payload": {
    "eventTicketClasses": [{
      ... //Event ticket Class JSON
    }],
    "eventTicketObjects": [{
      ... //Event ticket Object JSON
    }],
    "flightClasses": [{
      ... //Flight Class JSON
    }],
    "flightObjects": [{
      ... //Flight Object JSON
    }],
    "giftCardClasses": [{
      ... //Gift card Class JSON
    }],
    "giftCardObjects": [{
      ... //Gift card Object JSON
    }],
    "loyaltyClasses": [{
      ... //Loyalty Class JSON
    }],
    "loyaltyObjects": [{
      ... //Loyalty Object JSON
    }],
    "offerClasses": [{
      ... //Offer Class JSON
    }],
    "offerObjects": [{
      ... //Offer Object JSON
    }],
    "transitClasses": [{
      ... //Transit Class JSON
    }],
    "transitObjects": [{
      ... //Transit Object JSON
    }]
  },
  "origins": ["http://baconrista.com", "https://baconrista.com"]
}

4. 選擇要使用的要求格式

您可以透過 Android SDK 以下列其中一種格式發送要求:

如要進一步瞭解如何發送要求,請參閱呼叫 Android SDK

savePasses

savePasses 方法發送的要求會含有 JSON 字串酬載。也就是說,您可以直接將 JSON 用於步驟 3 中建立的物件。

您可以向「儲存至 Google Pay」功能傳送要求,以便處理在儲存程序中插入或既有的類別和物件。如果相關票證類別提供支援,您也可以透過同一項要求儲存多張票證。不過請注意,您無法加插既有的類別和物件。

為提高安全性,系統會將某些欄位視為機密欄位。以這些欄位來說,單純指定物件 ID 欄位並無法儲存既有的票證。要求中的機密欄位必須與既有物件的欄位相符,您才能儲存既有物件。機密欄位如下:

  • object.barcode.value
  • object.smartTapRedemptionValue
savePassesJwt

savePassesJwt 方法發送的要求會含有 JWT 字串權杖酬載。如要建立 JWT,請使用您的 OAuth 2.0 服務帳戶私密金鑰來簽署步驟 3 中產生的物件。下列程式碼片段展示了如何以不同程式語言建立 JWT 編碼:

Java

WobCredentials credentials = null;
WobUtils utils = null;

// Instantiate the WobUtils class which contains handy functions
// Wob utils can be found in the quickstart sample
try {
  credentials = new WobCredentials(
    ServiceAccountEmailAddress,
    ServiceAccountPrivateKeyPath,
    ApplicationName,
    IssuerId);
  utils = new WobUtils(credentials);
} catch (GeneralSecurityException e) {
  e.printStackTrace();
} catch (IOException e) {
  e.printStackTrace();
}

// Add valid domains for the Save to Wallet button
List<String> origins = new ArrayList<String>();
origins.add("http://baconrista.com");
origins.add("https://baconrista.com");
origins.add(req.getScheme() + "://" + req.getServerName() + ":" + req.getLocalPort());

//Generate Objects and Classes here
//........

WobPayload payload = new WobPayload();
payload.addObject({WalletObject/WalletClass});

// Convert the object into a Save to Android Pay Jwt
String jwt = null;
try {
  jwt = utils.generateSaveJwt(payload, origins);
} catch (SignatureException e) {
  e.printStackTrace();
}

PHP

$requestBody = [
  "iss"=> SERVICE_ACCOUNT_EMAIL_ADDRESS,
  "aud" => "google",
  "typ" => "savetoandroidpay",
  "iat"=> time(),
  "payload" => {
    "eventTicketClasses" => [ ], # Event ticket classes
    "eventTicketObjects" => [ ], # Event ticket objects
    "flightClasses" => [ ],      # Flight classes
    "flightObjects" => [ ],      # Flight objects
    "giftCardClasses" => [ ],    # Gift card classes
    "giftCardObjects" => [ ],    # Gift card objects
    "loyaltyClasses" => [ ],     # Loyalty classes
    "loyaltyObjects" => [ ],     # Loyalty objects
    "offerClasses" => [ ],       # Offer classes
    "offerObjects" => [ ],       # Offer objects
    "transitClasses" => [ ],     # Transit classes
    "transitObjects" => [ ]      # Transit objects
  },
  "origins" => ["http://baconrista.com", "https://baconrista.com"]
]
// Generate the Save to Android Pay Jwt
echo $jwt = $assertObj->makeSignedJwt($requestBody, $client);

Python

jwt = {
  'iss': config.SERVICE_ACCOUNT_EMAIL_ADDRESS,
  'aud': 'google',
  'typ': 'savetoandroidpay',
  'iat':  int(time.time()),
  'payload': {
    'webserviceResponse': {
      'result': 'approved',
      'message': 'Success.'
    },
    'eventTicketClasses': [], # Event ticket classes
    'eventTicketObjects': [], # Event ticket objects
    'flightClasses': [],      # Flight classes
    'flightObjects': [],      # Flight objects
    'giftCardClasses': [],    # Gift card classes
    'giftCardObjects': [],    # Gift card objects
    'loyaltyClasses': [],     # Loyalty classes
    'loyaltyObjects': [],     # Loyalty objects
    'offerClasses': [],       # Offer classes
    'offerObjects': [],       # Offer objects
    'transitClasses': [],     # Transit classes
    'transitObjects': []      # Transit objects
  },
  'origins' : ['http://baconrista.com', 'https://baconrista.com']
}

// Generate the Save to Android Pay Jwt
signer = crypt.Signer.from_string(app_key)
signed_jwt = crypt.make_signed_jwt(signer, jwt)
response = webapp2.Response(signed_jwt)

您可以向「儲存至 Google Pay」功能傳送要求,以便處理在儲存程序中插入或既有的類別和物件。如果相關票證類別提供支援,您也可以透過同一項要求儲存多張票證。不過請注意,您無法加插既有的類別和物件。只要類別和物件已存在,您就能使用 精簡 JWT。

5. 呼叫 Android SDK

首先,請使用 getPayApiAvailabilityStatus 方法來檢查 savePassessavePassesJwt 方法是否可用,如下方範例所示:

import com.google.android.gms.common.api.UnsupportedApiCallException;
import com.google.android.gms.pay.Pay;
import com.google.android.gms.pay.PayApiAvailabilityStatus;
import com.google.android.gms.pay.PayClient;
…
PayClient payClient = Pay.getClient(this);
payClient
  // Use PayClient.RequestType.SAVE_PASSES_JWT for the savePassesJwt API
  .getPayApiAvailabilityStatus(PayClient.RequestType.SAVE_PASSES)
  .addOnSuccessListener(
    status -> {
      switch (status) {
        case PayApiAvailabilityStatus.AVAILABLE:
          // You can call the savePasses API or savePassesJwt API
          ...
          break;
        case PayApiAvailabilityStatus.NOT_ELIGIBLE:
        default:
          // We recommend to either:
          // 1) Hide the save button
          // 2) Fall back to a different Save Passes integration (e.g. JWT link)
          //    Note however that the user *will* only be able to access their
          //    passes on web
          // A not eligible user might become eligible in the future.
          ...
          break;
        }
      })
  .addOnFailureListener(
    exception -> {
      if (exception instanceof UnsupportedApiCallException) {
        // Google Play Services too old. We could not check API availability or
        // user eligibility. We recommend to either:
        // 1) Fall back to a different Save Passes integration (e.g. JWT link)
        //    Note however that the user *may* only be able to access their
        //    passes on web
        // 2) Hide the save button
        ...
      } else {
        // Very old version of Google Play Services or unexpected error!
        ...
      }
    });

如果 API 可供使用,請在使用者輕觸 [儲存至 Google Pay] 按鈕時呼叫 savePassessavePassesJwt 方法。

savePasses

private static final int SAVE_TO_GOOGLE_PAY = 1000;
…
String jsonString = … // Build or fetch JSON request
PayClient payClient = Pay.getClient(this);
payClient.savePasses(jsonString, this, SAVE_TO_GOOGLE_PAY);

savePassesJwt

private static final int SAVE_TO_GOOGLE_PAY = 1000;
…
String jwtString = … // Fetch JWT from a secure server
PayClient payClient = Pay.getClient(this);
payClient.savePassesJwt(jwtString, this, SAVE_TO_GOOGLE_PAY);

這項呼叫會觸發儲存流程。流程完成之後,您的應用程式會透過 onActivityResult 解析結果。在「活動」部分中,這個接收端必須比照以下方式來定義:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  // `data` will only have information in the `SAVE_ERROR` case
  if (requestCode == SAVE_TO_GOOGLE_PAY) {
    switch (resultCode) {
      case Activity.RESULT_OK:
        // Save successful
        ...
        break;
      case Activity.RESULT_CANCELED:
        // Save canceled
        ...
        break;
      case PayClient.SavePassesResult.API_ERROR:
        // API error - this should not happen if getPayApiAvailabilityStatus is
        // used correctly
        ...
        break;
      case PayClient.SavePassesResult.SAVE_ERROR:
        // Save error - check EXTRA_API_ERROR_MESSAGE to debug the issue
        // Most save errors indicate an error in the app fingerprint or the Json
        // request payload. In most cases prompting the user to try again will not
        // help.
        if (data != null &&
            !isEmpty(data.getStringExtra(PayClient.EXTRA_API_ERROR_MESSAGE))) {
          ...
        } else {
          // Unexpected! A save error should always have a debug message associated
          // with it
          ...
        }
        break;
      case PayClient.SavePassesResult.INTERNAL_ERROR:
      default:
        // Internal error - prompt the user to try again, if the error persists
        // disable the button
        ...
        break;
    }
  } else {
    ...
  }
}

6. 將「儲存至 Google Pay」按鈕新增至您的使用者介面

您可以將 Google Pay 提供的 Android SDK 按鈕整合至應用程式,該按鈕的素材資源已列於品牌規範

這個工具包含有按鈕的向量圖片。

如要在應用程式中加入按鈕,請將工具包中的按鈕圖片複製到應用程式的 res 資料夾內,並將下方程式碼新增至 Android 版面配置檔案。請注意,除了正確的 src 值之外,每個按鈕也需要專屬的 contentDescription 字串和 minWidth 值。

<ImageButton
             android:layout_width="match_parent"
             android:layout_height="48dp"
             android:minWidth="200dp"
             android:clickable="true"
             android:src="@drawable/s2ap" />

按鈕的 layout_height 應為 48 dp,minWidth 則須為 200 dp。

請按照下列步驟將票證從應用程式儲存至 Google Pay:

  1. 完成將「儲存至 Google Pay」按鈕新增至電子郵件或簡訊一節中所述的步驟。
  2. 使用 ACTION_VIEW 意圖來開啟「儲存至 Google Pay」按鈕的深層連結。

    確認觸發意圖的按鈕符合品牌宣傳指南的規定。

以下是流程摘要的示例:

  1. 在儲存票證前的某個時間點,系統會透過 REST API 在後端建立類別。
  2. 使用者提出票證儲存要求時,您的伺服器後端會將代表物件的 JWT 傳送至您的 Android 用戶端應用程式。
  3. 您的 Android 用戶端應用程式包含符合品牌宣傳指南的 [儲存至 Google Pay] 按鈕。使用者按一下按鈕之後,系統會開啟連至 URI 的 ACTION_VIEW 意圖,其路徑中包含該組 JWT。示例如下:
    https://pay.google.com/gp/v/save/{jwt_generated}
    

使用 JWT POST 要求方法

您可以選擇透過 JWT POST 要求方法,為 Android 應用程式建立航班機票或活動票券類別與物件。如果儲存物件前,較難實作建立及插入類別所需的後端工作,這個方法就能派上用場。這個方法非常適合用於活動票券和登機證,因為這類票證會隨著時間經過建立許多類別。概略流程如下:

  1. 使用者在登機報到或兌換活動票券時,伺服器後端會向您的 Android 用戶端應用程式提供含有類別「和」物件的 JWT。
  2. 您的 Android 用戶端應用程式含有符合品牌規範的「儲存至 Google Pay」按鈕。點擊按鈕之後,將會發生下列情況:
    1. POST 要求透過 HTTPS 將 JWT 傳送至 Google 端點。
    2. 接著,系統會在 HTTP 回應內容產生後傳送當中的 URI,這個 URI 會用來開啟 ACTION_VIEW 意圖。

使用 JWT POST 要求方法時,您也必須提供 API 金鑰。這組金鑰會以查詢參數的形式附加至 REST API 呼叫。

建立類別

只有在收到先前未曾儲存過的 class.id 時,後端才會建立新類別。因此,雖然您可能會透過 JWT 重複將類別詳細資料傳送給 Google,但後端可辨識出該類別已儲存,因此不會在每次儲存登機證時,都建立新的類別。

更新類別

使用者儲存第一張登機證之後,系統就會一併儲存物件和類別。如您所預期,您可以將 class.id 和 REST API 搭配使用,以便執行 ADDMESSAGEGETLISTPATCHUPDATE 作業。

如要變更類別詳細資料,則必須使用 Class Update API。如果您已透過 class.id=XYZ 建立類別和其他一些類別詳細資料,之後卻嘗試透過 class.id=XYZ 建立含有不同類別詳細資料的類別,則系統仍會保留原本的類別,不會套用任何變更。

JWT 格式

Google Pay API for Passes JWT 參考說明文件詳細說明了您傳送的 JWT 應採用何種格式。以這個 payload 來說,您要傳送一個物件項目 (代表您要建立的物件) 和一個類別項目 (當中包含您建立的類別)。

HTTP 要求

您可以使用 INSERT 方法插入 JWT 中指定的類別和物件,而且必須將 API 金鑰設為查詢參數。

JWT INSERT 方法

在您提供 JWT 後,INSERT 方法會在 JWT 中插入指定的類別和物件。如果插入成功,要求就會傳回 200 HTTP 回應。

HTTP 要求
POST https://walletobjects.googleapis.com/walletobjects/v1/jwt/

授權

這項要求不需經過授權,不過 JWT 必須使用 RSA-SHA256 進行簽署,簽署金鑰為 OAuth 服務帳戶產生的金鑰。

要求內容

在要求內容中,請按照下列結構提供資料:

{ “jwt” : string }

回應內容

如果要求成功,這個方法就會在回應內容中傳回下列結構:

{
    "saveUri": string,
    "resources": {
      "eventTicketClasses": [ eventTicketClass resource, ... ],
      "eventTicketObjects": [ eventTicketObject resource, ... ],
      "flightClasses": [ flightClass resource, ... ],
      "flightObjects": [ flightObject resource, ... ],
      "giftCardClasses": [ giftCardClass resource, ... ],
      "giftCardObjects": [ giftCardObject resource, ... ],
      "loyaltyClasses": [ loyaltyClass resource, ... ],
      "loyaltyObjects": [ loyaltyObject resource, ... ],
      "offerClasses": [ offerClass resource, ... ],
      "offerObjects": [ offerObject resource, ... ],
      "transitClasses": [ transitClass resource, ... ],
      "transitObjects": [ transitObject resource, ... ]
    }
}

開啟 saveUri 這個 URI 之後,使用者即可將 JWT 中識別的物件儲存至自己的 Google 帳戶中。不過請注意,系統傳回的 URI 效期僅有一週。

如要瞭解詳情,請參閱 JWT 端點參考資料

流程圖

如要查看流程圖,請參閱一般 API 流程一文。