Implementamos un servidor para verificar las compras dentro de la aplicación en 60 minutos

¡Hola! Hoy te diré cómo implementar un servidor para verificar la compra en la aplicación y la suscripción en la aplicación para iOS y Android (validación servidor-servidor).



En Habré hay un artículo de 2013 sobre verificación de compras por parte del servidor. El artículo dice que la validación es principalmente necesaria para evitar el acceso a contenido pago mediante jailbreak y otro software. En mi opinión, en 2020 este problema no es tan urgente y, en primer lugar, se necesita un servidor con verificación de compra para sincronizar las compras dentro de una cuenta en varios dispositivos.



No hay ninguna dificultad técnica para verificar los recibos de compra; de hecho, el servidor simplemente "envía" la solicitud y almacena los datos de la compra.







Es decir, la tarea de dicho servidor se puede dividir en 4 etapas:



  • Reciba una solicitud con un recibo enviado por la aplicación después de la compra
  • Solicitud a Apple / Google para verificación de verificación
  • Guardar datos de transacciones
  • Respuesta de la aplicación


En el marco del artículo, omitiremos el punto 3, por ser puramente individual.



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



21004Shared 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