将卡券保存到 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 进行编码

创建对象后,将 LoyaltyClass 和 LoyaltyObject 编码到未签名的 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 字符串载荷。这意味着您可以直接为第 3 步中创建的对象使用 JSON。

您可以针对已存在或者在保存流程中插入的类和对象向“保存到 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 方法检查 savePasses 或 savePassesJwt 方法是否可用,如以下示例所示:

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 按钮时调用 savePasses 或 savePassesJwt 方法。

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 解析结果。在您的 Activity 中,需要按如下方式定义此接收器:

@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 是 48dp,minWidth 必须是 200dp。

要从应用将卡券保存到 Google Pay,请按以下步骤操作:

  1. 完成将“保存到 Google Pay”按钮添加到电子邮件或短信中的步骤。
  2. 使用 ACTION_VIEW Intent 打开保存到 Google Pay 按钮包含的深层链接。

    请确保触发 Intent 的按钮遵循品牌推广指南。

下面是流程摘要示例:

  1. 在保存卡券之前的某个时间,使用 REST API 在后端创建一个类。
  2. 当最终用户要求保存卡券时,您的服务器后端会将 JWT 发送给代表对象的 Android 客户端应用。
  3. 您的 Android 客户端应用包含符合我们品牌推广指南的保存到 Google Pay 按钮。 点击该按钮后,将针对路径中含有 JWT 的 URI 打开一个 ACTION_VIEW Intent。示例如下:
    https://pay.google.com/gp/v/save/{jwt_generated}

使用 JWT POST 请求方法

JWT POST 请求方法是为 Android 应用创建机票或活动门票类和对象的替代方法。在保存对象之前难以实现创建和插入类所需的后端工作时,就会使用此方法。此方法对于活动门票和登机牌非常有用,随着时间的推移,这些卡券会创建许多类。其流程归纳如下:

  1. 当最终用户办理登机手续或兑换活动门票时,您的服务器后端会将一个 JWT 呈现给同时包含该类和对象的 Android 客户端应用。
  2. 您的 Android 客户端应用包含符合我们品牌推广指南的保存到 Google 按钮。 点击该按钮时,将发生以下情况:
    1. POST 请求通过 HTTPS 将 JWT 发送到 Google 端点。
    2. 作为回应,系统将发送所产生 HTTP 响应正文中的 URI,该 URI 随后应该用于打开 ACTION_VIEW Intent。

JWT POST 请求方法还需要一个 API 密钥,它将以查询参数的形式附加到 REST API 调用。

类创建

只有在使用过去未保存的 class.id 呈现时,才会在后端创建新类。因此,虽然您可能会多次通过 JWT 将类详细信息传递给 Google,但后端会识别出该类已保存,并且不会在每次保存登机牌时都创建新类。

类更新

在第一张登机牌后,对象与类一起保存。您可以按照预期将 class.id 与我们的 REST API 一起使用,以便按预期执行 ADDMESSAGE、GET、LIST、PATCH 和 UPDATE 操作。

要更改类详细信息,您必须使用 Class Update API。如果您使用 class.id=XYZ 以及其他一些类详细信息来创建类,并且稍后尝试使用 class.id=XYZ 和不同的类详细信息来创建类,我们仍会保留原始类,不应用任何更改。

JWT 格式

如需详细了解您所发送的 JWT 的格式,请参阅我们的 Google Pay API for Passes 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,打开此 URI 后,最终用户可以将 JWT 中标识的对象保存到他们的 Google 帐号。此 URI 仅在返回后的一周内有效。

如需了解详情,请参阅 JWT 端点参考。

流程图

如需了解流程图,请参阅典型的 API 流程。