Payment Request API:集成指南

作者:Eiji Kitamura、Dave Gash、Zach Koch

Dogfood:PaymentRequest仍处于开发阶段。虽然我们认为其稳定性足以满足实现的要求,但可能仍需作出改动。 我们会持续更新本页,以时刻反映 API 的最新状况(M56 变更)。在此期间,为让您免于受到可能不具有向后兼容性的 API 变更的影响,我们提供了可嵌入网站的shim。这个 shim 可平息两个主流 Chrome 版本的任何 API 差异。

在线购物非常方便,但通常存在令人沮丧的体验,在移动设备上购物尤其如此。虽然移动流量在不断增长,但移动购物转化率仅占所有已完成购物活动的三分之一。换言之,移动设备用户的购物放弃率是桌面设备用户的两倍。为何?

用户放弃移动设备购物单的原因

在线购物单需要大量用户操作、难以使用,加载和刷新缓慢,并且需要多个步骤才能完成。这是因为在线支付的两大要素(即安全和便利)之间通常互为矛盾,左支右绌。

导致用户放弃交易的大部分问题都均与购物单有着直接关系。每个应用或网站均有自己的数据输入和验证流程,用户通常发现,在每个应用的购物点他们都需要输入相同的信息。此外,应用开发者力求创建支持多种独特支付方式的购物流程;即使是支付方式要求的细小差异也可能导致表单填写和提交过程变得相当复杂。

任何能或多或少改进这些问题的系统都值得提倡。我们已经尝试借助自动填充功能来帮助解决问题,但我们现在有了更全面的解决方案。

Payment Request API 简介

Payment Request API 是一个旨在消灭结账表单的系统。它显著改进了购物流程期间的用户工作流,为用户提供更一致的体验,并让电商公司能够轻松地利用各种完全不同的支付方式。Payment Request API 不是一种新的支付方式,也不与支付处理机构直接整合;它是一种旨在实现以下目标的处理层:

  • 让浏览器充当商家、用户和支付处理机构的中介
  • 尽可能标准化支付通信流程
  • 为不同的安全支付方式提供无缝支持
  • 适用于任何移动或非移动浏览器、设备或平台

Payment Request API 是一种开放式的跨浏览器标准,可以取代传统的结账流程,让商家能够在单个 API 调用中请求和接受任何付款。Payment Request API 允许网页在用户提供输入时与 User Agent 交换信息,然后核准或拒绝支付请求。

最重要的是,浏览器起到中介作用,为实现快速结账所需的全部信息均能储存在浏览器中,因此用户只需点击一次便可确认支付。

支付交易流程

利用 Payment Request API,可同时为用户和商家打造尽可能无缝的交易流程。

支付交易流程

流程一开始,商家网站首先创建新的PaymentRequest,并将购买所需的全部信息传递给浏览器:应支付的金额、期望使用的货币,以及网站接受的支付方式。浏览器确定网站所接受的支付方式和用户在目标设备上安装的支付方式之间的兼容性。

支付请求界面

然后,浏览器向用户显示支付 UI,用户选择支付方式并授权执行交易。支付方式可以简单直接,比如使用浏览器已存储的信用卡,也可能冷僻难解,比如使用专门针对网站支付开发的第三方应用(此功能即将实现)。用户授权执行交易后,所有必要的支付详细信息都将直接发回网站。例如,对于信用卡支付,网站将返回卡号、持卡人姓名、有效期和银行卡验证码 (CVC)。

Payment Request 也可以扩展为返回额外信息,例如收货地址和选项、付款人电子邮件和电话等。这样,无需要向用户显示结账表单,您即可获得完成支付所需的全部信息。

新流程有三重好处:从用户角度看,请求、授权、支付和结果等以往所有冗长的交互现在可一步完成;从网站角度看,只需调用一次 JavaScript API;从支付方式角度看,流程没有发生任何变化。

使用 Payment Request API

加载 Payment Request API shim

为减轻追赶这一现行标准 API 的压力,我们强烈建议在代码的<head>部分添加此 shim。 此 shim 将随 API 的变化而更新,并会尽力让代码能够至少在 Chrome 的 2 个主要版本上正常运行。

<script src="https://storage.googleapis.com/prshim/v1/payment-shim.js">

