[Node.JS, Google API, Монетизация мобильных приложений] Разворачиваем сервер для проверки In-app purchase за 60 минут
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Всем привет! Сегодня расскажу вам как развернуть сервер для проверки In-app Purchase и In-app Subscription для iOS и Android (server-server validation).
На хабре есть статья от 2013 года про серверную проверку покупок. В статье говорится о том, что валидация в первую очередь необходима для предотвращения доступа к платному контенту при помощи джейлбрейка и другого софта. На мой взгляд в 2020 году эта проблема не так актуальна, и в первую очередь сервер с проверкой покупок необходима для синхронизации покупок в рамках одного аккаунта на нескольких устройствах
В проверке чеков покупок нет никакой технической сложности, по факту сервер просто «проксирует» запрос и сохраняет данные о покупке.
То есть задачу такого сервера можно разделить на 4 этапа:
- Получение запроса с чеком, отправленным приложением после покупки
- Запрос в Apple/Google на проверку чека
- Сохранение данных о транзакции
- Ответ приложению
В рамках статьи опустим 3 пункт, ибо он сугубо индивидуален.
Код в статье будет написан на Node.js, но по сути логика универсальна и не составит труда использовать ее написать валидацию на любом языке программирования.
Еще есть статья хорошая «То, что нужно знать о проверке чека App Store (App Store receipt)», ребята делают сервис для работы с подписками. В статье детально описано, что такое чек (receipt) и для чего нужна проверка покупок.
Сразу скажу, что в сниппетах кода используются вспомогательные классы и интерфейсы, весь код доступен в репозитории по ссылке https://github.com/denjoygroup/inapppurchase. В приведенном ниже фрагментах кода, я постарался дать названия используемым методам такие, чтобы приходилось делать отсылки к этим функциям.
iOS
Для проверки вам нужен Apple Shared Secret – это ключ, который вы должны получить в iTunnes Connect, он нужен для проверки чеков.
В первую очередь зададим параметры для создания запросов:
apple: any = {
password: process.env.APPLE_SHARED_SECRET, // ключ, укажите свой
host: 'buy.itunes.apple.com',
sandbox: 'sandbox.itunes.apple.com',
path: '/verifyReceipt',
apiHost: 'api.appstoreconnect.apple.com',
pathToCheckSales: '/v1/salesReports'
}
Теперь создадим функцию для отправки запроса. В зависимости от среды, с которой работаете, вы должны отправлять запрос либо на sandbox.itunes.apple.com для тестовых покупок, либо в прод buy.itunes.apple.com
/**
* receiptValue - чек, который проверяете
* sandBox - среда разработк
**/
async _verifyReceipt(receiptValue: string, sandBox: boolean) {
let options = {
host: sandBox ? this._constants.apple.sandbox : this._constants.apple.host,
path: this._constants.apple.path,
method: 'POST'
};
let body = {
'receipt-data': receiptValue,
'password': this._constants.apple.password
};
let result = null;
let stringResult = await this._handlerService.sendHttp(options, body, 'https');
result = JSON.parse(stringResult);
return result;
}
Если запрос прошел успешно, то в ответе от сервера Apple в поле status вы получите данные о вашей покупке.
У статуса возможны несколько значений, в зависимости от которых вы должны обработать покупку
21000 – Запрос был отправлен – не методом POST
21002 – Чек поврежден, не удалось его распарсить
21003 – Некорректный чек, покупка не подтверждена
21004 – Ваш Shared Secret некорректный или не соответствует чеку
21005 – Сервер эпла не смог обработать ваш запрос, стоит попробовать еще раз
21006 – Чек недействителен
21007 – Чек из SandBox (тестовой среды), но был отправлен в prod
21008 – Чек из прода, но был отправлен в тестовую среду
21009 – Сервер эпла не смог обработать ваш запрос, стоит попробовать еще раз
21010 – Аккаунт был удален
0 – Покупка валидна
Пример ответа от iTunnes Connect выглядит следующим образом
{
"environment":"Production",
"receipt":{
"receipt_type":"Production",
"adam_id":1527458047,
"app_item_id":1527458047,
"bundle_id":"BUNDLE_ID",
"application_version":"0",
"download_id":34089715299389,
"version_external_identifier":838212484,
"receipt_creation_date":"2020-11-03 20:47:54 Etc/GMT",
"receipt_creation_date_ms":"1604436474000",
"receipt_creation_date_pst":"2020-11-03 12:47:54 America/Los_Angeles",
"request_date":"2020-11-03 20:48:01 Etc/GMT",
"request_date_ms":"1604436481804",
"request_date_pst":"2020-11-03 12:48:01 America/Los_Angeles",
"original_purchase_date":"2020-10-26 19:24:19 Etc/GMT",
"original_purchase_date_ms":"1603740259000",
"original_purchase_date_pst":"2020-10-26 12:24:19 America/Los_Angeles",
"original_application_version":"0",
"in_app":[
{
"quantity":"1",
"product_id":"PRODUCT_ID",
"transaction_id":"140000855642848",
"original_transaction_id":"140000855642848",
"purchase_date":"2020-11-03 20:47:53 Etc/GMT",
"purchase_date_ms":"1604436473000",
"purchase_date_pst":"2020-11-03 12:47:53 America/Los_Angeles",
"original_purchase_date":"2020-11-03 20:47:54 Etc/GMT",
"original_purchase_date_ms":"1604436474000",
"original_purchase_date_pst":"2020-11-03 12:47:54 America/Los_Angeles",
"expires_date":"2020-12-03 20:47:53 Etc/GMT",
"expires_date_ms":"1607028473000",
"expires_date_pst":"2020-12-03 12:47:53 America/Los_Angeles",
"web_order_line_item_id":"140000337829668",
"is_trial_period":"false",
"is_in_intro_offer_period":"false"
}
]
},
"latest_receipt_info":[
{
"quantity":"1",
"product_id":"PRODUCT_ID",
"transaction_id":"140000855642848",
"original_transaction_id":"140000855642848",
"purchase_date":"2020-11-03 20:47:53 Etc/GMT",
"purchase_date_ms":"1604436473000",
"purchase_date_pst":"2020-11-03 12:47:53 America/Los_Angeles",
"original_purchase_date":"2020-11-03 20:47:54 Etc/GMT",
"original_purchase_date_ms":"1604436474000",
"original_purchase_date_pst":"2020-11-03 12:47:54 America/Los_Angeles",
"expires_date":"2020-12-03 20:47:53 Etc/GMT",
"expires_date_ms":"1607028473000",
"expires_date_pst":"2020-12-03 12:47:53 America/Los_Angeles",
"web_order_line_item_id":"140000447829668",
"is_trial_period":"false",
"is_in_intro_offer_period":"false",
"subscription_group_identifier":"20675121"
}
],
"latest_receipt":"RECEIPT",
"pending_renewal_info":[
{
"auto_renew_product_id":"PRODUCT_ID",
"original_transaction_id":"140000855642848",
"product_id":"PRODUCT_ID",
"auto_renew_status":"1"
}
],
"status":0
}
Также перед отправкой запроса и после отправки стоит сверить id продукта, который запрашивает клиент и который мы получаем в ответе.
Полезная для нас информация содержится в свойствах in_app и latest_receipt_info, и на первый взгляд содержимое этих свойств идентичны, но:
latest_receipt_info содержит все покупки.
in_app содержит Non-consumable и Non-Auto-Renewable покупки.
Будем использовать latest_receipt_info, соотвественно в этом массиве ищем нужный нам продукт по свойству product_id и проверяем дату, если это подписка. Конечно, стоит еще проверить не начислили ли мы уже эту покупку пользователю, особенно актуально для Consumable Purchase. Проверять можно по свойству original_transaction_id, заранее сохранив в базе, но в рамках этого гайдлайна мы этого делать не будем.
Тогда проверка покупки будет выглядеть примерно так
/**
* product - id покупки
* resultFromApple - ответ от Apple, полученный выше
* productType - тип покупки (подписка, расходуемая или non-consumable)
* sandBox - тестовая среда или нет
*
**/
async parseResponse(product: string, resultFromApple: any, productType: ProductType, sandBox: boolean) {
let parsedResult: IPurchaseParsedResultFromProvider = {
validated: false,
trial: false,
checked: false,
sandBox,
productType: productType,
lastResponseFromProvider: JSON.stringify(resultFromApple)
};
switch (resultFromApple.status) {
/**
* Валидная подписка
*/
case 0: {
/**
* Ищем в ответе информацию о транзакции по запрашиваемому продукту
**/
let currentPurchaseFromApple = this.getCurrentPurchaseFromAppleResult(resultFromApple, product!, productType);
if (!currentPurchaseFromApple) break;
parsedResult.checked = true;
parsedResult.originalTransactionId = this.getTransactionIdFromAppleResponse(currentPurchaseFromApple);
if (productType === ProductType.Subscription) {
parsedResult.validated = (this.checkDateIsAfter(currentPurchaseFromApple.expires_date_ms)) ? true : false;
parsedResult.expiredAt = (this.checkDateIsValid(currentPurchaseFromApple.expires_date_ms)) ?
this.formatDate(currentPurchaseFromApple.expires_date_ms) : undefined;
} else {
parsedResult.validated = true;
}
parsedResult.trial = !!currentPurchaseFromApple.is_trial_period;
break;
}
default:
if (!resultFromApple) console.log('empty result from apple');
else console.log('incorrect result from apple, status:', resultFromApple.status);
}
return parsedResult;
}
После этого можно возвращать ответ на клиент по нашей покупке, которая хранится в переменной parsedResult. Формировать структуру этого объекта вы можете по своему усмотрению, зависит от ваших потребностей, но самое главное, что на этом шаге мы уже знаем валидна покупка или нет, и информацию об этом хранится в parsedResult.validated.
Если интересно, то могу написать отдельную статью о том, как обрабатывать ответ от iTunnes Connect по каждому свойству, ибо вот это далеко нетривиальная задача. Так же возможно будет полезно рассказать о том, как работать с проверкой автовозобновляемых покупок, когда их проверять и как, потому что по времени истечения подписки запускать крон недостаточно – однозначно возникнут проблемы и пользователь останется без оплаченных покупок, а в этом случае сразу будут отзывы с одной звездой в мобильном сторе.
Android
Для гугла достаточно сильно отличается формат запроса, ибо сначала надо авторизоваться посредством OAuth и потом только отправлять запрос на проверку покупки.
Для гугла нам понадобится чуть больше входных параметров:
google: any = {
host: 'androidpublisher.googleapis.com',
path: '/androidpublisher/v3/applications',
email: process.env.GOOGLE_EMAIL,
key: process.env.GOOGLE_KEY,
storeName: process.env.GOOGLE_STORE_NAME
}
Получить эти данные можно воспользовавшись инструкцией по ссылке.
Окей, гугл, прими запрос:
/**
* product - название продукта
* token - чек
* productType – тип покупки, подписка или нет
**/
async getPurchaseInfoFromGoogle(product: string, token: string, productType: ProductType) {
try {
let options = {
email: this._constants.google.email,
key: this._constants.google.key,
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
};
const client = new JWT(options);
let productOrSubscriptionUrlPart = productType === ProductType.Subscription ? 'subscriptions' : 'products';
const url = `https://${this._constants.google.host}${this._constants.google.path}/${this._constants.google.storeName}/purchases/${productOrSubscriptionUrlPart}/${product}/tokens/${token}`;
const res = await client.request({ url });
return res.data as ResultFromGoogle;
} catch(e) {
return e as ErrorFromGoogle;
}
}
Для авторизации воспользуемся библиотекой google-auth-library и класс JWT.
Ответ от гугла выглядит примерно так:
{
startTimeMillis: "1603956759767",
expiryTimeMillis: "1603966728908",
autoRenewing: false,
priceCurrencyCode: "RUB",
priceAmountMicros: "499000000",
countryCode: "RU",
developerPayload: {
"developerPayload":"",
"is_free_trial":false,
"has_introductory_price_trial":false,
"is_updated":false,
"accountId":""
},
cancelReason: 1,
orderId: "GPA.3335-9310-7555-53285..5",
purchaseType: 0,
acknowledgementState: 1,
kind: "androidpublisher#subscriptionPurchase"
}
Теперь перейдем к проверке покупки
parseResponse(product: string, result: ResultFromGoogle | ErrorFromGoogle, type: ProductType) {
let parsedResult: IPurchaseParsedResultFromProvider = {
validated: false,
trial: false,
checked: true,
sandBox: false,
productType: type,
lastResponseFromProvider: JSON.stringify(result),
};
if (this.isResultFromGoogle(result)) {
if (this.isSubscriptionResult(result)) {
parsedResult.expiredAt = moment(result.expiryTimeMillis, 'x').toDate();
parsedResult.validated = this.checkDateIsAfter(parsedResult.expiredAt);
} else if (this.isProductResult(result)) {
parsedResult.validated = true;
}
}
return parsedResult;
}
Тут все достаточно тривиально. На выходе мы также получаем parsedResult, где самое важное хранится в свойстве validated – прошла покупка проверку или нет.
Итог
По существу буквально в 2 метода можно проверить покупку. Репозиторий с полным кодом доступен по ссылке https://github.com/denjoygroup/inapppurchase (автор кода Алексей Геворкян)
Конечно, мы упустили очень много нюансов обработки покупки, которые стоит учитывать при работе с реальными покупками.
Есть два хороших сервиса, которые предоставляют сервис для проверки чеков: https://ru.adapty.io/ и https://apphud.com/. Но, во-первых, для некоторых категорий приложений нельзя передавать данные 3 стороне, а во-вторых, если вы хотите отдавать платный контент динамически при совершении пользователем покупки, то вам придется разворачивать свой сервер.
P.S.
Ну, и, конечно, самое важное в серверной разработке – это масштабируемость и устойчивость. Если у вас большая аудитория пользователей и при этом сервер не способен выдерживать нагрузки, то лучше и не реализовывать проверку покупок самим, а отправлять запросы сразу в iTunnes Connect и в Google API, иначе ваши пользователи сильно расстроятся.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Data Engineering] Python API в Delta Lake — простые и надежные операции Upsert и Delete (перевод)
- [Программирование, Разработка под iOS, Swift] Разница между @StateObject, @EnvironmentObject и @ObservedObject в SwiftUI (перевод)
- [Разработка под iOS, Системы сборки, Облачные сервисы] Интеграция CI/CD для нескольких сред с Jenkins и Fastlane. Часть 2 (перевод)
- [Open source, Git, Системы управления версиями, DevOps] Вышел релиз GitLab 13.5 с обновлениями для безопасности мобильных приложений и вики-страницами групп
- [Программирование, Java, Kotlin] Переезд из Java в Kotlin: как забрать коллекции с собой
- [Программирование, Разработка под iOS, Разработка мобильных приложений] SPM: модуляризация проекта для увеличения скорости сборки
- [Компьютерное железо, Видеокарты, Процессоры] Компьютеры Mac на Apple Silicon M1 не могут работать с внешними GPU
- [Исследования и прогнозы в IT, Бизнес-модели, Смартфоны, Социальные сети и сообщества, IT-компании] Про iPhone 12, названия моделей и ценообразование (перевод)
- [Мессенджеры, Разработка мобильных приложений, API, Софт, Видеоконференцсвязь] Top Chat Software to Integrate on Online Consultation Apps for Better Collaboration & Business Outcome
- [Гаджеты, Ноутбуки, Процессоры, IT-компании] Apple представила MacBook Air, MacBook Pro 13 и Mac mini на новых ARM-процессорах M1
Теги для поиска: #_node.js, #_google_api, #_monetizatsija_mobilnyh_prilozhenij (Монетизация мобильных приложений), #_subscriptions, #_inapp_purchases, #_inapp_purchase, #_api, #_receipt, #_validation, #_apple, #_ios, #_development, #_android, #_itunes_connect, #_google_api, #_node.js, #_google_api, #_monetizatsija_mobilnyh_prilozhenij (
Монетизация мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:24
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Всем привет! Сегодня расскажу вам как развернуть сервер для проверки In-app Purchase и In-app Subscription для iOS и Android (server-server validation). На хабре есть статья от 2013 года про серверную проверку покупок. В статье говорится о том, что валидация в первую очередь необходима для предотвращения доступа к платному контенту при помощи джейлбрейка и другого софта. На мой взгляд в 2020 году эта проблема не так актуальна, и в первую очередь сервер с проверкой покупок необходима для синхронизации покупок в рамках одного аккаунта на нескольких устройствах В проверке чеков покупок нет никакой технической сложности, по факту сервер просто «проксирует» запрос и сохраняет данные о покупке. То есть задачу такого сервера можно разделить на 4 этапа:
В рамках статьи опустим 3 пункт, ибо он сугубо индивидуален. Код в статье будет написан на Node.js, но по сути логика универсальна и не составит труда использовать ее написать валидацию на любом языке программирования. Еще есть статья хорошая «То, что нужно знать о проверке чека App Store (App Store receipt)», ребята делают сервис для работы с подписками. В статье детально описано, что такое чек (receipt) и для чего нужна проверка покупок. Сразу скажу, что в сниппетах кода используются вспомогательные классы и интерфейсы, весь код доступен в репозитории по ссылке https://github.com/denjoygroup/inapppurchase. В приведенном ниже фрагментах кода, я постарался дать названия используемым методам такие, чтобы приходилось делать отсылки к этим функциям. iOS Для проверки вам нужен Apple Shared Secret – это ключ, который вы должны получить в iTunnes Connect, он нужен для проверки чеков. В первую очередь зададим параметры для создания запросов: apple: any = {
password: process.env.APPLE_SHARED_SECRET, // ключ, укажите свой host: 'buy.itunes.apple.com', sandbox: 'sandbox.itunes.apple.com', path: '/verifyReceipt', apiHost: 'api.appstoreconnect.apple.com', pathToCheckSales: '/v1/salesReports' } Теперь создадим функцию для отправки запроса. В зависимости от среды, с которой работаете, вы должны отправлять запрос либо на sandbox.itunes.apple.com для тестовых покупок, либо в прод buy.itunes.apple.com /**
* receiptValue - чек, который проверяете * sandBox - среда разработк **/ async _verifyReceipt(receiptValue: string, sandBox: boolean) { let options = { host: sandBox ? this._constants.apple.sandbox : this._constants.apple.host, path: this._constants.apple.path, method: 'POST' }; let body = { 'receipt-data': receiptValue, 'password': this._constants.apple.password }; let result = null; let stringResult = await this._handlerService.sendHttp(options, body, 'https'); result = JSON.parse(stringResult); return result; } Если запрос прошел успешно, то в ответе от сервера Apple в поле status вы получите данные о вашей покупке. У статуса возможны несколько значений, в зависимости от которых вы должны обработать покупку 21000 – Запрос был отправлен – не методом POST 21002 – Чек поврежден, не удалось его распарсить 21003 – Некорректный чек, покупка не подтверждена 21004 – Ваш Shared Secret некорректный или не соответствует чеку 21005 – Сервер эпла не смог обработать ваш запрос, стоит попробовать еще раз 21006 – Чек недействителен 21007 – Чек из SandBox (тестовой среды), но был отправлен в prod 21008 – Чек из прода, но был отправлен в тестовую среду 21009 – Сервер эпла не смог обработать ваш запрос, стоит попробовать еще раз 21010 – Аккаунт был удален 0 – Покупка валидна Пример ответа от iTunnes Connect выглядит следующим образом {
"environment":"Production", "receipt":{ "receipt_type":"Production", "adam_id":1527458047, "app_item_id":1527458047, "bundle_id":"BUNDLE_ID", "application_version":"0", "download_id":34089715299389, "version_external_identifier":838212484, "receipt_creation_date":"2020-11-03 20:47:54 Etc/GMT", "receipt_creation_date_ms":"1604436474000", "receipt_creation_date_pst":"2020-11-03 12:47:54 America/Los_Angeles", "request_date":"2020-11-03 20:48:01 Etc/GMT", "request_date_ms":"1604436481804", "request_date_pst":"2020-11-03 12:48:01 America/Los_Angeles", "original_purchase_date":"2020-10-26 19:24:19 Etc/GMT", "original_purchase_date_ms":"1603740259000", "original_purchase_date_pst":"2020-10-26 12:24:19 America/Los_Angeles", "original_application_version":"0", "in_app":[ { "quantity":"1", "product_id":"PRODUCT_ID", "transaction_id":"140000855642848", "original_transaction_id":"140000855642848", "purchase_date":"2020-11-03 20:47:53 Etc/GMT", "purchase_date_ms":"1604436473000", "purchase_date_pst":"2020-11-03 12:47:53 America/Los_Angeles", "original_purchase_date":"2020-11-03 20:47:54 Etc/GMT", "original_purchase_date_ms":"1604436474000", "original_purchase_date_pst":"2020-11-03 12:47:54 America/Los_Angeles", "expires_date":"2020-12-03 20:47:53 Etc/GMT", "expires_date_ms":"1607028473000", "expires_date_pst":"2020-12-03 12:47:53 America/Los_Angeles", "web_order_line_item_id":"140000337829668", "is_trial_period":"false", "is_in_intro_offer_period":"false" } ] }, "latest_receipt_info":[ { "quantity":"1", "product_id":"PRODUCT_ID", "transaction_id":"140000855642848", "original_transaction_id":"140000855642848", "purchase_date":"2020-11-03 20:47:53 Etc/GMT", "purchase_date_ms":"1604436473000", "purchase_date_pst":"2020-11-03 12:47:53 America/Los_Angeles", "original_purchase_date":"2020-11-03 20:47:54 Etc/GMT", "original_purchase_date_ms":"1604436474000", "original_purchase_date_pst":"2020-11-03 12:47:54 America/Los_Angeles", "expires_date":"2020-12-03 20:47:53 Etc/GMT", "expires_date_ms":"1607028473000", "expires_date_pst":"2020-12-03 12:47:53 America/Los_Angeles", "web_order_line_item_id":"140000447829668", "is_trial_period":"false", "is_in_intro_offer_period":"false", "subscription_group_identifier":"20675121" } ], "latest_receipt":"RECEIPT", "pending_renewal_info":[ { "auto_renew_product_id":"PRODUCT_ID", "original_transaction_id":"140000855642848", "product_id":"PRODUCT_ID", "auto_renew_status":"1" } ], "status":0 } Также перед отправкой запроса и после отправки стоит сверить id продукта, который запрашивает клиент и который мы получаем в ответе. Полезная для нас информация содержится в свойствах in_app и latest_receipt_info, и на первый взгляд содержимое этих свойств идентичны, но: latest_receipt_info содержит все покупки. in_app содержит Non-consumable и Non-Auto-Renewable покупки. Будем использовать latest_receipt_info, соотвественно в этом массиве ищем нужный нам продукт по свойству product_id и проверяем дату, если это подписка. Конечно, стоит еще проверить не начислили ли мы уже эту покупку пользователю, особенно актуально для Consumable Purchase. Проверять можно по свойству original_transaction_id, заранее сохранив в базе, но в рамках этого гайдлайна мы этого делать не будем. Тогда проверка покупки будет выглядеть примерно так /**
* product - id покупки * resultFromApple - ответ от Apple, полученный выше * productType - тип покупки (подписка, расходуемая или non-consumable) * sandBox - тестовая среда или нет * **/ async parseResponse(product: string, resultFromApple: any, productType: ProductType, sandBox: boolean) { let parsedResult: IPurchaseParsedResultFromProvider = { validated: false, trial: false, checked: false, sandBox, productType: productType, lastResponseFromProvider: JSON.stringify(resultFromApple) }; switch (resultFromApple.status) { /** * Валидная подписка */ case 0: { /** * Ищем в ответе информацию о транзакции по запрашиваемому продукту **/ let currentPurchaseFromApple = this.getCurrentPurchaseFromAppleResult(resultFromApple, product!, productType); if (!currentPurchaseFromApple) break; parsedResult.checked = true; parsedResult.originalTransactionId = this.getTransactionIdFromAppleResponse(currentPurchaseFromApple); if (productType === ProductType.Subscription) { parsedResult.validated = (this.checkDateIsAfter(currentPurchaseFromApple.expires_date_ms)) ? true : false; parsedResult.expiredAt = (this.checkDateIsValid(currentPurchaseFromApple.expires_date_ms)) ? this.formatDate(currentPurchaseFromApple.expires_date_ms) : undefined; } else { parsedResult.validated = true; } parsedResult.trial = !!currentPurchaseFromApple.is_trial_period; break; } default: if (!resultFromApple) console.log('empty result from apple'); else console.log('incorrect result from apple, status:', resultFromApple.status); } return parsedResult; } После этого можно возвращать ответ на клиент по нашей покупке, которая хранится в переменной parsedResult. Формировать структуру этого объекта вы можете по своему усмотрению, зависит от ваших потребностей, но самое главное, что на этом шаге мы уже знаем валидна покупка или нет, и информацию об этом хранится в parsedResult.validated. Если интересно, то могу написать отдельную статью о том, как обрабатывать ответ от iTunnes Connect по каждому свойству, ибо вот это далеко нетривиальная задача. Так же возможно будет полезно рассказать о том, как работать с проверкой автовозобновляемых покупок, когда их проверять и как, потому что по времени истечения подписки запускать крон недостаточно – однозначно возникнут проблемы и пользователь останется без оплаченных покупок, а в этом случае сразу будут отзывы с одной звездой в мобильном сторе. Android Для гугла достаточно сильно отличается формат запроса, ибо сначала надо авторизоваться посредством OAuth и потом только отправлять запрос на проверку покупки. Для гугла нам понадобится чуть больше входных параметров: google: any = {
host: 'androidpublisher.googleapis.com', path: '/androidpublisher/v3/applications', email: process.env.GOOGLE_EMAIL, key: process.env.GOOGLE_KEY, storeName: process.env.GOOGLE_STORE_NAME } Получить эти данные можно воспользовавшись инструкцией по ссылке. Окей, гугл, прими запрос: /**
* product - название продукта * token - чек * productType – тип покупки, подписка или нет **/ async getPurchaseInfoFromGoogle(product: string, token: string, productType: ProductType) { try { let options = { email: this._constants.google.email, key: this._constants.google.key, scopes: ['https://www.googleapis.com/auth/androidpublisher'], }; const client = new JWT(options); let productOrSubscriptionUrlPart = productType === ProductType.Subscription ? 'subscriptions' : 'products'; const url = `https://${this._constants.google.host}${this._constants.google.path}/${this._constants.google.storeName}/purchases/${productOrSubscriptionUrlPart}/${product}/tokens/${token}`; const res = await client.request({ url }); return res.data as ResultFromGoogle; } catch(e) { return e as ErrorFromGoogle; } } Для авторизации воспользуемся библиотекой google-auth-library и класс JWT. Ответ от гугла выглядит примерно так: {
startTimeMillis: "1603956759767", expiryTimeMillis: "1603966728908", autoRenewing: false, priceCurrencyCode: "RUB", priceAmountMicros: "499000000", countryCode: "RU", developerPayload: { "developerPayload":"", "is_free_trial":false, "has_introductory_price_trial":false, "is_updated":false, "accountId":"" }, cancelReason: 1, orderId: "GPA.3335-9310-7555-53285..5", purchaseType: 0, acknowledgementState: 1, kind: "androidpublisher#subscriptionPurchase" } Теперь перейдем к проверке покупки parseResponse(product: string, result: ResultFromGoogle | ErrorFromGoogle, type: ProductType) {
let parsedResult: IPurchaseParsedResultFromProvider = { validated: false, trial: false, checked: true, sandBox: false, productType: type, lastResponseFromProvider: JSON.stringify(result), }; if (this.isResultFromGoogle(result)) { if (this.isSubscriptionResult(result)) { parsedResult.expiredAt = moment(result.expiryTimeMillis, 'x').toDate(); parsedResult.validated = this.checkDateIsAfter(parsedResult.expiredAt); } else if (this.isProductResult(result)) { parsedResult.validated = true; } } return parsedResult; } Тут все достаточно тривиально. На выходе мы также получаем parsedResult, где самое важное хранится в свойстве validated – прошла покупка проверку или нет. Итог По существу буквально в 2 метода можно проверить покупку. Репозиторий с полным кодом доступен по ссылке https://github.com/denjoygroup/inapppurchase (автор кода Алексей Геворкян) Конечно, мы упустили очень много нюансов обработки покупки, которые стоит учитывать при работе с реальными покупками. Есть два хороших сервиса, которые предоставляют сервис для проверки чеков: https://ru.adapty.io/ и https://apphud.com/. Но, во-первых, для некоторых категорий приложений нельзя передавать данные 3 стороне, а во-вторых, если вы хотите отдавать платный контент динамически при совершении пользователем покупки, то вам придется разворачивать свой сервер. P.S. Ну, и, конечно, самое важное в серверной разработке – это масштабируемость и устойчивость. Если у вас большая аудитория пользователей и при этом сервер не способен выдерживать нагрузки, то лучше и не реализовывать проверку покупок самим, а отправлять запросы сразу в iTunnes Connect и в Google API, иначе ваши пользователи сильно расстроятся. =========== Источник: habr.com =========== Похожие новости:
Монетизация мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 22:24
Часовой пояс: UTC + 5