Nous déployons un serveur pour vérifier les achats intégrés en 60 minutes

salut! Aujourd'hui, je vais vous dire comment déployer un serveur pour vérifier les achats intégrés et les abonnements intégrés pour iOS et Android (validation serveur-serveur).



Sur Habré il y a un article de 2013 sur la vérification des achats par le serveur. L'article indique que la validation est principalement nécessaire pour empêcher l'accès au contenu payant à l'aide de jailbreak et d'autres logiciels. À mon avis, en 2020, ce problème n'est pas si urgent, et tout d'abord, un serveur avec vérification des achats est nécessaire pour synchroniser les achats au sein d'un même compte sur plusieurs appareils.



Il n'y a pas de difficulté technique à vérifier les reçus d'achat, en fait, le serveur «procède» simplement la demande et stocke les données d'achat.







Autrement dit, la tâche d'un tel serveur peut être divisée en 4 étapes:



  • Recevoir une demande avec un reçu envoyĂ© par l'application après l'achat
  • Demande Ă  Apple / Google pour vĂ©rification de chèque
  • Sauvegarde des donnĂ©es de transaction
  • RĂ©ponse de l'application


Dans le cadre de l'article, nous omettons le point 3, car il est purement individuel.



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, .




All Articles