创建 PaymentRequest

创建 PaymentRequest 的第一步是通过调用PaymentRequest构造函数创建一个PaymentRequest对象。此步骤通常(并非总是)与用户发起的、表示其想要执行购买的操作相关。对象使用包含所需数据的参数构造。

var request = new PaymentRequest(
  methodData, // required payment method data
  details,    // required information about transaction
  options     // optional parameter for things like shipping, etc.
);

PaymentRequest 构造函数

methodData 参数

methodData参数包含支持的支付方式列表,以及有关支付方式的额外信息(如相关)。此序列含有PaymentMethodData字典,其中包括与应用预期接受的支付方式相关的标准标识符,以及任何支付方式的特定数据。如需了解更多详情,请参见Payment Request API 架构

目前,PaymentRequest在 Chrome 中仅支持以下标准信用卡:“amex”、“diners”、“discover”、“jcb”、“maestro”、“mastercard”、“unionpay”和“visa”。

var methodData = [
  {
    supportedMethods: ["visa", "mastercard"]
  }
]

支付方式和数据

details 参数

details参数包含有关交易的信息。有两个主要元素:total,反映要支付的总额和要使用的货币;可选的displayItems集,显示最终金额包含哪些分项。此参数并非用作明细项目列表,而是订单主要组成部分的摘要:总价、折扣、税、运费等。

支付请求界面

需要注意的是,Payment Request API 不执行计算。也就是说,它不会也不能确保显示的分项总和等于应付总金额。这些计算由开发者负责。因此,您应始终确保分项之和与总金额一致。此外,PaymentRequest不支持退款,因此金额应始终为正(但单独的分项可以为负,比如折扣)。

浏览器将根据您的定义渲染标签,并根据用户的语言区域自动应用正确的货币格式。请注意,应使用与内容相同的语言渲染标签。

var details = {
  displayItems: [
    {
      label: "Original donation amount",
      amount: { currency: "USD", value : "65.00" }, // US$65.00
    },
    {
      label: "Friends and family discount",
      amount: { currency: "USD", value : "-10.00" }, // -US$10.00
      pending: true // The price is not determined yet
    }
  ],
  total:  {
    label: "Total",
    amount: { currency: "USD", value : "55.00" }, // US$55.00
  }
}

交易详情

pending常用于显示运费或税额等取决于收货地址或收货选项的项目。Chrome 会在 UI 中指示支付请求的待定字段。

details中所使用的重复值或计算值可指定为字符串字面量或各字符串变量。

var currency = "USD";
var amount = "65.00";
var discount = "-10.00";
var total = "55.00";

PaymentRequest 变量

显示 PaymentRequest

支付请求界面

通过调用show()方法激活PaymentRequest界面。此方法会调用一个原生 UI,让用户检查购物详情、添加或更改信息并最终进行支付。用户接受或拒绝支付请求时,将返回可解析Promise(带有其then()方法和回调函数)。

request.show().then(function(paymentResponse) {
  // Process paymentResponse here
  paymentResponse.complete("success");
}).catch(function(err) {
  console.error("Uh oh, something bad happened", err.message);
});

PaymentRequest show 方法

取消 PaymentRequest

您可以通过调用abort()方法主动取消PaymentRequest。这在购物会话超时或交易期间购物车中的商品售罄时特别有用。

如果应用需要在调用show()方法之后、promise 尚未解析之前(例如,如果商品已下架,或者用户没有在指定时间内确认订单)取消支付请求,则可使用此方法。

如果您取消请求,将需要创建一个新的PaymentRequest实例,才能再次调用show()

var paymentTimeout = window.setTimeout(function() {
  window.clearTimeout(paymentTimeout);
  request.abort().then(function() {
    console.log('Payment timed out after 20 minutes.');
  }).catch(function() {
    console.log('Unable to abort.');
  });
}, 20 * 60 * 1000);  /* 20 minutes */

PaymentRequest abort 方法

处理 PaymentResponse

用户批准库款请求后,show()方法的 promise 会立即解析,生成PaymentResponse对象。

