Aggiornato a novembre 2026. La versione del 2023 di questa parte usava un vecchio formato di Google Pay, chiudeva la finestra di pagamento con complete('success') prima di elaborare il pagamento e aveva un ramo per l’annullamento che non veniva mai eseguito. Il codice qui sotto la sostituisce.
Nella parte 1 abbiamo costruito una richiesta di pagamento che funziona: Google Pay in modalità di test, un pulsante che compare solo quando il browser può pagare e una gestione corretta degli annullamenti. In questa parte chiediamo a chi paga i dati di contatto, vediamo la spedizione, gestiamo i pagamenti che falliscono sul server, aggiungiamo Apple Pay e mettiamo tutto insieme in una demo da provare.
Chiedere i dati di chi paga
new PaymentRequest() accetta un terzo argomento facoltativo: un oggetto di opzioni che dice alla finestra di pagamento cos’altro raccogliere. Sono tutte false per impostazione predefinita.
const options = {
requestPayerName: true, // il nome di chi paga
requestPayerEmail: true, // il suo indirizzo email
requestPayerPhone: false // il suo numero di telefono
};
const request = new PaymentRequest([googlePay], details, options);
Quando l’utente conferma, i valori arrivano nella risposta come payerName, payerEmail e payerPhone. Google consiglia di raccogliere email, telefono e indirizzo di spedizione con queste opzioni del browser e non con l’API di Google Pay, così funzionano allo stesso modo con qualsiasi metodo di pagamento, e di usare Google Pay solo per i dati di pagamento e, se serve, per l’indirizzo di fatturazione.
Chiedi solo quello che userai. Ogni campo in più è una cosa in più che l’utente deve controllare prima di pagare.
La spedizione: funziona ancora, ma è destinata a sparire
L’API può anche raccogliere un indirizzo di spedizione e far scegliere all’utente un’opzione di spedizione, con il prezzo aggiornato nella finestra. Si attiva con requestShipping e le opzioni si descrivono dentro i dettagli del pagamento:
const options = { requestShipping: true, shippingType: 'shipping' }; // oppure 'delivery', 'pickup'
const details = {
total: { label: 'Totale', amount: { currency: 'EUR', value: '100.00' } },
shippingOptions: [
{ id: 'standard', label: 'Standard (3-5 giorni)', amount: { currency: 'EUR', value: '0.00' }, selected: true },
{ id: 'express', label: 'Express (24 h)', amount: { currency: 'EUR', value: '9.90' } }
]
};
Due eventi ti avvisano quando l’utente cambia qualcosa. In entrambi si risponde con event.updateWith(), passando i nuovi dettagli o una promise che si risolve con essi:
request.addEventListener('shippingaddresschange', (event) => {
event.updateWith(updateForAddress(request.shippingAddress));
});
request.addEventListener('shippingoptionchange', (event) => {
event.updateWith(updateForOption(request.shippingOption));
});
function updateForAddress(address) {
if (address.country !== 'IT') {
// una lista vuota vuol dire che non possiamo spedire a questo indirizzo
return { error: 'Spiacenti, spediamo solo in Italia.', shippingOptions: [] };
}
return { total: details.total, shippingOptions: details.shippingOptions };
}
function updateForOption(optionId) {
const shipping = optionId === 'express' ? 9.9 : 0;
return {
shippingOptions: details.shippingOptions.map((option) => ({ ...option, selected: option.id === optionId })),
total: { label: 'Totale', amount: { currency: 'EUR', value: (100 + shipping).toFixed(2) } }
};
}
Tre dettagli su cui si inciampa spesso:
- il totale non viene ricalcolato da solo: quando cambia l’opzione di spedizione, il nuovo totale lo mandi tu;
- mentre la finestra è aperta, il browser può nascondere parte dell’indirizzo per privacy, quindi il calcolo va fatto sul paese o sul CAP, e l’indirizzo completo si controlla nella risposta finale;
- Safari concede 30 secondi per risolvere
updateWith(); dopo, il pagamento scade.
Ora lo stato di tutto questo. MDN segna shippingaddresschange, shippingoptionchange e le proprietà collegate (shippingAddress, shippingOption, shippingType) come deprecati e non standard. Chrome (dalla versione 60) e Safari (dalla 11.1) li supportano ancora, quindi le integrazioni esistenti continuano a funzionare. Per un negozio nuovo è più prudente non dipenderne: chiedi l’indirizzo in un tuo modulo prima del pagamento, oppure lascia che se ne occupi il componente di checkout del tuo fornitore.
Quando il pagamento fallisce sul server: retry()
L’utente può confermare nella finestra e l’addebito può fallire lo stesso: una carta rifiutata, un indirizzo email non valido. Invece di chiudere la finestra e ricominciare da capo, response.retry() mostra di nuovo la stessa finestra con il tuo messaggio di errore, così l’utente può correggere. La promise si risolve quando conferma di nuovo, e lo stesso oggetto response contiene i dati nuovi. Chrome lo supporta dalla versione 78 e Safari dalla 12.1.
let outcome = await sendToServer(response);
while (!outcome.ok && typeof response.retry === 'function') {
try {
await response.retry({ error: outcome.message });
} catch (error) {
return; // l'utente ha chiuso la finestra
}
outcome = await sendToServer(response);
}
await response.complete(outcome.ok ? 'success' : 'fail');
Oltre a un error generale, retry() accetta errori per singoli campi, per esempio { payer: { email: 'Questo indirizzo email non è valido.' } }, che la finestra mostra accanto al campo. Nei browser senza retry() il ciclo viene saltato e il pagamento si chiude con complete('fail').
Aggiungere Apple Pay
Safari offre Apple Pay solo attraverso questa API. Il metodo va nello stesso array di Google Pay; ogni browser usa i metodi che supporta e ignora gli altri:
const applePay = {
supportedMethods: 'https://apple.com/apple-pay',
data: {
version: 3,
merchantIdentifier: 'merchant.com.example',
merchantCapabilities: ['supports3DS', 'supportsCredit', 'supportsDebit'],
supportedNetworks: ['amex', 'discover', 'masterCard', 'visa'],
countryCode: 'IT'
}
};
const request = new PaymentRequest([googlePay, applePay], details, options);
Perché funzioni ti servono un identificativo esercente Apple Pay (il merchant.com.example qui sopra) e un server configurato per elaborare i pagamenti Apple Pay. La guida di Apple sulla configurazione dell’ambiente elenca i passaggi.
Poi c’è la validazione dell’esercente. Prima che Safari mostri la finestra di Apple Pay, il tuo sito deve ottenere da Apple una sessione di pagamento. Poco dopo la chiamata a show(), Safari lancia un evento merchantvalidation con una validationURL. Il tuo server chiede la sessione a quell’URL e la pagina la passa, o ne passa una promise, a event.complete():
request.onmerchantvalidation = (event) => {
event.complete(fetchPaymentSession(event.validationURL));
};
async function fetchPaymentSession(validationURL) {
const answer = await fetch('/apple-pay/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ validationURL })
});
return answer.json();
}
L’endpoint /apple-pay/session è tuo: contatta Apple con le tue credenziali di esercente e restituisce la sessione. Anche questo evento è segnato su MDN come deprecato e non standard, e Chrome non lo supporta, ma è il modo in cui Safari gestisce Apple Pay. Dopo che l’utente ha autorizzato il pagamento, response.details contiene il token di Apple Pay per il tuo fornitore di pagamenti, e chiudi con complete('success') o complete('fail'), esattamente come con Google Pay.
La demo completa
La demo su CodePen mette tutto insieme con Google Pay in modalità di test, quindi non viene addebitato nulla. Servono un pulsante con id pay-button, nascosto all’inizio, e un paragrafo vuoto con id result. Apple Pay non c’è, perché la validazione dell’esercente richiede un server e un vero account esercente. Questo è il suo JavaScript, con i testi tradotti:
const button = document.getElementById('pay-button');
const result = document.getElementById('result');
// Google Pay in modalità TEST, nel formato che Google documenta per la Payment Request API
const googlePay = {
supportedMethods: 'https://google.com/pay',
data: {
environment: 'TEST',
apiVersion: 2,
apiVersionMinor: 0,
merchantInfo: { merchantName: 'Example Merchant' },
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
allowedCardNetworks: ['MASTERCARD', 'VISA']
},
tokenizationSpecification: {
type: 'PAYMENT_GATEWAY',
parameters: { gateway: 'example', gatewayMerchantId: 'exampleGatewayMerchantId' }
}
}]
}
};
const details = {
total: { label: 'Totale', amount: { currency: 'EUR', value: '100.00' } },
displayItems: [
{ label: 'Prodotto A', amount: { currency: 'EUR', value: '50.00' } },
{ label: 'Prodotto B', amount: { currency: 'EUR', value: '50.00' } }
]
};
const options = { requestPayerName: true, requestPayerEmail: true };
async function setUp() {
if (!window.PaymentRequest) {
return;
}
const request = new PaymentRequest([googlePay], details, options);
if (await request.canMakePayment()) {
button.hidden = false;
}
}
button.addEventListener('click', async () => {
const request = new PaymentRequest([googlePay], details, options);
let response;
try {
response = await request.show();
} catch (error) {
if (error.name === 'AbortError') {
result.textContent = 'Pagamento annullato.';
} else {
console.error(error);
result.textContent = 'Qualcosa è andato storto.';
}
return;
}
let outcome = await sendToServer(response);
while (!outcome.ok && typeof response.retry === 'function') {
try {
await response.retry({ error: outcome.message });
} catch (error) {
result.textContent = 'Pagamento annullato.';
return;
}
outcome = await sendToServer(response);
}
await response.complete(outcome.ok ? 'success' : 'fail');
result.textContent = outcome.ok
? 'Grazie, ' + response.payerName + '. La ricevuta arriverà a ' + response.payerEmail + '.'
: outcome.message;
});
// Solo per la demo: un negozio vero invia in POST response.details, payerName e payerEmail al suo server,
// che addebita il token tramite il fornitore di pagamenti e risponde con ok o con un messaggio di errore
async function sendToServer(response) {
console.log(response.methodName, response.details, response.payerName, response.payerEmail);
return { ok: true };
}
setUp();
Prima di andare in produzione
- Servi la pagina in HTTPS e tieni il checkout normale per i browser senza l’API, Firefox compreso.
- Non fidarti mai del totale che arriva dal browser: il server ricalcola l’ordine e addebita quell’importo.
- Chiama
complete()solo dopo che il server ha risposto. - Tratta
AbortErrorcome un annullamento, non come un errore. - Prova prima con l’ambiente
TESTdi Google Pay, poi passa aPRODUCTIONcon l’ID esercente che ti dà Google.
Per approfondire
- MDN (in inglese): PaymentRequest() e le sue opzioni, PaymentResponse.retry() e MerchantValidationEvent
- Google Pay con la Payment Request API (in inglese)
- Introducing the Payment Request API for Apple Pay, sul blog di WebKit (in inglese)