Acquisti in-app per iOS parte 4: convalida dell’acquisto lato server.
Updated: Marzo 20, 2023
Cos’è la convalida degli acquisti lato server?
La convalida lato server (convalida delle ricevute (receipt validation) lato server) è un modo per verificare l’autenticità dell’acquisto. A differenza della convalida basata sul dispositivo, quella del server si verifica, guarda un po’, sul lato server. “Convalida” significa che il dispositivo o il server effettua una richiesta ai server di Apple per sapere se l’acquisto sia effettivamente avvenuto e se sia valido.
Perché convalidare gli acquisti?
Va notato che la convalida lato server non è obbligatoria: gli acquisti in-app continueranno a funzionare anche senza. Tuttavia, assicura più di qualche vantaggio:
- Analisi avanzata dei pagamenti, che è particolarmente importante per gli abbonamenti, poiché tutto ciò che accade dopo l’attivazione non viene elaborato dal dispositivo. Senza l’elaborazione degli acquisti sul lato server, non sarà possibile recuperare lo stato attuale dell’abbonamento e sapere se l’utente abbia rinnovato l’abbonamento (subscription renewal) o lo abbia annullato (subscription cancellation), se ci siano problemi di pagamento (payment issues) e così via.
- Possibilità di verificare l’autenticità dell’acquisto. Sarai sicuro che la transazione non sia fraudolenta e che l’utente abbia effettivamente pagato il tuo prodotto.
- Abbonamenti a più piattaforme. Se puoi controllare lo stato dell’abbonamento dell’utente in tempo reale, puoi sincronizzarlo con altre piattaforme. Ad esempio, l’utente che ha acquistato l’abbonamento da un dispositivo iOS potrà utilizzarlo su Android, sul Web e su altre piattaforme.
- Possibilità di controllare l’accesso ai contenuti dal lato server, che protegge dagli utenti che cercano di accedere ai dati senza abbonamento, eseguendo semplicemente le richieste al server.
In base alla nostra esperienza, il primo vantaggio è già sufficiente per impostare l’elaborazione degli acquisti lato server.
Convalida dell’acquisto
In generale, il processo di convalida delle ricevute su iOS è il seguente:
Generazione di un segreto condiviso (shared secret)
Per inviare una richiesta di convalida del pagamento, è necessario includere il segreto condiviso, per autorizzare la richiesta. Può essere generato in App Store Connect.
Il segreto condiviso può essere creato per un’app specifica (segreto specifico dell’app) o per tutte le app dell’account (segreto primario).
Per generare un segreto specifico per l’app, apri la pagina dell’app in App Store Connect, vai su In-App Purchases → Manage e fai clic su App-Specific Shared Secret. Nella finestra che si aprirà, potrai generare un nuovo token o copiare quello esistente.
Per ricevere il segreto per tutte le app dell’account, apri la pagina Users e Access e seleziona la scheda Shared Secret.
Richiesta della convalida dei pagamenti
Una volta ricevuto il segreto condiviso, potrai inviare le ricevute per farle convalidare sui server Apple. Questo viene fatto tramite la richiesta verifyReceipt. Devi inviare una richiesta POST a https://buy.itunes.apple.com/verifyReceipt. Nel corpo JSON della richiesta, passa il segreto condiviso nel campo password e la ricevuta nel campo receipt-data. Esiste anche il parametro facoltativo exclude-old-transactions. Se ha il valore true per ogni abbonamento con rinnovamento automatico, si riceverà solo l’ultima transazione, invece dell’intera cronologia dei rinnovi.
Ecco il payload della richiesta di convalida dell’acquisto:
{
"password": "f4d35830e3...52aae",
"receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"exclude-old-transactions": false
}
Se stai lavorando in un ambiente Sandbox (cioè stai testando gli acquisti), invia le richieste di convalida a https://sandbox.itunes.apple.com/verifyReceipt. Il segreto condiviso, il payload e i formati di risposta rimangono invariati.
È importante notare che non sarà possibile convalidare una ricevuta creata in ambiente Sandbox su un server di produzione e viceversa. Per questo motivo, nei sistemi reali, la prassi migliore è quella di indirizzare la prima richiesta al server di produzione e reindirizzarla al server Sandbox nel caso in cui la chiave di stato restituisca il codice di errore 21007. Durante la revisione dell’app, questo comportamento è indispensabile, poiché consente ai dipendenti Apple di testare gli acquisti e agli utenti reali dell’app di effettuarli.
Tra gli altri errori da tenere a mente, c’è il codice d’errore 21004, che significa che stiamo usando il segreto sbagliato. È importante monitorarlo, poiché ha un impatto sia sull’esperienza dell’utente sia sull’accuratezza delle analisi. Nel peggiore dei casi, se all’utente non viene mai concesso l’accesso alle funzioni premium dopo averle pagate, l’app può essere rimossa dall’App Store.
Se la convalida è andata a buon fine (status=0), la risposta conterrà i dettagli delle transazioni dell’utente.
Ecco la risposta alla richiesta di convalida del pagamento:
{
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 123,
"app_item_id": 123,
"bundle_id": "com.adapty.sample_app",
"application_version": "1",
"download_id": 123,
"version_external_identifier": 123,
"receipt_creation_date": "2021-04-28 19:42:01 Etc/GMT",
"receipt_creation_date_ms": "1619638921000",
"receipt_creation_date_pst": "2021-04-28 12:42:01 America/Los_Angeles",
"request_date": "2021-08-09 18:26:02 Etc/GMT",
"request_date_ms": "1628533562696",
"request_date_pst": "2021-08-09 11:26:02 America/Los_Angeles",
"original_purchase_date": "2017-04-09 21:18:41 Etc/GMT",
"original_purchase_date_ms": "1491772721000",
"original_purchase_date_pst": "2017-04-09 14:18:41 America/Los_Angeles",
"original_application_version": "1",
"in_app": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
],
"status": 0
}
La risposta è piuttosto macchinosa ed è stata semplificata nella nuova versione dell’API del server dell’App Store, ma l’attuale implementazione (implementation) non è così difficile da ottenere.
Recupero dello stato dell’abbonamento e della cronologia delle transazioni
Per sapere se l’utente abbia accesso alle funzioni premium dell’app, è necessario un modo per determinare lo stato del suo abbonamento. Nella versione attuale dell’API non esiste una richiesta dedicata per recuperare lo stato dell’abbonamento, quindi è necessario lavorare comunque con la cronologia delle transazioni.
L’array latest_receipt_info per impostazione predefinita, contiene tutte le transazioni di acquisto in-app di un determinato utente, ad eccezione dei prodotti consumabili, che vengono completati sul lato dell’app. In questo modo è possibile recuperare l’intera cronologia degli acquisti dell’utente. Questo è molto utile sia per l’analisi che per determinare lo stato attuale dell’abbonamento.
Sembra che le transazioni arrivino sempre già ordinate dalle più recenti. Per essere sicuri, tuttavia, si consiglia di implementare l’ordinamento per data di transazione.
Il payload della transazione:
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
Per verificare lo stato attuale dell’abbonamento, è sufficiente recuperare l’ultima transazione della catena e guardare la proprietà expires_date. L’eccezione è rappresentata dal periodo di tolleranza (grace period), di cui parleremo più avanti.
Ai fini analitici, si consiglia di salvare le seguenti proprietà:
- product_id: l’identificativo testuale del prodotto acquistato.
- transaction_id: l’identificativo numerico univoco della transazione. Ogni acquisto o rinnovo avrà un proprio identificativo che potrà essere utilizzato per capire se la transazione sia stata elaborata in precedenza.
- original_transaction_id: l’identificativo numerico univoco della catena di transazioni. All’attivazione dell’abbonamento o del periodo di prova, corrisponderà a transaction_id. Tuttavia, in caso di ulteriori rinnovi dell’abbonamento, transaction_id cambierà, mentre original_transaction_id rimarrà la stessa. Questo è utile per sapere quante volte è stato rinnovato l’abbonamento.
- purchase_date и original_purchase_date: la data della transazione e la data della transazione originale. Funziona con la stessa logica della proprietà precedente.
- expires_date: la data di scadenza dell’abbonamento.
- cancellation_date: la data del rimborso (refund), non dell’annullamento dell’abbonamento, come potrebbe far pensare il nome. Se la risposta contiene questo campo, significa che puoi togliere l’accesso all’utente all’abbonamento e contabilizzare l’annullamento nei tuoi dati analitici: per questa transazione, non riceverai alcun pagamento.
- is_in_intro_offer_period: il flag che indica se l’offerta introduttiva (intro offer) sia stata utilizzata all’attivazione dell’abbonamento.
- is_trial_period: il flag che indica l’esistenza di un periodo di prova (trial period) al momento dell’attivazione dell’abbonamento.
- offer_code_ref_name: il codice dell’offerta utilizzato al momento dell’attivazione dell’abbonamento.
- promotional_offer_id: l’identificatore di testo dell’offerta promozionale (promo offer) che è stato utilizzato quando si è inserito il periodo di fatturazione corrente.
- in_app_ownership_type: il tipo di proprietà dell’abbonamento. Rivela se l’utente abbia acquistato il prodotto da solo o se lo abbia ricevuto come parte dell’abbonamento familiare. Tra i valori possibili, ci sono:
- PURCHASED: l’utente ha acquistato personalmente il prodotto.
- FAMILY_SHARED: l’utente ha ricevuto il prodotto come parte dell’abbonamento familiare.
Alla WWDC 2021, Apple ha annunciato che sta pianificando di aggiungere un campo appAccountToken per le info sulle transazioni. Il campo conterrà l’identificativo utente nel tuo sistema. Questo identificativo deve essere in formato UUID e viene definito lato app quando viene inizializzato l’acquisto. Se definito, verrà restituito in tutte le transazioni di questa catena (rinnovi di abbonamenti (subscriptions renewals), problemi di fatturazione (billing issues), ecc., il che significa che sarà facile per te capire quale utente abbia effettuato l’acquisto.
Sarà bene che tu tenga traccia anche del parametro subscription_group_identifier. Se l’utente aveva in precedenza transazioni con prove attive o offerte introduttive, non dovrebbe avere accesso a queste per lo stesso gruppo di abbonamenti. Questo dovrebbe essere tracciato lato server.
Informazioni sul rinnovo dell’abbonamento, periodo di tolleranza e problemi di fatturazione
L’array pending_renewal_info memorizza i dati di rinnovo dell’abbonamento. Permette di capire cosa succederà all’abbonamento nel prossimo periodo di fatturazione. Ad esempio, se si scopre che l’utente ha rinunciato al rinnovo automatico, si può suggerire di passare a un piano diverso o di presentare un’offerta promozionale. Questi eventi (events) possono essere facilmente monitorati con le notifiche del server (server notifications), di cui parleremo tra poco.
Dati di rinnovo dell’abbonamento:
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
- product_id: l’identificativo testuale del prodotto acquistato.
- auto_renew_product_id: l’identificativo testuale del prodotto che verrà attivato nel prossimo periodo di fatturazione. Se è diverso da quello attuale (product_id), significa che l’utente ha cambiato tipo di abbonamento.
- auto_renew_status: il flag che indica se l’abbonamento deve essere mantenuto nel periodo di fatturazione successivo.
- expiration_intent: il motivo della scadenza dell’abbonamento. Tra i valori possibili, ci sono:
- 1—l’utente ha annullato il rinnovo,
- 2 —l’abbonamento è stato annullato per problemi di fatturazione,
- 3 —l’utente non ha accettato un aumento di prezzo,
- 4 —il prodotto in abbonamento non era disponibile per il rinnovo, ad esempio se era stato rimosso da App Store Connect,
- 5 — un motivo sconosciuto.
- grace_period_expires_date: la data di scadenza del periodo di tolleranza, se previsto per la tua app. In questo caso, l’utente dovrebbe avere accesso alle funzioni premium fino alla data specificata qui, non a quella della transazione stessa. Se disponi di questa chiave, puoi comunicare all’utente che deve aggiornare le informazioni della carta o ricaricare il saldo.
- is_in_billing_retry_period: il flag che indica se l’abbonamento abbia lo stato di nuovo tentativo di fatturazione (billing retry state). Significa che l’abbonamento non è stato annullato, ma che Apple non è riuscita ad addebitare il pagamento del rinnovo e continuerà a cercare di farlo per 60 giorni.
- offer_code_ref_name: il codice dell’offerta che verrà utilizzato nel prossimo periodo di fatturazione.
- promotional_offer_id: l’identificativo di testo dell’offerta promozionale che verrà utilizzata nel prossimo periodo di fatturazione.
- price_consent_status: il flag che indica se l’utente abbia accettato l’imminente aumento del prezzo dell’abbonamento. Se il valore è 0, è necessario offrire all’utente un prodotto diverso o un’offerta promozionale per evitare che annulli l’abbonamento.
Abbonamenti consumabili, non consumabili e non rinnovabili
Se l’utente non ha prodotti auto-rinnovabili (auto-renewable products), non verranno restituite le chiavi latest_receipt_info e pending_renewal_info. In questo caso, le transazioni si trovano in receipt → in_app. Il formato della transazione è simile a quello delle transazioni con rinnovo automatico, ma non presenta campi per la scadenza, i rinnovi, le offerte e altre proprietà esclusive delle transazioni con rinnovo automatico.
È opportuno notare che receipt → in_app arriverà anche per le transazioni auto-rinnovabili, ma la pratica migliore è quella di usare latest_receipt_info, poiché i dati relativi all’abbonamento in esso contenuti saranno i più aggiornati.
Notifiche delle transazioni del server
Fino a qualche tempo fa si doveva escogitare un sistema complesso, per tenere traccia dei cambiamenti dello stato degli abbonamenti. Ad esempio, per capire se l’abbonamento fosse stato rinnovato o meno, era necessario inviare una richiesta di verifica dello stato dell’abbonamento ai server Apple ogni ora a partire da 24 ore prima della sua scadenza. Apple ha aggiunto sempre più notifiche server, nel corso del tempo, che ora coprono praticamente tutti gli eventi importanti relativi agli abbonamenti. Questo sarà molto utile: quando ci saranno modifiche da parte di Apple, riceverai una notifica sul tuo server. In altre parole, riceverai una notifica sui nuovi acquisti, sui rinnovi degli abbonamenti, sui problemi di fatturazione, ecc. Ciò consente di raccogliere analisi molto più accurate e di gestire lo stato dell’abbonamento in modo molto più semplice.
Le notifiche del server possono essere abilitate in App Store Connect. Apri la pagina dell’applicazione e vai su General -> App information. Quindi, inserisci il link nel campo URL per le notifiche del server App Store e salva le modifiche.
Notifica del server:
{
"notification_type": "DID_RENEW",
"password": "f4d35830e3...52aae",
"environment": "PROD",
"auto_renew_product_id": "basic_subscription_1_month",
"auto_renew_status": "true",
"unified_receipt": {
"status": 0,
"environment": "Production",
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_status": "1",
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853"
}
]
},
"bid": "com.adapty.sample_app",
"bvrs": "0"
}
Il formato della notifica del server è simile alla risposta di convalida del pagamento. I dettagli della transazione sono memorizzati in unified_receipt → latest_receipt_info. La chiave password memorizza il segreto condiviso della tua app, consentendo di verificare l’autenticità della richiesta. La chiave notification_type contiene il tipo di evento. A mio parere, i più utili sono:
- DID_CHANGE_RENEWAL_STATUS: significa che l’utente ha disattivato o attivato il rinnovo automatico dell’abbonamento (quest’ultimo è molto più raro). Se l’utente ha scelto di non rinnovare l’abbonamento automaticamente, è possibile incoraggiarlo a rientrare tra gli abbonati attivi.
- DID_FAIL_TO_RENEW: non è stato possibile rinnovare l’abbonamento a causa di un problema di pagamento. Dovrai avvisare l’utente, in modo che l’abbonamento non venga annullato automaticamente.
- DID_RENEW: il rinnovo dell’abbonamento è andato a buon fine.
- REFUND: l’abbonamento è stato rimborsato. Dovresti limitare l’accesso dell’utente alle funzionalità premium che l’acquisto doveva garantire e contabilizzare il rimborso (cioè la perdita di questo denaro) nei tuoi dati analitici.
Conclusione
La convalida del server può potenziare le analisi dei dati raccolti dalla tua app. Inoltre, rende più difficile ai truffatori l’accesso ai tuoi contenuti premium e ti permette di rendere i tuoi abbonamenti multipiattaforma. Allo stesso tempo, l’implementazione della convalida del server può richiedere molto tempo, soprattutto se si ha bisogno di dati molto precisi. Ciò richiederebbe di prendere in considerazione molti casi secondari: upgrade o crossgrade dell’abbonamento, periodi di prova, offerte promozionali/introduttive, periodi di tolleranza, rimborsi, abbonamenti familiari, ecc. È inoltre necessario conoscere e tenere conto di varie sfumature, ad esempio, la politica di Apple di abbassare la commissione dal 30% al 15% per gli abbonamenti che vengono rinnovati regolarmente per oltre un anno.