Payment Request API, el nuevo estándar para pagos en Internet (cuarta parte)

4 de septiembre de 2018

Este tutorial es la cuarta parte de la siguiente serie de tutoriales que explica el funcionamiento práctico de la nueva API llamada Payment Request para hacer pagos en Internet:

  • Primera parte: introducción general y cómo definir las formas de pago disponibles.
  • Segunda parte: cómo mostrar los detalles del pago y solicitar información al usuario.
  • Tercera parte: cómo completar (o cancelar) el proceso de pago.
  • Cuarta parte: cómo solicitar la dirección y forma de envío.

Solicitando la información de envío

Establece la propiedad requestShipping a true en el objeto de opciones para indicarle al navegador que el usuario debe rellenar la información de envío:

const options = {
  requestShipping: true,
};

Así es como el navegador muestra esta opción:

Solicitando al usuario la información de envío

Por defecto el título de esta opción es "Shipping" en inglés (o "Envío" en español), pero se puede cambiar fácilmente. Por ejemplo, un servicio de reparto de comida puede querer llamarlo "Delivery" ("entrega") y un servicio de lavandería podría llamarlo "Pickup" ("recogida").

Para ello, cambia el valor de la propiedad shippingType a uno de los valores estandarizados como "Delivery" o "Pickup":

Cambiando el título de las opciones de envío

La forma en la que el usuario indica la dirección de envío sigue un proceso estandarizado resumido en la siguiente imagen:

Proceso completo de la selección de la dirección de envío

El flujo aproximado es:

  1. El usuario pincha el botón de envío.
  2. El usuario selecciona una dirección predefinida o añade una nueva.
  3. La aplicación puede opcionalmente validar la dirección introducida.
  4. Se activa el campo con las opciones de envío que ofrece tu tienda (por ejemplo, envío normal, prioritario, etc.)
  5. El usuario selecciona una de las diferentes opciones de envío disponibles.
  6. La aplicación puede opcionalmente validar la opción seleccionada.
  7. El usuario ya puede continuar con la compra.

Respondiendo a los cambios en la dirección de envío

Si la aplicación solicita la dirección de envío, cada vez que el usuario cambia la dirección se genera un evento de tipo shippingAddressChange. Así la aplicación puede comprobar que la dirección cumple las condiciones que requiera tu tienda (por ejemplo, no enviar a determinados países).

Actualmente algunos navegadores obligan a definir un listener para este evento shippingaddresschange, pero en el futuro podría ser opcional. Este es un ejemplo de cómo crear este listener:

const paymentRequest = new PaymentRequest(
    supportedPaymentMethods, paymentDetails, options);

paymentRequest.addEventListener('shippingaddresschange', (event) => {
  // TODO: comprueba la dirección seleccionada por el usuario y
  // muestra las opciones de envío disponibles
});

paymentRequest.show()
.then(...)
.catch(...)

Obteniendo la dirección de envío

Cuando se lanza el evento shippingAddressChange, la dirección seleccionada se añade al objeto PaymentRequest asociado mediante su parámetro shippingAddress.

Por ejemplo, puedes mostrar la dirección de envío en la consola del navegador de la siguiente manera:

const paymentRequest = new PaymentRequest(...);
paymentRequest.addEventListener('shippingaddresschange', (event) => {
  console.log(paymentRequest.shippingAddress);
  // ...

  // también puedes usar event-target para acceder al objeto PaymentRequest:
  // const paymentRequestInstance = event.target;
  // console.log(paymentRequestInstance.shippingAddress);
});

Esto es lo que verías en la consola del navegador:

Detalles de la dirección de envío en la consola del navegador

El objeto shippingAddress tiene las siguientes propiedades de solo lectura:

Propiedad Explicación
recipient (String) Nombre de la persona de contacto.
organization (String) Empresa, institución u organismo que se encuentra en esa dirección
addressLine (Array de Strings) La dirección postal completa, que pued eincluir la dirección, número, código postal e incluso instrucciones específicas para encontrar la dirección
city (String) El nombre de la localidad/ciudad donde se encuentra la dirección
region (String) La subdivisión administrativa de primer nivel de acuerdo con el país ("estados" en México, "provincias" en Argentina, "comunidades" en España, "prefecturas" en Japón, etc.)
country (String) Código de dos letras del país según el CLDR (Common Locale Data Repository). Ejemplo: CO para Colombia, PE para Perú, CL para Chile, etc.
postalCode (String) Código postal al que pertenece la dirección
phone (String) Número de teléfono de contacto
languageCode (String) Código BCP-47 del idioma en el que está escrita la dirección (se utiliza para saber el orden y los separadores a usar cuando se muestra la dirección)
sortingCode (String) Código de ordenamiento de direcciones que se utiliza en algunos países, como por ejemplo Francia
dependentLocality (String) Región dentro de una ciudad a la que pertenece la dirección (por ejemplo, el barrio o distrito).

Definiendo las opciones de envío disponibles

