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); } }