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:
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":
La forma en la que el usuario indica la dirección de envío sigue un proceso estandarizado resumido en la siguiente imagen:
El flujo aproximado es:
- El usuario pincha el botón de envío.
- El usuario selecciona una dirección predefinida o añade una nueva.
- La aplicación puede opcionalmente validar la dirección introducida.
- Se activa el campo con las opciones de envío que ofrece tu tienda (por ejemplo, envío normal, prioritario, etc.)
- El usuario selecciona una de las diferentes opciones de envío disponibles.
- La aplicación puede opcionalmente validar la opción seleccionada.
- 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:
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:
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:
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:
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:
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 tipopaymentDetails
con eltotal
,displayItems
yshippingoptions
(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 propiedaderror
.
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).
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:
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',
},
},
],
});
});