El objeto que recibes cuando se lanza el evento shippingaddresschange es de tipo PaymentRequestUpdateEvent, que incluye un método llamado updateWith(). Este método se debe ejecutar con un objeto de tipo paymentDetails o con una promesa que se convierta en un objeto paymentDetails.

El siguiente ejemplo sencillo llama al método event.updateWith() con un objeto que define la cantidad total y un array vacío como shippingOptions:

paymentRequest.addEventListener('shippingaddresschange', (event) => {
  const paymentDetails = {
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: 10,
      },
    },
    shippingOptions: [],
  };
  event.updateWith(paymentDetails);
});

Cuando no devuelves ninguna opción de envío, le estás diciendo al navegador que esa dirección de envío no es válida para tu tienda, por lo que mostrará un mensaje de error al usuario:

Mensaje de error cuando no hay opciones de envío disponibles

Si quieres personalizar este mensaje de error, añade una propiedad llamada error en los detalles de la transacción que se pasan a event.updateWith():

paymentRequest.addEventListener('shippingaddresschange', (event) => {
  const paymentDetails = {
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: 10,
      },
    },
    error: 'This is an example error message 🎉',
    shippingOptions: [],
  };

  event.updateWith(paymentDetails);
});

Así se muestra este mensaje de error personalizado al usuario:

Mensaje de error personalizado cuando no hay opciones de envío disponibles

Añadiendo opciones de envío

Cuando la tienda dispone de opciones de envío para la dirección indicada, añádelas en la propiedad shippingOptions como un array de objetos, cada uno de ellos con las propiedades id, label y amount (a su vez dividido en value y currency):

paymentRequest.addEventListener('shippingaddresschange', (event) => {
  const paymentRequest = event.target;
  console.log(paymentRequest.shippingAddress);

  event.updateWith({
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: '0',
      },
    },
    shippingOptions: [
      {
        id: 'economy',
        label: 'Economy Shipping (5-7 Days)',
        amount: {
          currency: 'USD',
          value: '0',
        },
      }, {
        id: 'express',
        label: 'Express Shipping (2-3 Days)',
        amount: {
          currency: 'USD',
          value: '5',
        },
      }, {
        id: 'next-day',
        label: 'Next Day Delivery',
        amount: {
          currency: 'USD',
          value: '12',
        },
      },
    ],
  });
});

Este ejemplo añade varias opciones de envío para que el usuario elija la que le interese:

Elección de la forma de envío

En estos ejemplos, se llama al método event.updateWith() directamente con las opciones de envío disponibles. Sin embargo, es posible que tengas que hacer alguna llamada a tu aplicación para obtener esta información. En esos casos puedes pasar una promesa a event.updateWith(), tal y como se muestra en el siguiente ejemplo:

paymentRequest.addEventListener('shippingaddresschange', (event) => {
  const paymentRequest = event.target;
  console.log(paymentRequest.shippingAddress);

  const shippingAddrCheckPromise = fetch('/api/get-shipping-opts/', {
    method: 'POST',
    credentials: 'include',
     headers: {
       'Content-Type': 'application/json'
     },
    body: JSON.stringify(paymentRequest.shippingAddress),
  })
  .then((response) => {
    return response.json();
  })
  .then((responseData) => {
    return {
      total: {
        label: 'Total',
        amount: {
          currency: 'USD',
          value: '0',
        },
      },
      shippingOptions: responseData.shippingOptions,
    };
  });

  event.updateWith(shippingAddrCheckPromise);
});

En este caso, mientras se realiza la petición el usuario ve una pantalla de espera:

Pantalla de espera mientras se obtienen las formas de envío

Cosas a ener en cuenta

No llamar a updateWith()

Si no llamas a event.updateWith() o la promesa que le pasas no se resuelve en un tiempo razonable, el navegador considera el pago caducado y te devolverá el siguiente error:

DOMException: Timed out as the page didn't resolve the promise from change event

Tipo de envío no válido

Si el valor de shippingType es diferente a 'shipping', 'delivery' o 'pickup', verás el siguiente error:

The provided value '...' is not a valid enum value of type PaymentShippingType.

ID repetido en la forma de envío

