
Tutorial
September 7, 2022
13 min read
September 7, 2022
37 min read
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.
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:
Mówiąc z naszego doświadczenia, pierwsza zaleta wystarczy, aby skonfigurować przetwarzanie zakupów po stronie serwera.
Ogólnie rzecz biorąc, proces walidacji potwierdzeń zakupu w systemie iOS wygląda następująco:
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.
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.
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:
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.
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"
}
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.
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:
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.
Further reading
Tutorial
September 7, 2022
13 min read
Tutorial
September 7, 2022
22 min read
Tutorial
September 7, 2022
12 min read