Zakupy w aplikacji na iOS, część 4: walidacja zakupów po stronie serwera
Updated: 20 marca, 2023
Co to jest walidacja zakupu na serwerze?
Walidacja serwerowa (walidacja zakupów po stronie serwera – server-side receipt validation) jest sposobem weryfikacji autentyczności zakupu. W przeciwieństwie do walidacji opartej na urządzeniach, walidacja serwerowa odbywa się po stronie serwera (niespodzianka, nieprawdaż?). Walidacja oznacza, że urządzenie lub serwer wysyła żądanie do serwerów Apple, aby dowiedzieć się, czy zakup rzeczywiście nastąpił i czy był poprawny.
Dlaczego warto dokonywać walidacji zakupów?
Należy zauważyć, że walidacja serwerowa nie jest obowiązkowa i zakupy w aplikacji będą działać także bez niej. Zapewnia ona jednak pewne korzyści:
- Zaawansowana analityka płatności, która jest szczególnie ważna w przypadku subskrypcji, ponieważ wszystko, co dzieje się po aktywacji, nie jest przetwarzane przez urządzenie. Bez przetwarzania zakupu po stronie serwera nie będzie można odzyskać bieżącego statusu subskrypcji i dowiedzieć się, czy użytkownik odnowił lub anulował subskrypcję, czy występują problemy z opłatami (billing issues), i tak dalej.
- Możliwość weryfikacji autentyczności zakupu. Będziesz mieć pewność, że transakcja nie jest oszustwem, a użytkownik faktycznie zapłacił za twój produkt.
- Subskrypcje między platformami. Jeśli możesz sprawdzić status subskrypcji użytkownika w czasie rzeczywistym, możesz zsynchronizować ją z innymi platformami. Na przykład użytkownik, który kupił subskrypcję z urządzenia z systemem iOS, będzie mógł z niej korzystać na Androidzie, na stronie internetowej i na innych platformach.
- Możliwość kontrolowania dostępu do treści od strony serwera, który chroni przed użytkownikami próbującymi uzyskać dostęp do danych bez subskrypcji, po prostu wykonując żądania do serwera.
Mówiąc z naszego doświadczenia, pierwsza zaleta wystarczy, aby skonfigurować przetwarzanie zakupów po stronie serwera.
Walidacja zakupu
Ogólnie rzecz biorąc, proces walidacji potwierdzeń zakupu w systemie iOS wygląda następująco:
Generowanie shared secret
Aby wysłać żądanie weryfikacji płatności, musisz dołączyć tak zwany „shared secret”, aby autoryzować żądanie. Możesz go wygenerować w App Store Connect.
Shared secret można utworzyć dla określonej aplikacji (tzw. app-specific secret) lub dla wszystkich aplikacji na koncie (primary secret).
Aby wygenerować app-specific secret, otwórz stronę aplikacji w App Store Connect, przejdź do In-App Purchases → Manage i kliknij „App-Specific Shared Secret”. W oknie, które się otworzy, można będzie wygenerować nowy token lub skopiować istniejący.
Aby otrzymać secret dla wszystkich aplikacji na koncie, otwórz stronę Users and Access page i wybierz zakładkę Shared Secret.
Żądanie walidacji płatności
Po otrzymaniu shared secret możesz wysyłać potwierdzenia dla uzyskania walidacji na serwerach Apple. Odbywa się to poprzez żądanie verifyReceipt. Musisz wysłać żądanie POST na adres https://buy.itunes.apple.com/verifyReceipt. W treści żądania JSON przekaż shared secret w polu password i potwierdzenie w polu receipt-data. Istnieje również opcjonalny parametr exclude-old-transactions. Jeśli posiada on wartość true, wtedy dla każdej subskrypcji z automatycznym odnawianiem otrzymasz tylko ostatnią transakcję zamiast pełnej historii odnowienia.
Oto jak wygląda żądanie o walidację zakupu:
{
"password": "f4d35830e3...52aae",
"receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"exclude-old-transactions": false
}
Jeśli pracujesz w środowisku sandbox (czyli testujesz zakupy), wyślij prośby o walidację do https://sandbox.itunes.apple.com/verifyReceipt. Shared secret, jak również żądanie i formaty odpowiedzi pozostają takie same.
Należy pamiętać, że nie będzie można zweryfikować potwierdzenia utworzonego w środowisku sandbox na serwerze produkcyjnym (Production server) i odwrotnie. Dlatego w systemach rzeczywistych najlepszą praktyką jest skierowanie pierwszego żądania do serwera produkcyjnego i przekierowanie go do serwera sandbox na wypadek, gdyby klucz stanu zwrócił kod błędu 21007. Takie zachowanie jest konieczne podczas przeglądania aplikacji, ponieważ pozwala pracownikom Apple testować zakupy, a jednocześnie umożliwia ich dokonywanie rzeczywistym użytkownikom aplikacji.
Między innymi błędami warto zauważyć, że istnieje kod błędu 21004 , który oznacza używanie niewłaściwego secret. Ważne jest, aby to śledzić, ponieważ ma to wpływ zarówno na doświadczenie użytkownika, jak i dokładność analizy. W najgorszym przypadku aplikacja może zostać usunięta ze sklepu App Store, jeśli użytkownik nie uzyska dostępu do funkcji premium po zapłaceniu za nie.
Jeśli walidacja przebiegła pomyślnie (status= 0), odpowiedź będzie zawierać szczegóły transakcji użytkownika.
Oto odpowiedź na żądanie weryfikacji płatności:
{
"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
}
Odpowiedź jest dość uciążliwa i została uproszczona w nowej wersji API App Store Server, ale obecna implementacja nie jest tak trudna do opanowania.
Pobieranie statusu subskrypcji i historii transakcji
Aby dowiedzieć się, czy użytkownik ma dostęp do funkcji premium w aplikacji, musisz określić status jego subskrypcji. W bieżącej wersji interfejsu API nie ma dedykowanego żądania pobierania statusu subskrypcji, więc w każdym przypadku musisz pracować z historią transakcji.
Tablica latest_receipt_info domyślnie zawiera wszystkie transakcje zakupu w aplikacji danego użytkownika, z wyjątkiem produktów eksploatacyjnych, które są zakończone po stronie aplikacji. W ten sposób możesz odzyskać całą historię zakupów użytkownika. Jest to bardzo przydatne zarówno dla analityki, jak i określania aktualnego statusu subskrypcji.
Wydaje się, że transakcje zawsze są już sortowane od najnowszych. Dla pewności jednak nadal polecam implementację własnego sortowania według daty transakcji.
Żądanie transakcji:
{
"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"
}
Aby sprawdzić aktualny status subskrypcji, wystarczy pobrać najnowszą transakcję łańcucha i spojrzeć na wartość expires_date. Wyjątkiem od tego byłby okres karencji, który omówimy nieco później.
Dla celów analitycznych zalecam zapisanie następujących właściwości:
- product_id: identyfikator tekstowy zakupionego produktu.
- transaction_id: niepowtarzalny identyfikator numeryczny transakcji. Każdy zakup lub odnowienie będzie miał swój własny identyfikator, który można wykorzystać do ustalenia, czy transakcja została wcześniej przetworzona.
- original_transaction_id: niepowtarzalny identyfikator numeryczny łańcucha transakcji. Będzie pasować do transaction_id po aktywacji subskrypcji lub wersji próbnej. Natomiast po kolejnych odnowieniach subskrypcji transaction_id zmieni się, zaś original_transaction_id pozostanie bez zmian. Jest to przydatne do śledzenia, ile razy subskrypcja została odnowiona.
- purchase_date i original_purchase_date: data transakcji i oryginalna data transakcji. Działa to zgodnie z tą samą logiką co poprzednia wartość.
- expires_date: data wygaśnięcia subskrypcji.
- cancellation_date: data zwrotu środków, nie anulowania subskrypcji, jak sama nazwa może sugerować. Jeśli odpowiedź posiada to pole, oznacza to, że możesz zablokować użytkownikowi dostęp do jego subskrypcji, a także konto do anulowania w swojej analityce — nie otrzymasz żadnej płatności z tej transakcji.
- is_in_intro_offer_period: oznaczenie pokazujące, czy oferta początkowa została użyta po aktywacji subskrypcji.
- is_trial_period: oznaczenie pokazujące, czy po aktywacji subskrypcji był okres próbny.
- offer_code_ref_name: kod oferty, który został użyty podczas aktywacji subskrypcji.
- promotional_offer_id: identyfikator tekstowy oferty promocyjnej, który został użyty przy wprowadzaniu bieżącego okresu rozliczeniowego.
- in_app_ownership_type: typ własności subskrypcji. Ujawnia, czy użytkownik zakupił produkt samodzielnie, czy otrzymał go w ramach subskrypcji rodzinnej. Niektóre możliwe wartości obejmują:
- PURCHASED: użytkownik zakupił produkt samodzielnie.
- FAMILY_SHARED: użytkownik otrzymał produkt w ramach subskrypcji rodzinnej.
Apple ogłosił na WWDC 2021, że planuje dodać pole appAccountToken do informacji o transakcji. Będzie on zawierał identyfikator użytkownika w twoim systemie. Identyfikator ten musi być w formacie UUID i jest zdefiniowany po stronie aplikacji, gdy zakup zostanie zainicjowany. Po zdefiniowaniu zostanie zwrócony we wszystkich transakcjach tego łańcucha (odnowienie subskrypcji, problemy z rozliczeniami itp.), co oznacza, że łatwo będzie ci zrozumieć, który użytkownik dokonał zakupu.
Należy również śledzić parametr subscription_group_identifier. Jeśli użytkownik miał wcześniej jakiekolwiek transakcje z aktywnymi próbami lub ofertami początkowymi, nie powinien mieć do nich dostępu dla tej samej grupy subskrypcji. To powinno być śledzone po stronie serwera.
Informacje o odnowieniu subskrypcji, okres karencji (grace period) i problemy z rozliczeniami
Tablica pending_renewal_info przechowuje dane odnowienia subskrypcji. Pozwala to zrozumieć, co stanie się z subskrypcją w następnym okresie rozliczeniowym. Na przykład, jeśli odkryłeś, że użytkownik zrezygnował z automatycznego odnawiania, możesz zasugerować mu przejście na inny plan lub przedstawić mu ofertę promocyjną. Zdarzenia te (events) można łatwo śledzić za pomocą powiadomień serwera (server notifications), które omówię wkrótce.
Dane odnowienia subskrypcji:
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
- product_id: identyfikator tekstowy zakupionego produktu.
- auto_renew_product_id: identyfikator tekstowy produktu, który zostanie aktywowany w następnym okresie rozliczeniowym. Jeśli różni się od bieżącego (product_id), oznacza to, że użytkownik zmienił typ subskrypcji.
- auto_renew_status: oznaczenie pokazujące, czy subskrypcja ma być kontynuowana w następnym okresie rozliczeniowym.
- expiration_intent: powód wygaśnięcia subskrypcji. Niektóre możliwe wartości obejmują:
- 1: użytkownik sam anulował subskrypcję,
- 2: subskrypcja została anulowana z powodu problemów z płatnościami,
- 3: użytkownik nie zgodził się na podwyżkę ceny,
- 4: produkt subskrypcji był niedostępny do odnowienia, np. jeśli został usunięty z App Store Connect.
- 5: przyczyna nie jest znana.
- grace_period_expires_date: data wygaśnięcia okresu karencji, jeśli istnieje dla twojej aplikacji. W takim przypadku użytkownik powinien mieć dostęp do funkcji premium do daty podanej tutaj, a nie tej w samej transakcji. Jeśli posiadasz ten klucz, możesz powiadomić użytkownika o konieczności aktualizacji danych karty lub o doładowaniu salda.
- is_in_billing_retry_period: oznaczenie pokazujące, czy subskrypcja ma status ponowienia płatności. Oznacza to, że subskrypcja nie została anulowana, ale Apple nie mogło pobrać opłaty za odnowienie i będzie próbować to zrobić przez 60 dni.
- offer_code_ref_name: kod oferty, który zostanie użyty w następnym okresie rozliczeniowym.
- promotional_offer_id: identyfikator tekstowy oferty promocyjnej, który będzie używany w następnym okresie rozliczeniowym.
- price_consent_status: oznaczenie pokazujące, czy użytkownik wyraził zgodę na zbliżający się wzrost cen subskrypcji. Jeśli ma wartość 0, powinieneś zaoferować użytkownikowi inny produkt lub ofertę promocyjną, aby nie anulował subskrypcji.
Subskrypcje związane oraz niezwiązane z jednorazowym użyciem nabytego dobra, a także subskrypcje nieodnawialne (non-renewable)
Jeśli użytkownik nie posiada produktów autoodnawialnych (auto-renewable), klucze latest_receipt_info oraz pending_renewal_info nie zostaną zwrócone. W tym przypadku transakcje można znaleźć w receipt → in_app. Format transakcji jest podobny do transakcji z automatycznym odnawianiem, ale nie zawiera pól dotyczących wygaśnięcia, odnowienia, ofert i innych właściwości wyłącznie dla transakcji z automatycznym odnawianiem.
Należy zauważyć, że receipt → in_app pojawią się również dla transakcji autoodnawialnych, ale lepszą praktyką jest stosowanie latest_receipt_info, ponieważ zawarte w nim dane subskrypcji będą najbardziej aktualne.
Serwerowe powiadomienia o transakcjach
Pewien czas temu konieczne było opracowanie złożonego systemu śledzenia zmian statusu subskrypcji. Na przykład, aby dowiedzieć się, czy subskrypcja została odnowiona, konieczne było wysyłanie żądania statusu subskrypcji do serwerów Apple co godzinę, począwszy od 24 godzin przed wygaśnięciem subskrypcji. Apple z czasem dodawało coraz więcej powiadomień serwerowych, a obecnie obejmują one praktycznie wszystkie ważne wydarzenia, które mają związek z subskrypcjami. Jest to bardzo przydatne: po wprowadzeniu jakichkolwiek zmian po stronie Apple otrzymasz powiadomienie o tym na własnym serwerze. Oznacza to, że zostaniesz powiadomiony o nowych zakupach, odnowieniach subskrypcji, problemach z płatnościami, itp. Pozwala to na zbieranie o wiele dokładniejszych danych analitycznych, a także znacznie ułatwia zarządzanie statusem subskrypcji.
Powiadomienia serwerowe można włączyć w App Store Connect. Otwórz stronę aplikacji i przejdź do General -> App information. Następnie umieść link w polu URL for App Store Server Notifications i zapisz zmiany.
Powiadomienie serwera:
{
"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"
}
Format powiadomienie serwerowego jest podobny do odpowiedzi walidacji płatności. Szczegóły transakcji są przechowywane w unified_receipt → latest_receipt_info. Klucz hasła przechowuje shared secret dla Twojej aplikacji, co pozwala zweryfikować autentyczność żądania. Klucz notification_type przechowuje typ zdarzenia. Moim zdaniem najbardziej przydatne z nich to:
- DID_CHANGE_RENEWAL_STATUS: oznacza to, że użytkownik wyłączył lub włączył automatyczne odnawianie subskrypcji (to ostatnie jest znacznie rzadsze). Jeśli użytkownik zrezygnował z automatycznego odnawiania, możesz zachęcić go do ponownego dołączenia do aktywnych subskrybentów.
- DID_FAIL_TO_RENEW: subskrypcji nie można odnowić z powodu problemów z płatnościami. Powinieneś powiadomić o tym użytkownika, aby subskrypcja nie została automatycznie anulowana.
- DID_RENEW: subskrypcja została pomyślnie odnowiona.
- REFUND: subskrypcja została zwrócona. Należy ograniczyć dostęp użytkownika do funkcji premium, które ten zakup miał przyznać i rozliczyć zwrot (czyli utratę tych pieniędzy) w swoich danych analitycznych.
Wnioski
Walidacja serwerowa może poprawić twoją wewnętrzną analitykę o dane, które zbierasz ze swojej aplikacji. Utrudnia ono również oszustom dostęp do treści premium, a także umożliwia udostępnianie subskrypcji na różnych platformach. Jednocześnie wdrożenie walidacji serwera może zająć sporo czasu, zwłaszcza jeśli potrzebujesz bardzo dokładnych danych. Wymagałoby to uwzględnienia wielu przypadków dodatkowych: uaktualnienia subskrypcji, przejścia subskrypcji, okresów próbnych (trial periods), ofert promocyjnych/początkowych (promo/intro offers), okresów karencji, zwrotów, subskrypcji rodzinnych itp. Musisz również znać i uwzględniać różne niuanse. Przykładowo, Apple ma politykę obniżania prowizji z 30% do 15% za subskrypcje, które są regularnie odnawiane przez ponad rok.