repo_description: Parse Server for Node.js / Express
repo_topics: baas backend file-storage graphql graphql-api graphql-relay graphql-server hacktoberfest mbaas mongodb nodejs notifications object-storage parse-platform parse-server postgres relay rest-api
code: import PromiseRouter from '../PromiseRouter';
  const request = require('../request');
  const rest = require('../rest');
  import Parse from 'parse/node';
  
  // TODO move validation logic in IAPValidationController
  const IAP_SANDBOX_URL = 'https://sandbox.itunes.apple.com/verifyReceipt';
  const IAP_PRODUCTION_URL = 'https://buy.itunes.apple.com/verifyReceipt';
  
  const APP_STORE_ERRORS = {
    21000: 'The App Store could not read the JSON object you provided.',
    21002: 'The data in the receipt-data property was malformed or missing.',
    21003: 'The receipt could not be authenticated.',
    21004: 'The shared secret you provided does not match the shared secret on file for your account.',
    21005: 'The receipt server is not currently available.',
    21006: 'This receipt is valid but the subscription has expired.',
    21007: 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.',
    21008: 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.',
  };
  
  function appStoreError(status) {
    status = parseInt(status);
    var errorString = APP_STORE_ERRORS[status] || 'unknown error.';
    return { status: status, error: errorString };
  }
  
  function validateWithAppStore(url, receipt) {
    return request({
      url: url,
      method: 'POST',
      body: { 'receipt-data': receipt },
      headers: {
        'Content-Type': 'application/json',
      },
    }).then(httpResponse => {
      const body = httpResponse.data;
      if (body && body.status === 0) {
        // No need to pass anything, status is OK
        return;
      }
      // receipt is from test and should go to test
      throw body;
    });
  }
  
  function getFileForProductIdentifier(productIdentifier, req) {
    return rest
      .find(
        req.config,
        req.auth,
        '_Product',
        { productIdentifier: productIdentifier },
        undefined,
        req.info.clientSDK,
        req.info.context
      )
      .then(function (result) {
        const products = result.results;
        if (!products || products.length != 1) {
          // Error not found or too many
          throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
        }
  
        var download = products[0].download;
        return Promise.resolve({ response: download });
      });
  }
  
  export class IAPValidationRouter extends PromiseRouter {
    handleRequest(req) {
      let receipt = req.body?.receipt;
      const productIdentifier = req.body?.productIdentifier;
  
      if (!receipt || !productIdentifier) {
        // TODO: Error, malformed request
        throw new Parse.Error(Parse.Error.INVALID_JSON, 'missing receipt or productIdentifier');
      }
  
      // Transform the object if there
      // otherwise assume it's in Base64 already
      if (typeof receipt == 'object') {
        if (receipt['__type'] == 'Bytes') {
          receipt = receipt.base64;
        }
      }
  
      if (process.env.TESTING == '1' && req.body?.bypassAppStoreValidation) {
        return getFileForProductIdentifier(productIdentifier, req);
      }
  
      function successCallback() {
        return getFileForProductIdentifier(productIdentifier, req);
      }
  
      function errorCallback(error) {
        return Promise.resolve({ response: appStoreError(error.status) });
      }
  
      return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then(
        () => {
          return successCallback();
        },
        error => {
          if (error.status == 21007) {
            return validateWithAppStore(IAP_SANDBOX_URL, receipt).then(
              () => {
                return successCallback();
              },
              error => {
                return errorCallback(error);
              }
            );
          }
  
          return errorCallback(error);
        }
      );
    }
  
    mountRoutes() {
      this.route('POST', '/validate_purchase', this.handleRequest);
    }
  }
  