diff --git a/test/api/v3/unit/libs/inAppPurchases.test.js b/test/api/v3/unit/libs/inAppPurchases.test.js index 42eefa0195..00986aa3ac 100644 --- a/test/api/v3/unit/libs/inAppPurchases.test.js +++ b/test/api/v3/unit/libs/inAppPurchases.test.js @@ -2,15 +2,19 @@ import { model as User } from '../../../../../website/server/models/user'; import iapLibrary from 'in-app-purchase'; import iap from '../../../../../website/server/libs/api-v3/inAppPurchases'; -describe.only('In App Purchases', () => { +describe('In App Purchases', () => { let user; + let setupSpy; + let validateSpy; beforeEach(() => { user = new User(); + setupSpy = sinon.spy(); + validateSpy = sinon.spy(); sandbox.stub(iapLibrary, 'setup').yields(); sandbox.stub(iapLibrary, 'validate').yields(); - sandbox.stub(iapLibrary, 'isValidated').yields(); + sandbox.stub(iapLibrary, 'isValidated').returns(); }); afterEach(() => { @@ -19,21 +23,22 @@ describe.only('In App Purchases', () => { describe('Android', () => { it('applies new valid receipt', async () => { - let res = await iap.iapAndroidVerify(user, { + iapLibrary.setup.yields(undefined, setupSpy); + + await iap.iapAndroidVerify(user, { receipt: {token: 1}, }); - console.log(res); + expect(setupSpy).to.have.been.called; + expect(validateSpy).to.have.been.called; }); it('example with stub yielding an error', async () => { iapLibrary.setup.yields(new Error('an error')); - let res = await iap.iapAndroidVerify(user, { + await iap.iapAndroidVerify(user, { receipt: {token: 1}, }); - - console.log(res); }); }); }); diff --git a/website/server/controllers/top-level/payments/iap.js b/website/server/controllers/top-level/payments/iap.js index 884c6b72c9..eafcb65a73 100644 --- a/website/server/controllers/top-level/payments/iap.js +++ b/website/server/controllers/top-level/payments/iap.js @@ -24,7 +24,9 @@ api.iapAndroidVerify = { middlewares: [authWithUrl], async handler (req, res) { let resObject = await iapAndroidVerify(res.locals.user, req.body); - return res.json(resObject); + return res + .status(resObject.ok ? 200 : 500) + .json(resObject); }, }; @@ -41,7 +43,9 @@ api.iapiOSVerify = { middlewares: [authWithHeaders()], async handler (req, res) { let resObject = await iapIOSVerify(res.locals.user, req.body); - return res.json(resObject); + return res + .status(resObject.ok ? 200 : 500) + .json(resObject); }, }; diff --git a/website/server/libs/api-v3/inAppPurchases.js b/website/server/libs/api-v3/inAppPurchases.js index 8c5043a029..1797751ef6 100644 --- a/website/server/libs/api-v3/inAppPurchases.js +++ b/website/server/libs/api-v3/inAppPurchases.js @@ -3,14 +3,15 @@ import iap from 'in-app-purchase'; import payments from './payments'; import { model as IapPurchaseReceipt } from '../../models/iapPurchaseReceipt'; import Bluebird from 'bluebird'; +import logger from './logger'; // Validation ERROR Codes -const INVALID_PAYLOAD = 6778001; +// const INVALID_PAYLOAD = 6778001; // const CONNECTION_FAILED = 6778002; // const PURCHASE_EXPIRED = 6778003; iap.config({ - // this is the path to the directory containing iap-sanbox/iap-live files + // This is the path to the directory containing iap-sanbox/iap-live files googlePublicKeyPath: nconf.get('IAP_GOOGLE_KEYDIR'), }); @@ -18,155 +19,135 @@ let iapSetup = Bluebird.promisify(iap.setup, { context: iap }); let iapValidate = Bluebird.promisify(iap.validate, { context: iap }); async function iapAndroidVerify (user, iapBody) { + // Defining these 2 variables here so they can be logged in case of error + let googleRes; + let token; + try { await iapSetup(); + let testObj = { data: iapBody.transaction.receipt, signature: iapBody.transaction.signature, }; - try { - let googleRes = iapValidate(iap.GOOGLE, testObj); + googleRes = await iapValidate(iap.GOOGLE, testObj); - if (iap.isValidated(googleRes)) { - let resObj = { + if (iap.isValidated(googleRes)) { + token = testObj.data.token || testObj.data.purchaseToken; + + let existingReceipt = await IapPurchaseReceipt.findOne({ + _id: token, + }).exec(); + + if (!existingReceipt) { + await IapPurchaseReceipt.create({ + _id: token, + consumed: true, + userID: user._id, + }); + + await payments.buyGems({ + user, + paymentMethod: 'IAP GooglePlay', + amount: 5.25, + }); + + return { ok: true, data: googleRes, }; - - let token = testObj.data.token; - if (!token) token = testObj.data.purchaseToken; - - let existingReceipt = await IapPurchaseReceipt.findOne({ - _id: token, - }).exec(); - - if (!existingReceipt) { - try { - await IapPurchaseReceipt.create({ - token, - consumed: true, - userID: user._id, - }); - - await payments.buyGems({ - user, - paymentMethod: 'IAP GooglePlay', - amount: 5.25, - }); - - return resObj; - } catch (err) { - return resObj; - } - } else { - return resObj; - } + } else { + throw new Error('RECEIPT_ALREADY_USED'); } - } catch (error) { - return { - ok: false, - data: { - code: INVALID_PAYLOAD, - message: error.toString(), - }, - }; + } else { + throw new Error('INVALID_RECEIPT'); } - } catch (error) { + } catch (err) { + logger.error(err, { + userId: user._id, + iapBody, + googleRes, + token, + }); + return { ok: false, - data: 'IAP Error', + data: 'An error occurred while processing the purchase.', }; } } async function iapIOSVerify (user, iapBody) { - iap.setup(function iosSetupResult (error) { - if (error) { - let resObj = { - ok: false, - data: 'IAP Error', - }; + // Defining these 2 variables here so they can be logged in case of error + let token; + let appleRes; - return resObj; - } + try { + await iapSetup(); + appleRes = await iapValidate(iap.APPLE, iapBody.transaction.receipt); - // iap is ready - iap.validate(iap.APPLE, iapBody.transaction.receipt, (err, appleRes) => { - if (err) { - let resObj = { - ok: false, - data: { - code: INVALID_PAYLOAD, - message: err.toString(), - }, - }; + if (iap.isValidated(appleRes)) { + token = 'aaa'; - return resObj; + let existingReceipt = await IapPurchaseReceipt.findOne({ + _id: token, + }).exec(); + + if (!existingReceipt) { + await IapPurchaseReceipt.create({ + _id: token, + consumed: true, + userID: user._id, + }); + } else { + throw new Error('RECEIPT_ALREADY_USED'); } - if (iap.isValidated(appleRes)) { - let purchaseDataList = iap.getPurchaseData(appleRes); - if (purchaseDataList.length > 0) { - let correctReceipt = true; + let purchaseDataList = iap.getPurchaseData(appleRes); + if (purchaseDataList.length === 0) throw new Error('NO_ITEM_PURCHASED'); - for (let index in purchaseDataList) { - switch (purchaseDataList[index].productId) { - case 'com.habitrpg.ios.Habitica.4gems': - payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1}); - break; - case 'com.habitrpg.ios.Habitica.8gems': - payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2}); - break; - case 'com.habitrpg.ios.Habitica.20gems': - case 'com.habitrpg.ios.Habitica.21gems': - payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25}); - break; - case 'com.habitrpg.ios.Habitica.42gems': - payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5}); - break; - default: - correctReceipt = false; - } - } + let correctReceipt = true; - if (correctReceipt) { - let resObj = { - ok: true, - data: appleRes, - }; - - // yay good! - return resObj; - } + // Purchasing one item at a time (processing of await(s) below is sequential not parallel) + for (let index in purchaseDataList) { + switch (purchaseDataList[index].productId) { + case 'com.habitrpg.ios.Habitica.4gems': + await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 1}); // eslint-disable-line babel/no-await-in-loop + break; + case 'com.habitrpg.ios.Habitica.8gems': + await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 2}); // eslint-disable-line babel/no-await-in-loop + break; + case 'com.habitrpg.ios.Habitica.20gems': + case 'com.habitrpg.ios.Habitica.21gems': + await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 5.25}); // eslint-disable-line babel/no-await-in-loop + break; + case 'com.habitrpg.ios.Habitica.42gems': + await payments.buyGems({user, paymentMethod: 'IAP AppleStore', amount: 10.5}); // eslint-disable-line babel/no-await-in-loop + break; + default: + correctReceipt = false; } - - // wrong receipt content - let resObj = { - ok: false, - data: { - code: INVALID_PAYLOAD, - message: 'Incorrect receipt content', - }, - }; - - return resObj; } - // invalid receipt - let resObj = { - ok: false, - data: { - code: INVALID_PAYLOAD, - message: 'Invalid receipt', - }, - }; - - return resObj; + if (!correctReceipt) throw new Error('INVALID_ITEM_PURCHASED'); + } else { + throw new Error('INVALID_RECEIPT'); + } + } catch (err) { + logger.error(err, { + userId: user._id, + iapBody, + appleRes, + token, }); - }); -} + return { + ok: false, + data: 'An error occurred while processing the purchase.', + }; + } +} module.exports = { iapAndroidVerify,