Всем привет, я Кирилл, СТО Adapty. Я делал систему серверной валидации для наших SDK. Сегодня как раз и расскажу про серверную верификацию покупок на iOS. Это третья статья из серии о внедрении внутренних покупок на iOS, советую познакомиться с остальными: iOS in-app purchases часть 1: конфигурация и добавление в проект. iOS in-app purchases часть 2: инициализация и обработка покупок. iOS in-app purchases часть 3: серверная валидация покупки. - Вы тут iOS in-app purchases часть 4: локальное тестирование покупок в XCode. iOS in-app purchases часть 5: обработка ошибок SKErrors. Серверная валидация (server-side receipt validation) — это способ проверить подлинность покупки. В отличие от проверки покупки на устройстве, серверная валидация происходит, внимание, на сервере. Валидация означает, что устройство или сервер обращаются к серверам Apple и спрашивают, действительно ли была покупка и валидная ли она. Стоит отметить, что серверная валидация не обязательна, встроенные покупки и подписки будут работать и без неё. Но она даёт ряд преимуществ: Продвинутая аналитика платежей, особенно актуальная для подписок, потому что все события после активации происходят без участия устройства. Если не сделать серверную обработку покупок, вы не будете знать статус подписки в конкретный момент времени: продлил ли пользователь подписку, отменил ее, есть ли проблема с платежом и тд. Проверка подлинности покупки, то есть вы будете уверены, что это не фродовая транзакция, а значит, пользователь заплатил деньги за ваш продукт. Кроссплатформенные подписки. Зная в живом режиме статус подписки пользователя, можно синхронизировать его с другими платформами. Например, пользователь, оформивший подписку на iOS, сможет пользоваться сервисом на Android, Web и других платформах. Возможность контролировать доступ к контенту со стороны сервера, что исключает ситуации, когда пользователь без подписки может получать доступ к данным просто отправляя запросы к серверу. По нашему опыту, один первый пункт — достаточная причина делать обработку покупок на сервере.  В целом процесс валидации платежей на iOS можно описать схемой: Чтобы отправить запрос на валидацию платежа, необходимо предоставить shared secret, с помощью которого авторизуется запрос. Получить его можно в App Store Connect. Shared secret может быть создан для конкретного приложения (app-specific) или же для всех приложений в аккаунте (primary). Чтобы получить secret для конкретного приложения, необходимо открыть страницу данного приложения в App Store Connect, перейти в раздел In-App Purchases → Manage и кликнуть на ссылку App-Specific Shared Secret. В открывшемся окне можно будет создать новый или скопировать существующий токен. Чтобы получить secret для всех приложений в аккаунте, необходимо открыть страницу Users and Access и перейти на вкладку Shared Secret. После получения shared secret вы можете отправлять квитанцию (receipt) для валидации на сервер Apple. Это делается с помощью запроса verifyReceipt. Нужно отправить POST-запрос на https://buy.itunes.apple.com/verifyReceipt. В JSON теле запроса нужно отправить shared secret в поле password и receipt в поле receipt-data. Также есть опциональный параметр exclude-old-transactions. Если его значение true, то для каждой авто-возобновляемой подписки вернётся только последняя транзакция, вместо всей истории продлений. Если вы работаете в Sandbox режиме, то есть тестируете покупки, запросы на валидацию надо отправлять на https://sandbox.itunes.apple.com/verifyReceipt. Shared secret, формат пейлоада и ответа остаются без изменений. Важно отметить, что receipt, созданный в Sandbox, нельзя будет провалидировать на Production сервере, и наоборот. Поэтому в реальных системах рекомендуется всегда первый запрос отправлять на Production сервер, и, если в ответ в ключе status пришла ошибка с кодом 21007, повторить запрос на Sandbox сервер. Такое поведение обязательно нужно включать на период ревью приложения, чтобы сотрудники Apple могли тестировать покупки, и в это же время их могут делать реальные пользователи вашего приложения. Среди других ошибок, на которые нужно обращать внимание, — 21004, которая говорит о том, что мы используем неправильный secret. Её важно мониторить, потому что она влияет и на пользовательский опыт, и на точность аналитики. В худшем случае, приложение могут удалить из App Store, если пользователю после покупки не даётся доступ к платным возможностям. В случае успешной валидации (status=0), в ответ приходит информация о транзакциях пользователя.  Ответ громоздкий, и в новой версии App Store Server API его упростили, но и с текущей реализацией нетрудно разобраться. Чтобы понимать, есть ли у пользователя доступ к платным функциям приложения, вам нужно уметь определять статус подписки. В данной версии API нет отдельного запроса на получение статуса подписки, поэтому вам в любом случае будет нужно работать с историей транзакций. В latest_receipt_info по умолчанию хранятся все транзакции пользователя, за исключением consumable продуктов, которые завершены на стороне мобильного приложения. Таким образом, вы можете восстановить всю историю покупок пользователя. Это полезно и для аналитики, и для вычисления текущего статуса подписки. Кажется, что сейчас транзакции всегда приходят отсортированными от самой старой к самой новой, но для надёжности я рекомендую сделать собственную сортировку по дате транзакций. Для получения текущего статуса подписки достаточно взять хронологически последнюю транзакцию в цепочке и посмотреть на дату истечения подписки (expires_date). Исключением будет grace period, о нём мы поговорим чуть позже. Для аналитических целей я рекомендую сохранять следующие поля: product_id — текстовый идентификатор продукта, который был куплен. transaction_id — числовой уникальный идентификатор транзакции. У каждой покупки/продления будет свой идентификатор, его можно использовать, чтобы понимать, была ли ранее обработана данная транзакция. original_transaction_id — числовой уникальный идентификатор цепочки транзакций. При активации подписки/триала он будет совпадать с transaction_id, но при последующих продлениях transaction_id будет меняться, а original_transaction_id будет одинаковым. Это удобно для того, чтобы отслеживать количество продлений. purchase_date и original_purchase_date — дата транзакции и дата оригинальной транзакции по аналогии с предыдущем пунктом. expires_date — дата истечения подписки cancellation_date — дата рефанда, а не отмены подписки, как может показаться из названия. Если в ответе есть это поле, значит, вы можете отозвать доступ пользователя к приложению, а также учесть в аналитике, что деньги за эту транзакцию не будут выплачены. is_in_intro_offer_period — флаг, показывающий, был ли использован интро оффер при активации подписки. is_trial_period — флаг, показывающий, был ли использован триал при активации подписки. offer_code_ref_name — оффер код, который был использован при активации подписки. promotional_offer_id — текстовый идентификатор промо оффера, который был использован при переходе на текущий период подписки. in_app_ownership_type — тип владения подпиской, отвечает на вопрос, купил ли пользователь продукт сам или получил его в рамках семейной подписки. Возможные значения: PURCHASED — пользователь сам купил продукт. FAMILY_SHARED — пользователь получил продукт в рамках семейной подписки. На WWDC 2021 Apple сообщили, что позже в транзакцию добавят поле appAccountToken, которое содержит идентификатор пользователя в вашей системе. Этот идентификатор должен быть в формате UUID и задаётся на стороне мобильного приложения в момент инициализации покупки. Если задан, то он будет возвращаться во всех транзакциях в этой цепочке (продления, проблемы с биллингом и тд.), а значит вы легко сможете понять, какой пользователь сделал покупку. Также стоит следить за параметром subscription_group_identifier. Если у пользователя ранее была транзакция с активным триалом или интро оффером, то в рамках данной группы подписок, пользователю больше недоступны триал и интро оффер, и это нужно отслеживать со стороны сервера. В pending_renewal_info лежит информация о продлении подписки. Она позволяет понять, что произойдёт с подпиской в следующем периоде. Например, если вы обнаружили, что пользователь отменил автопродление, вы можете предложить ему переход на другой план или промо оффер. Отслеживать такие события удобно с помощью серверных уведомлений, о которых я скоро расскажу. product_id — текстовый идентификатор продукта, который был куплен. auto_renew_product_id — текстовый идентификатор продукта, который активируется в следующем периоде. Если он отличается от текущего (product_id), значит пользователь сменил тип подписки. auto_renew_status — флаг, показывающий будет ли продлена подписка на следующий период. expiration_intent — причина истечения подписки. Возможные значения: 1 — пользователь сам отменил подписку. 2 — подписка отменилась из-за проблем с оплатой. 3 — пользователь не согласился на повышение цены 4 — продукт подписки не был доступен для продления, например, если его удалили из App Store Connect. 5 — неизвестная причина. grace_period_expires_date — дата истечения grace period, если он включен для вашего приложения. В этом случае пользователю должны быть доступны платные функции приложения до даты, указанной здесь, а не до той, которая указана в самой транзакции. Если есть этот ключ, вы можете сообщить пользователю о том, что ему нужно обновить данные карты для оплаты или пополнить баланс. is_in_billing_retry_period — флаг, показывающий находится ли подписка в статусе billing retry. Это значит, что подписка не была отменена, но при этом Apple не смог списать деньги за продление и будет пытаться делать это в течение 60 дней. offer_code_ref_name — оффер код, который будет использован в следующем периоде подписки. promotional_offer_id — текстовый идентификатор промо оффера, который будет использован в следующем периоде подписки. price_consent_status — флаг, показывающий, согласился ли пользователь с грядущим повышением цены на подписку. Если его значение 0, то вам стоит предложить пользователю другой продукт или промо оффер, чтобы он не прекратил подписку. Если у пользователя нет авто-возобновляемых продуктов, то ключи latest_receipt_info и pending_renewal_info не возвращаются. В таком случае, транзакции стоит искать в receipt → in_app. Формат транзакций похож на авто-возобновляемые транзакции, но в нём нет полей истечения, продления, офферов и других эксклюзивных для авто-возобновляемых транзакций параметров. Стоит отметить, что receipt → in_app приходит и для авто-возобновляемых транзакций, но лучше использовать latest_receipt_info, потому что в нём будет наиболее актуальная информация по подписке. Некоторое время назад требовалось написать сложную систему, чтобы отслеживать изменения в состоянии подписок. Например, чтобы понять, продлилась подписка или нет, нужно было за сутки до её истечения раз в час отправлять запрос на сервер Apple, чтобы узнать её статус. Apple постепенно добавлял серверные уведомления, и в настоящее время практически все важные события, связанные с подписками, покрыты серверными уведомлениями. Это очень удобно: как только со стороны Apple произошло какое-то изменение, вы можете получить информацию об этом на своём сервере. То есть вы будете получать информацию о новых покупках, продлениях, проблемах с платежами и так далее. Это позволяет собирать более точную аналитику, а также упрощает менеджмент состояния подписчика. Настроить получение серверных уведомлений можно в App Store Connect. Необходимо открыть страницу приложения, перейти в раздел General → App Information, указать желаемую ссылку в поле URL for App Store Server Notifications и сохранить изменения. Формат серверных уведомлений похож на ответ валидации платежа. Информация о транзакциях лежит в unified_receipt → latest_receipt_info. В ключе password лежит shared secret для вашего приложения, таким образом вы можете убедиться в подлинности запроса. В ключе notification_type указан тип события. На мой взгляд, самые полезные: DID_CHANGE_RENEWAL_STATUS — пользователь выключил или (намного реже) включил автопродление подписки. Если автопродление выключили, надо пытаться вернуть пользователя в число активных подписчиков. DID_FAIL_TO_RENEW — подписку не получилось продлить из-за проблем с оплатой. Стоит сообщить пользователю об этом, чтобы у него не отменилась подписка автоматически. DID_RENEW — подписка успешно продлилась. REFUND — пользователю вернули деньги за покупку. Нужно закрыть ему доступ к функциям, которые давала эта покупка и отразить рефанд (потерю денег) в аналитике. Серверная валидация значительно улучшает качество аналитики, которую вы можете собирать со своего приложения. Она также усложняет несанкционированный доступ к платному контенту и позволяет делать мультиплатформенные подписки. При этом реализация валидации может занять довольно много времени, особенно если нужна высокая точность данных, а значит надо учитывать много сайд-кейсов: upgrade подписки, cross grade подписки, trial период, promo/introductory offers, grace period, возвраты, семейные подписки и тд. Плюс нужно знать и учитывать различные нюансы, например, что для подписок, которые продлеваются больше года, Apple снижает комиссию с 30% до 15%. Если вы хотите получить работающую серверную валидацию и продвинутую аналитику с самого начала жизни вашего приложения и не хотите разрабатывать всё это самостоятельно, вам стоит попробовать Adapty. Мы делаем этот сервис не только для технической реализации подписок: Встроенная аналитика позволяет быстро понять основные метрики приложения. Когортный анализ отвечает на вопрос, как быстро сходится экономика. А/Б тесты увеличивают выручку приложения. Интеграции с внешними системами позволяют отправлять транзакции в сервисы атрибуции и продуктовой аналитики. Промо кампании уменьшают отток аудитории. Open source SDK позволяет интегрировать подписки в приложение за несколько часов. Серверная валидация и API для работы с другими платформами. Познакомьтесь подробнее с этими возможностями, чтобы быстрее внедрить подписки в своё приложение и улучшить конверсии.