你好!今天,我将告诉您如何部署服务器以检查iOS和Android的应用内购买和应用内订阅(服务器-服务器验证)。
在Habré上有一篇2013年的文章,涉及购买服务器的支票。该文章说,验证是防止使用越狱和其他软件访问付费内容的主要必要条件。我认为,到2020年,这个问题并不是那么紧迫,首先,需要具有购买验证功能的服务器才能在多个设备上的一个帐户内同步购买。
检查采购收据没有技术上的困难;实际上,服务器只是“代理”请求并存储采购数据。
也就是说,这种服务器的任务可以分为四个阶段:
- 购买后接收带有应用发送的收据的请求
- 要求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, .