Реалізація серверної верифікації покупок (Receipt Validation)
Клієнт прислав звіт про помилку: користувач купив Premium, отримав токен транзакції, потім відновив додаток з резервної копії на іншому пристрої — і Premium знову активний без повторної оплати. Класика. Причина — валідація тільки на клієнті: додаток перевіряє локальний receipt або trust-флаг від StoreKit, не звіряючись з сервером.
Чому клієнтська валідація — це не валідація
На iOS StoreKit 2 повертає Transaction з підписом Apple. Можна верифікувати підпис локально через Transaction.verificationResult, але це не захищає від replay-атак: зловмисник перехоплює валідний receipt одного користувача і підставляє його в інший акаунт. На Android ситуація аналогічна — BillingClient.queryPurchasesAsync() повертає Purchase об'єкти, які клієнт не повинен трактувати як підтвердження без серверної перевірки purchaseToken.
Найчастіша схема шахрайства — «receipt sharing»: один receipt розповсюджується між користувачами через форуми. Без серверної бази, яка фіксує який originalTransactionId (iOS) або orderId (Android) уже використаний, це не поймати.
Як улаштована нормальна серверна верифікація
Сторона iOS (App Store Server API). Старий підхід — POST на https://buy.itunes.apple.com/verifyReceipt з base64-encoded receipt-data — застарів. Apple просуває App Store Server API v1: клієнт передає серверу transactionId з Transaction.id (StoreKit 2), сервер робить GET /inApps/v1/history/{transactionId} з JWT-токеном (підписаним ES256 ключем з App Store Connect). Відповідь — JWSTransaction, який потрібно декодувати й верифікувати підпис через Apple Root CA.
Паралельно потрібно підписатися на App Store Server Notifications V2: Apple пушить події (DID_RENEW, EXPIRED, REFUND, GRACE_PERIOD_EXPIRED) на ваш endpoint. Без цього статус підписки на сервері застарівує — користувач відмінив підписку, а у вас він ще числиться Premium.
Сторона Android (Google Play Developer API). Для розповсюджувальних покупок — purchases.products.get з packageName, productId, purchaseToken. Для підписок — purchases.subscriptions.v2.get. Авторизація через Service Account з роллю Financial data viewer — це мінімально необхідні права, не давайте Editor на весь проект. Відповідь містить purchaseState (0 = Purchased, 1 = Canceled, 2 = Pending) і acknowledgementState — якщо 0, потрібно викликати purchases.products.acknowledge, інакше Google повертає гроші автоматично через 3 дні.
Ідемпотентність і захист від replay. У базі зберігаємо таблицю purchase_receipts з унікальним індексом по original_transaction_id + product_id. При кожному запиті на верифікацію спочатку перевіряємо наявність запису — якщо вже верифікували з іншим user_id, відповідаємо помилкою. Це й є захист від receipt sharing.
purchase_receipts
id uuid PK
user_id uuid FK
platform enum('ios','android')
original_transaction_id varchar UNIQUE (per product)
product_id varchar
purchase_state smallint
expires_at timestamptz -- для підписок
raw_payload jsonb -- оригінальна відповідь від Apple/Google
verified_at timestamptz
Стек та інтеграція
Серверна частина частіше за все Node.js (бібліотека app-store-server-api) або Python (google-auth + googleapiclient). Для Node зручний пакет node-apple-receipt-verify для legacy-endpoint, але краще одразу брати app-store-server-api від Apple — підтримує JWT-авторизацію й верифікацію JWS з коробки.
На стороні клієнта iOS — мінімум коду: отримати Transaction.id з Transaction.all або з updates потоку, надіслати на бекенд. Не передавайте весь appStoreReceiptURL — це legacy, і файл може бути невалідним на симуляторі.
На Android клієнт передає purchaseToken і productId з Purchase.purchaseToken. Важливо: токен може бути одним для кількох productId при апгрейді підписки — урахуйте це в логіці.
Процес роботи
Починаємо з аудиту поточної схеми валідації — де саме перевіряється receipt, є ли серверна база покупок, обробляються ли Server Notifications. Далі проектуємо схему БД і API-ендпоінти, реалізуємо верифікацію для кожної платформи, настроюємо webhook-обробник для Server Notifications, покриваємо тестами з моковими відповідями від Apple/Google. Окремий етап — нагрузкове тестування ендпоінта верифікації, тому що при пікових запусках (акція, фіча в топі App Store) він отримує все відразу.
Терміни — від 2 до 5 днів, залежить від наявності серверної інфраструктури та кількості типів покупок (розповсюджувальні, підписки, consumable, non-consumable). Якщо сервер уже є й потрібно тільки додати верифікацію — ближче до 2 днів. Повна архітектура з нуля плюс міграція існуючих користувачів — до 5.