Si dos o más de las formas de envío que devuelves contienen el mismo ID, el navegador considera que la dirección no es válida para el pago. Se trata del mismo comportamiento que cuando guardas un array vacío en la propiedad `shippingOptions y se muestra el mensaje "Can't ship to this address. Select a different address.".

Falta información en una forma de envío

Si alguna de las formas de envío no incluye toda la información necesaria, la promesa devuelta por show() la rechaza:

// La forma de envío no incluye el ID
DOMException: required member id is undefined.

// La forma de envío no incluye el título
DOMException: required member label is undefined.

// La forma de envío no incluye el coste
DOMException: required member amount is undefined.

// La forma de envío no incluye la divisa del coste
DOMException: required member currency is undefined.

// La forma de envío no incluye el valor del coste
DOMException: required member value is undefined.

Respondiendo a los cambios en la forma de envío

Al igual que sucede con los cambios de dirección, también es posible responder a los cambios en la forma de envío. De esta forma puedes marcar la opción como seleccionada y recalcular el coste total de la compra.

Actualmente algunos navegadores obligan a definir un listener para este evento shippingoptionchange, pero en el futuro podría ser opcional. Este es un ejemplo de cómo crear este listener:

paymentRequest.addEventListener('shippingoptionchange', (event) => {
  // TODO: seleccionar esa forma de envío y recalcular el total
});

Este evento es similar a shippingaddresschange en muchas cosas:

  • Debes pasar al método event.updateWith() un objeto de tipo paymentDetails con el total, displayItems y shippingoptions (también puedes pasar una promesa que se coniverta en un objeto de ese tipo).
  • Si pasas un array vacío a shippingoptions, se considera un error porque la dirección no está soportada.
  • Puedes mostrar un mensaje de error personalizado pasando un array vacío a shippingoptions y definiendo el mensaje en la propiedad error.

Lo único que debes hacer es marcar como seleccionada la opción de envío que ha elegido el usuario. Para ello debes hacer lo siguiente:

paymentRequest.addEventListener('shippingoptionchange', (event) => {
  // Paso 1: Obtén el objeto `PaymentRequest` mediante el evento
  const prInstance = event.target;

  // Paso 2: Obtén la opción de envío seleccionada mediante el objeto `PaymentRequest`
  const selectedId = prInstance.shippingOption;

  // Paso 3: Define la opción `selected` a `true` para la opción seleccionada
  globalShippingOptions.forEach((option) => {
    option.selected = option.id === selectedId;
  });

  // TODO: actualizar el total y los detalles a mostrar

  event.updateWith({
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: '0',
      },
    },
    shippingOptions: globalShippingOptions,
  });
});

Este código hace que el navegador actualice la información cuando el usuario selecciona una nueva opción de envío (o muestre una pantalla de espera si has devuelto una promesa en vez de un objeto).

Proceso completo de selección de la opción de envío

Cosas a tener en cuenta

Además de todas las cosas que se comentaron para el evento shippingaddresschange, también debes tener en cuenta lo siguiente:

No marcar una opción de envío como seleccionada

Si no marcas ninguna opción de envío como seleccionada, se producirá un bucle infinito: el usuario selecciona una forma de envío, pero como no se marca como seleccionada, el navegador el solicita otra vez que seleccione una forma de envío y así sucesivamente.

Seleccionar varias opciones de envío

Si marcas como selected varias opciones de envío, se utilizará la última de las que estén marcadas.

Completando los pagos más rápidamente

Todo lo visto hasta ahora es el flujo estándar recomendado y normal para realizar pagos, pero en ocasiones se pueden utilizar atajos para hacer el proceso más ágil.

Preseleccionar la forma de envío

Si las formas de envío son conocidas y siempre las mismas, puedes pasarlas en el constructor de PaymentRequest mediante un un objeto shippingOptions dentro del objeto paymentDetails:

const paymentDetails = {
  total: {
    label: 'Total',
    amount: {
      currency: 'USD',
      value: '0',
    },
  },
  shippingOptions: [
    {
      id: 'economy',
      label: 'Economy Shipping (5-7 Days)',
      amount: {
        currency: 'USD',
        value: '0',
      },
    },
  ],
};
new PaymentRequest(paymentMethods, paymentDetails, options);

En este caso, el usuario sigue teniendo que elegir la forma de envío. Sin embargo, si marcas alguna opción como selected, el usuario la mostrará preseleccionada:

const paymentDetails = {
  total: {
    label: 'Total',
    amount: {
      currency: 'USD',
      value: '0',
    },
  },
  shippingOptions: [
    {
      id: 'economy',
      label: 'Economy Shipping (5-7 Days)',
      selected: true,
      amount: {
        currency: 'USD',
        value: '0',
      },
    },
  ],
};
new PaymentRequest(paymentMethods, paymentDetails, options);

Así es como verá el usuario la forma de envío preseleccionada:

Ejemplo de opción de envío preseleccionada

Si preseleccionas una forma de envío, el usuario puede completar el pago inmediatamente al pulsar el botón "Pay". Lo malo es que no se lanza el evento shippingaddresschange, por lo que no puedes validar los detalles de la dirección.

Preseleccionar la dirección de envío

Igualmente, si marcas como selected alguna de las direcciones del usuario, el pago se podrá completar mucho más rápidamente.

paymentRequest.addEventListener('shippingaddresschange', (event) => {
  const paymentRequest = event.target;
  console.log(paymentRequest.shippingAddress);

  event.updateWith({
    total: {
      label: 'Total',
      amount: {
        currency: 'USD',
        value: '0',
      },
    },
    shippingOptions: [
      {
        id: 'economy',
        label: 'Economy Shipping (5-7 Days)',
        selected: true,
        amount: {
          currency: 'USD',
          value: '0',
        },
      }, {
        id: 'express',
        label: 'Express Shipping (2-3 Days)',
        amount: {
          currency: 'USD',
          value: '5',
        },
      }, {
        id: 'next-day',
        label: 'Next Day Delivery',
        amount: {
          currency: 'USD',
          value: '12',
        },
      },
    ],
  });
});