将卡券保存到 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 字符串载荷。这意味着您可以直接为第 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 方法检查 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 解析结果。在您的 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 一起使用,以便按预期执行 ADDMESSAGEGETLISTPATCHUPDATE 操作。

要更改类详细信息,您必须使用 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 流程