diff --git a/package-lock.json b/package-lock.json index 6a78a1b4ff..2a5463252d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.92.5", + "version": "4.92.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d724865b27..c62fc9be3c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.92.5", + "version": "4.92.6", "main": "./website/server/index.js", "dependencies": { "@google-cloud/trace-agent": "^3.6.0", diff --git a/test/common/ops/buy/buy.js b/test/common/ops/buy/buy.js index 5ec36dfd3a..f203ae0cd9 100644 --- a/test/common/ops/buy/buy.js +++ b/test/common/ops/buy/buy.js @@ -142,4 +142,52 @@ describe('shared.ops.buy', () => { buy(user, {params: {key: 'potion'}, quantity: 2}); expect(user.stats.hp).to.eql(50); }); + + it('errors if user supplies a non-numeric quantity', (done) => { + try { + buy(user, { + params: { + key: 'dilatoryDistress1', + }, + type: 'quest', + quantity: 'bogle', + }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('invalidQuantity')); + done(); + } + }); + + it('errors if user supplies a negative quantity', (done) => { + try { + buy(user, { + params: { + key: 'dilatoryDistress1', + }, + type: 'quest', + quantity: -3, + }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('invalidQuantity')); + done(); + } + }); + + it('errors if user supplies a decimal quantity', (done) => { + try { + buy(user, { + params: { + key: 'dilatoryDistress1', + }, + type: 'quest', + quantity: 1.83, + }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('invalidQuantity')); + done(); + } + }); }); diff --git a/test/common/ops/buy/purchase.js b/test/common/ops/buy/purchase.js index fd1ab65b48..9c3bb5233c 100644 --- a/test/common/ops/buy/purchase.js +++ b/test/common/ops/buy/purchase.js @@ -108,6 +108,47 @@ describe('shared.ops.purchase', () => { done(); } }); + + it('returns error when user supplies a non-numeric quantity', (done) => { + let type = 'eggs'; + let key = 'Wolf'; + + try { + purchase(user, {params: {type, key}, quantity: 'jamboree'}, analytics); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidQuantity')); + done(); + } + }); + + it('returns error when user supplies a negative quantity', (done) => { + let type = 'eggs'; + let key = 'Wolf'; + user.balance = 10; + + try { + purchase(user, {params: {type, key}, quantity: -2}, analytics); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidQuantity')); + done(); + } + }); + + it('returns error when user supplies a decimal quantity', (done) => { + let type = 'eggs'; + let key = 'Wolf'; + user.balance = 10; + + try { + purchase(user, {params: {type, key}, quantity: 2.9}, analytics); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidQuantity')); + done(); + } + }); }); context('successful purchase', () => { diff --git a/website/client/assets/css/sprites/spritesmith-largeSprites-0.css b/website/client/assets/css/sprites/spritesmith-largeSprites-0.css index 48dbcfc757..7aae94a025 100644 --- a/website/client/assets/css/sprites/spritesmith-largeSprites-0.css +++ b/website/client/assets/css/sprites/spritesmith-largeSprites-0.css @@ -1,66 +1,72 @@ .promo_april_fools_2019 { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: 0px -425px; + background-position: 0px -840px; width: 423px; height: 147px; } .promo_armoire_backgrounds_201904 { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: 0px -573px; + background-position: -424px -840px; width: 423px; height: 147px; } +.promo_butterflies { + background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); + background-position: 0px 0px; + width: 676px; + height: 676px; +} .promo_celestial_rainbow_potions { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: 0px -277px; + background-position: -433px -677px; width: 423px; height: 147px; } .promo_classes_spring2019 { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -328px 0px; + background-position: 0px -677px; width: 432px; height: 162px; } .promo_egg_hunt { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -761px 0px; + background-position: -1005px 0px; width: 354px; height: 147px; } .promo_mystery_201903 { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -761px -444px; + background-position: -1005px -444px; width: 351px; height: 147px; } .promo_seasonalshop_spring { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -424px -277px; + background-position: -1005px -592px; width: 162px; height: 138px; } .promo_shiny_seeds { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -761px -296px; + background-position: -1005px -296px; width: 351px; height: 147px; } .promo_spring_avatar_customizations { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -761px -148px; + background-position: -1005px -148px; width: 354px; height: 147px; } .promo_take_this { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: -761px -592px; + background-position: -1168px -592px; width: 96px; height: 69px; } .scene_yesterdailies_repeatables { background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); - background-position: 0px 0px; + background-position: -677px 0px; width: 327px; height: 276px; } diff --git a/website/client/assets/images/sprites/spritesmith-largeSprites-0.png b/website/client/assets/images/sprites/spritesmith-largeSprites-0.png index e5b968f66a..a008da5c0b 100644 Binary files a/website/client/assets/images/sprites/spritesmith-largeSprites-0.png and b/website/client/assets/images/sprites/spritesmith-largeSprites-0.png differ diff --git a/website/client/components/shops/buyModal.vue b/website/client/components/shops/buyModal.vue index fee9615707..83e2c0649d 100644 --- a/website/client/components/shops/buyModal.vue +++ b/website/client/components/shops/buyModal.vue @@ -48,7 +48,7 @@ strong {{ $t('howManyToBuy') }} div(v-if='showAmountToBuy(item)') .box - input(type='number', min='0', v-model.number='selectedAmountToBuy') + input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy') span(:class="{'notEnough': notEnoughCurrency}") span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]") span.cost(:class="getPriceClass()") {{ item.value }} @@ -71,7 +71,7 @@ button.btn.btn-primary( @click="buyItem()", v-else, - :disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft', + :disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid', :class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}" ) {{ $t('buyNow') }} @@ -260,6 +260,7 @@ import * as Analytics from 'client/libs/analytics'; import spellsMixin from 'client/mixins/spells'; import planGemLimits from 'common/script/libs/planGemLimits'; + import numberInvalid from 'client/mixins/numberInvalid'; import svgClose from 'assets/svg/close.svg'; import svgGold from 'assets/svg/gold.svg'; @@ -291,7 +292,7 @@ ]; export default { - mixins: [currencyMixin, notifications, spellsMixin, buyMixin], + mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin], components: { BalanceInfo, EquipmentAttributesGrid, diff --git a/website/client/components/shops/quests/buyQuestModal.vue b/website/client/components/shops/quests/buyQuestModal.vue index 11217d0912..f61e7138ba 100644 --- a/website/client/components/shops/quests/buyQuestModal.vue +++ b/website/client/components/shops/quests/buyQuestModal.vue @@ -22,7 +22,7 @@ .how-many-to-buy strong {{ $t('howManyToBuy') }} .box - input(type='number', min='0', v-model.number='selectedAmountToBuy') + input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy') span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold") span.value(:class="priceType") {{ item.value }} @@ -34,7 +34,8 @@ button.btn.btn-primary( @click="buyItem()", v-else, - :class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}" + :class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}", + :disabled='numberInvalid', ) {{ $t('buyNow') }} div.right-sidebar(v-if="item.drop") @@ -207,12 +208,13 @@ import QuestInfo from './questInfo.vue'; import notifications from 'client/mixins/notifications'; import buyMixin from 'client/mixins/buy'; + import numberInvalid from 'client/mixins/numberInvalid'; import questDialogDrops from './questDialogDrops'; import questDialogContent from './questDialogContent'; export default { - mixins: [currencyMixin, notifications, buyMixin], + mixins: [buyMixin, currencyMixin, notifications, numberInvalid], components: { BalanceInfo, QuestInfo, @@ -309,7 +311,6 @@ return `Unknown type: ${drop.type}`; } }, - purchaseGems () { this.$root.$emit('bv::show::modal', 'buy-gems'); }, diff --git a/website/client/mixins/numberInvalid.js b/website/client/mixins/numberInvalid.js new file mode 100644 index 0000000000..74db1d8be5 --- /dev/null +++ b/website/client/mixins/numberInvalid.js @@ -0,0 +1,7 @@ +export default { + computed: { + numberInvalid () { + return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy); + }, + }, +}; diff --git a/website/common/errors/commonErrorMessages.js b/website/common/errors/commonErrorMessages.js index 00a2d38fea..a9c4173405 100644 --- a/website/common/errors/commonErrorMessages.js +++ b/website/common/errors/commonErrorMessages.js @@ -9,6 +9,7 @@ module.exports = { itemNotFound: 'Item "<%= key %>" not found.', questNotFound: 'Quest "<%= key %>" not found.', spellNotFound: 'Skill "<%= spellId %>" not found.', + invalidQuantity: 'Quantity to purchase must be a positive whole number.', invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"', missingPetFoodFeed: '"pet" and "food" are required parameters.', missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.', diff --git a/website/common/locales/en/npc.json b/website/common/locales/en/npc.json index 3e02837d6c..754ed810b3 100644 --- a/website/common/locales/en/npc.json +++ b/website/common/locales/en/npc.json @@ -99,7 +99,7 @@ "unlocked": "Items have been unlocked", "alreadyUnlocked": "Full set already unlocked.", "alreadyUnlockedPart": "Full set already partially unlocked.", - "invalidQuantity": "Quantity to purchase must be a number.", + "invalidQuantity": "Quantity to purchase must be a positive whole number.", "USD": "(USD)", "newStuff": "New Stuff by Bailey", diff --git a/website/common/script/ops/buy/abstractBuyOperation.js b/website/common/script/ops/buy/abstractBuyOperation.js index 85b9588f0a..0a6f7735dd 100644 --- a/website/common/script/ops/buy/abstractBuyOperation.js +++ b/website/common/script/ops/buy/abstractBuyOperation.js @@ -21,7 +21,7 @@ export class AbstractBuyOperation { let quantity = _get(req, 'quantity'); this.quantity = quantity ? Number(quantity) : 1; - if (isNaN(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity')); + if (this.quantity < 1 || !Number.isInteger(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity')); } /** diff --git a/website/common/script/ops/buy/purchase.js b/website/common/script/ops/buy/purchase.js index 752ffed231..20975506f0 100644 --- a/website/common/script/ops/buy/purchase.js +++ b/website/common/script/ops/buy/purchase.js @@ -73,7 +73,7 @@ module.exports = function purchase (user, req = {}, analytics) { let key = get(req.params, 'key'); let quantity = req.quantity ? Number(req.quantity) : 1; - if (isNaN(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language)); + if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language)); if (!type) { throw new BadRequest(i18n.t('typeRequired', req.language)); diff --git a/website/raw_sprites/spritesmith_large/promo_butterflies.png b/website/raw_sprites/spritesmith_large/promo_butterflies.png new file mode 100644 index 0000000000..032aa50527 Binary files /dev/null and b/website/raw_sprites/spritesmith_large/promo_butterflies.png differ diff --git a/website/server/controllers/api-v3/news.js b/website/server/controllers/api-v3/news.js index 5c5279997f..766ecd73a4 100644 --- a/website/server/controllers/api-v3/news.js +++ b/website/server/controllers/api-v3/news.js @@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth'; let api = {}; // @TODO export this const, cannot export it from here because only routes are exported from controllers -const LAST_ANNOUNCEMENT_TITLE = 'HABITICA BLOG: USE CASE SPOTLIGHT'; +const LAST_ANNOUNCEMENT_TITLE = 'BEHIND THE SCENES: A BUTTERFLY GARDENING ADVENTURE WITH BEFFYMAROO!'; const worldDmg = { // @TODO bailey: false, }; @@ -30,14 +30,13 @@ api.getNews = {
This month's Use Case Spotlight is about Reviewing and Evaluating your Tasks! It features a number of great suggestions submitted by Habiticans in the Use Case Spotlights Guild. We hope it helps any of you who might be considering a refresh for your Task Lists.
-Plus, we're collecting user submissions for the next spotlight! How do you keep things fresh and interesting if you've been using Habitica for a long time? We’ll be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!
-There's a new Behind the Scenes post on the Habitica Blog! Beffymaroo shares some information about starting your own butterfly garden and enjoying watching these fascinating--and beneficial--creatures in your home and yard.
+