PaymentResponse 具有下列字段:
methodName 表示所选支付方式的字符串(例如 visa)
details 含有methodName信息的字典
shippingAddress 用户的收货地址(如有请求)
shippingOption 所选发货选项的 ID(如有请求)
payerEmail 付款人的电子邮件地址(如有请求)
payerPhone 付款人的电话号码(如有请求)
payerName 付款人的姓名(如有请求)

对于信用卡付款,响应为标准格式。对于非信用卡付款(例如 Android Pay),响应将由支付服务商提供文档说明。信用卡响应包含下列字典:

cardholderName、cardNumber、expiryMonth、expiryYear、cardSecurityCode、billingAddress

接收支付信息后,应用需将支付信息提交给支付处理机构进行处理。发生请求时,UI 会显示一个转环。收到响应后,应用需调用complete()来关闭 UI。

request.show().then(paymentResponse => {
  var paymentData = {
    // payment method string, e.g. “visa”
    method: paymentResponse.methodName,
    // payment details as you requested
    details: paymentResponse.details
  };
  return fetch('/pay', {
    method: 'POST',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(paymentData)
  }).then(res => {
    if (res.status === 200) {
      return res.json();
    } else {
      throw 'Payment Error';
    }
  }).then(res => {
    paymentResponse.complete("success");
  }, err => {
    paymentResponse.complete("fail");
  });
}).catch(err => {
  console.error("Uh oh, something bad happened", err.message);
});

complete()方法告知 User Agent 用户交互已结束,并允许应用将结果通知用户以及处理剩余的 UI 元素。

paymentResponse.complete('success').then(() => {
  // Success UI
}

paymentResponse.complete('fail').then(() => {
  // Error UI
};

PaymentRequest complete 方法

收集收货地址

如果您是出售实体物品的商家,则可能需要通过 Payment Request API 收集用户的收货地址。这通过将requestShipping: true添加到options参数来实现。设置此参数后,“Shipping”将添加到 UI,用户可从已保存的地址列表中选择收货地址,也可以添加新的收货地址。

您也可以通过指定shippingType在 UI 中使用“Delivery”或“Pickup”替代“Shipping”。这仅作显示用途。

注:details.shippingOptions在初始化时必须为undefined或空数组才能接收shippingaddresschange事件。否则事件将无法触发。

var options = {
  requestShipping: true,
  shippingType: "shipping" // "shipping"(default), "delivery" or "pickup"
};

var request = new PaymentRequest(methodData, details, options)

交易选项

用户选择或添加新的收货地址时,系统会动态计算发货选项。您可以添加shippingaddresschange事件的侦听器,它在用户选择收货地址时触发。然后,可以验证是否能够发货到该地址,计算发货选项并以新的发货选项和计价信息更新details.shippingOptions。您可以通过将某一选项的selected设为true来提供默认发货选项。

如果要出于地区不受支持等原因而拒绝使用某一地址,可以向details.shippingOptions传递空数组。UI 会通知用户,所选地址无法用于收货。

注:解析shippingaddresschange事件并保留details.shippingOptions的空数组状态也会导致地址遭拒(换言之,您无法发货到该地址)。请确保发货选项保持更新,并能匹配用户提供的地址。

request.addEventListener('shippingaddresschange', e => {
  e.updateWith(((details, addr) => {
    if (addr.country === 'US') {
      var shippingOption = {
        id: '',
        label: '',
        amount: {currency: 'USD', value: '0.00'},
        selected: true
      };
      if (addr.region === 'US') {
        shippingOption.id = 'us';
        shippingOption.label = 'Standard shipping in US';
        shippingOption.amount.value = '0.00';
        details.total.amount.value = '55.00';
      } else {
        shippingOption.id = 'others';
        shippingOption.label = 'International shipping';
        shippingOption.amount.value = '10.00';
        details.total.amount.value = '65.00';
      }
      if (details.displayItems.length === 2) {
        details.displayItems.splice(1, 0, shippingOption);
      } else {
        details.displayItems.splice(1, 1, shippingOption);
      }
      details.shippingOptions = [shippingOption];
    } else {
      details.shippingOptions = [];
    }
    return Promise.resolve(details);
  })(details, request.shippingAddress));
});

用户确认支付请求后,show()方法的 promise 得到解析。应用可使用PaymentResponse对象的.shippingAddress属性,将收货地址和其他属性告知支付处理机构。

request.show().then(paymentResponse => {
  var paymentData = {
    // payment method string
    method: paymentResponse.methodName,
    // payment details as you requested
    details: paymentResponse.details.toJSON(),
    // shipping address information
    address: paymentResponse.shippingAddress.toJSON()
  };
  // Send information to the server
});

让 PaymentRequest 获得渐进式增强

由于 Payment Request API 是一项新兴功能,许多浏览器尚未对其提供支持。要确定该功能是否可用,请查询window.PaymentRequest

if (window.PaymentRequest) {
  // PaymentRequest supported
  // Continue with PaymentRequest API
} else {
  // PaymentRequest NOT supported
  // Continue with existing form based solution
}

注:最好使用普通的常规结账流程链接。然后使用 JavaScript 在支持 PaymentRequest 的情况下阻止导航。

汇总

function onBuyClicked(event) {
  if (!window.PaymentRequest) {
    return;
  }
  // Payment Request API is available.
  // Stop the default anchor redirect.
  event.preventDefault();

  var supportedInstruments = [{
    supportedMethods: [
      'visa', 'mastercard', 'amex', 'discover', 'maestro',
      'diners', 'jcb', 'unionpay', 'bitcoin'
    ]
  }];

  var details = {
    displayItems: [{
      label: 'Original donation amount',
      amount: { currency: 'USD', value: '65.00' }
    }, {
      label: 'Friends and family discount',
      amount: { currency: 'USD', value: '-10.00' }
    }],
    total: {
      label: 'Total due',
      amount: { currency: 'USD', value : '55.00' }
    }
  };

  var options = {
    requestShipping: true,
    requestPayerEmail: true,
    requestPayerPhone: true,
    requestPayerName: true
  };

  // Initialization
  var request = new PaymentRequest(supportedInstruments, details, options);

  // When user selects a shipping address
  request.addEventListener('shippingaddresschange', e => {
    e.updateWith(((details, addr) => {
      var shippingOption = {
        id: '',
        label: '',
        amount: { currency: 'USD', value: '0.00' },
        selected: true
      };
      // Shipping to US is supported
      if (addr.country === 'US') {
        shippingOption.id = 'us';
        shippingOption.label = 'Standard shipping in US';
        shippingOption.amount.value = '0.00';
        details.total.amount.value = '55.00';
      // Shipping to JP is supported
      } else if (addr.country === 'JP') {
        shippingOption.id = 'jp';
        shippingOption.label = 'International shipping';
        shippingOption.amount.value = '10.00';
        details.total.amount.value = '65.00';
      // Shipping to elsewhere is unsupported
      } else {
        // Empty array indicates rejection of the address
        details.shippingOptions = [];
        return Promise.resolve(details);
      }
      // Hardcode for simplicity
      if (details.displayItems.length === 2) {
        details.displayItems[2] = shippingOption;
      } else {
        details.displayItems.push(shippingOption);
      }
      details.shippingOptions = [shippingOption];

      return Promise.resolve(details);
    })(details, request.shippingAddress));
  });

  // When user selects a shipping option
  request.addEventListener('shippingoptionchange', e => {
    e.updateWith(((details) => {
      // There should be only one option. Do nothing.
      return Promise.resolve(details);
    })(details));
  });

  // Show UI then continue with user payment info
  request.show().then(result => {
    // POST the result to the server
    return fetch('/pay', {
      method: 'POST',
      credentials: 'include',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(result.toJSON())
    }).then(res => {
      // Only if successful
      if (res.status === 200) {
        return res.json();
      } else {
        throw 'Failure';
      }
    }).then(response => {
      // You should have received a JSON object
      if (response.success == true) {
        return result.complete('success');
      } else {
        return result.complete('fail');
      }
    }).then(() => {
      console.log('Thank you!',
          result.shippingAddress.toJSON(),
          result.methodName,
          result.details.toJSON());
    }).catch(() => {
      return result.complete('fail');
    });
  }).catch(function(err) {
    console.error('Uh oh, something bad happened: ' + err.message);
  });
}

// Assuming an anchor is the target for the event listener.
document.querySelector('#start').addEventListener('click', onBuyClicked);

results matching ""

    No results matching ""