diff --git a/package-lock.json b/package-lock.json index 9c4a039594..efcde093ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -151,7 +151,7 @@ }, "@sinonjs/formatio": { "version": "2.0.0", - "resolved": "http://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", "dev": true, "requires": { @@ -3765,7 +3765,7 @@ }, "compression": { "version": "1.7.2", - "resolved": "http://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.2.tgz", "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "requires": { "accepts": "1.3.5", @@ -7544,7 +7544,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "optional": true, "requires": { "nan": "2.6.2", "node-pre-gyp": "0.6.39" @@ -7581,7 +7580,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "optional": true, "requires": { "delegates": "1.0.0", "readable-stream": "2.2.9" @@ -7815,7 +7813,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", - "optional": true, "requires": { "fstream": "1.0.11", "inherits": "2.0.3", @@ -7826,7 +7823,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, "requires": { "aproba": "1.1.1", "console-control-strings": "1.1.0", @@ -7915,7 +7911,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "optional": true, "requires": { "assert-plus": "0.2.0", "jsprim": "1.4.0", @@ -8013,7 +8008,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", - "optional": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.0.2", @@ -8073,7 +8067,6 @@ "version": "0.6.39", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", - "optional": true, "requires": { "detect-libc": "1.0.2", "hawk": "3.1.3", @@ -8102,7 +8095,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", - "optional": true, "requires": { "are-we-there-yet": "1.1.4", "console-control-strings": "1.1.0", @@ -8223,7 +8215,6 @@ "version": "2.81.0", "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", - "optional": true, "requires": { "aws-sign2": "0.6.0", "aws4": "1.6.0", @@ -8365,7 +8356,6 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", - "optional": true, "requires": { "debug": "2.6.8", "fstream": "1.0.11", @@ -8415,14 +8405,12 @@ "uuid": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz", - "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=", - "optional": true + "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE=" }, "verror": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", - "optional": true, "requires": { "extsprintf": "1.0.2" } @@ -8431,7 +8419,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "optional": true, "requires": { "string-width": "1.0.2" } @@ -13626,8 +13613,7 @@ "nan": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", - "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=", - "optional": true + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" }, "nanomatch": { "version": "1.2.9", diff --git a/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js b/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js index 974c65ee95..0ea0c966aa 100644 --- a/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js +++ b/test/api/v3/integration/challenges/GET-challenges_challengeId_members.test.js @@ -123,8 +123,8 @@ describe('GET /challenges/:challengeId/members', () => { }); }); - // @TODO times out too many times (when it takes more than 8s) - xit('supports using req.query.lastId to get more members', async () => { + it('supports using req.query.lastId to get more members', async function () { + this.timeout(30000); // @TODO: times out after 8 seconds let group = await generateGroup(user, {type: 'party', name: generateUUID()}); let challenge = await generateChallenge(user, group); diff --git a/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js b/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js index 2e6a612102..abef14c57c 100644 --- a/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js +++ b/test/api/v3/integration/groups/GET-groups_groupId_invites.test.js @@ -81,7 +81,8 @@ describe('GET /groups/:groupId/invites', () => { }); }); - it('supports using req.query.lastId to get more invites', async () => { + it('supports using req.query.lastId to get more invites', async function () { + this.timeout(30000); // @TODO: times out after 8 seconds let leader = await generateUser({balance: 4}); let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()}); diff --git a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js index 67ae5705b2..99be54d4a9 100644 --- a/test/api/v3/integration/groups/GET-groups_groupId_members.test.js +++ b/test/api/v3/integration/groups/GET-groups_groupId_members.test.js @@ -142,8 +142,8 @@ describe('GET /groups/:groupId/members', () => { }); }); - // @TODO times out too many times (when it takes more than 8s) - xit('supports using req.query.lastId to get more members', async () => { + it('supports using req.query.lastId to get more members', async function () { + this.timeout(30000); // @TODO: times out after 8 seconds let leader = await generateUser({balance: 4}); let group = await generateGroup(leader, {type: 'guild', privacy: 'public', name: generateUUID()}); diff --git a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js index f25038002c..8ff7e5d7c3 100644 --- a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js +++ b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js @@ -443,8 +443,7 @@ describe('Purchasing a group plan for group', () => { await api.createSubscription(data); - let updatedUser = await User.findById(recipient._id).exec(); - + const updatedUser = await User.findById(recipient._id).exec(); expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3); }); diff --git a/test/api/v3/unit/libs/payments/stripe/checkout.test.js b/test/api/v3/unit/libs/payments/stripe/checkout.test.js index fca369540c..faba2dd5ef 100644 --- a/test/api/v3/unit/libs/payments/stripe/checkout.test.js +++ b/test/api/v3/unit/libs/payments/stripe/checkout.test.js @@ -37,6 +37,22 @@ describe('checkout', () => { payments.createSubscription.restore(); }); + it('should error if there is no token', async () => { + await expect(stripePayments.checkout({ + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + message: 'Missing req.body.id', + name: 'BadRequest', + }); + }); + it('should error if gem amount is too low', async () => { let receivingUser = new User(); receivingUser.save(); @@ -64,7 +80,6 @@ describe('checkout', () => { }); }); - it('should error if user cannot get gems', async () => { gift = undefined; sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); diff --git a/test/common/ops/buy/buySpecialSpell.js b/test/common/ops/buy/buySpell.js similarity index 90% rename from test/common/ops/buy/buySpecialSpell.js rename to test/common/ops/buy/buySpell.js index bd4b0cf22c..02d0bda6a4 100644 --- a/test/common/ops/buy/buySpecialSpell.js +++ b/test/common/ops/buy/buySpell.js @@ -1,4 +1,4 @@ -import buySpecialSpell from '../../../../website/common/script/ops/buy/buySpecialSpell'; +import {BuySpellOperation} from '../../../../website/common/script/ops/buy/buySpell'; import { BadRequest, NotFound, @@ -15,6 +15,11 @@ describe('shared.ops.buySpecialSpell', () => { let user; let analytics = {track () {}}; + function buySpecialSpell (_user, _req, _analytics) { + const buyOp = new BuySpellOperation(_user, _req, _analytics); + + return buyOp.purchase(); + } beforeEach(() => { user = generateUser(); sinon.stub(analytics, 'track'); diff --git a/website/client/app.vue b/website/client/app.vue index 5c87315d0e..8471bb1adb 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -44,8 +44,6 @@ div router-view app-footer audio#sound(autoplay, ref="sound") - source#oggSource(type="audio/ogg", :src="sound.oggSource") - source#mp3Source(type="audio/mp3", :src="sound.mp3Source") diff --git a/website/client/components/groups/group.vue b/website/client/components/groups/group.vue index 3f61a5033f..28436ff084 100644 --- a/website/client/components/groups/group.vue +++ b/website/client/components/groups/group.vue @@ -27,25 +27,16 @@ .svg-icon.gem(v-html="icons.gem") span.number {{group.balance * 4}} div(v-once) {{ $t('guildBank') }} - .row.chat-row - .col-12 - h3(v-once) {{ $t('chat') }} - .row.new-message-row - textarea(:placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')", v-model='newMessage', @keydown='updateCarretPosition', @keyup.ctrl.enter='sendMessageShortcut()', @paste='disableMessageSendShortcut()') - autocomplete(:text='newMessage', v-on:select="selectedAutocomplete", :coords='coords', :chat='group.chat') - .row.chat-actions - .col-6.chat-receive-actions - button.btn.btn-secondary.float-left.fetch(v-once, @click='fetchRecentMessages()') {{ $t('fetchRecentMessages') }} - button.btn.btn-secondary.float-left(v-once, @click='reverseChat()') {{ $t('reverseChat') }} - .col-6.chat-send-actions - button.btn.btn-secondary.send-chat.float-right(v-once, @click='sendMessage()') {{ $t('send') }} - community-guidelines + chat( + :label="$t('chat')", + :group="group", + :placeholder="!isParty ? $t('chatPlaceholder') : $t('partyChatPlaceholder')", + @fetchRecentMessages="fetchRecentMessages()" + ) + template(slot="additionRow") .row(v-if='showNoNotificationsMessage') .col-12.no-notifications | {{$t('groupNoNotifications')}} - .row - .col-12.hr - chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name') .col-12.col-sm-4.sidebar .row(:class='{"guild-background": !isParty}') .col-12 @@ -260,7 +251,6 @@ diff --git a/website/client/components/header/index.vue b/website/client/components/header/index.vue index fc65f70051..5e032622a7 100644 --- a/website/client/components/header/index.vue +++ b/website/client/components/header/index.vue @@ -55,7 +55,6 @@ div } #app-header { - margin-top: 56px; padding-left: 24px; padding-top: 9px; padding-bottom: 8px; diff --git a/website/client/components/header/menu.vue b/website/client/components/header/menu.vue index 22800b5dc9..dab97c4348 100644 --- a/website/client/components/header/menu.vue +++ b/website/client/components/header/menu.vue @@ -3,60 +3,60 @@ div inbox-modal creator-intro profile - b-navbar.navbar.navbar-inverse.fixed-top.navbar-expand-lg(type="dark", :class="navbarZIndexClass") - .navbar-header + b-navbar.topbar.navbar-inverse.static-top.navbar-expand-lg(type="dark", :class="navbarZIndexClass") + b-navbar-brand.brand .logo.svg-icon.d-none.d-xl-block(v-html="icons.logo") - .svg-icon.gryphon.d-md-block.d-none.d-xl-none - .svg-icon.gryphon.d-sm-block.d-lg-none.d-md-none - b-nav-toggle(target='nav_collapse') - b-collapse#nav_collapse.collapse.navbar-collapse.justify-content-between.flex-wrap(is-nav) - .ul.navbar-nav - router-link.nav-item(tag="li", :to="{name: 'tasks'}", exact) - a.nav-link(v-once) {{ $t('tasks') }} - router-link.nav-item.dropdown(tag="li", :to="{name: 'items'}", :class="{'active': $route.path.startsWith('/inventory')}") - a.nav-link(v-once) {{ $t('inventory') }} - .dropdown-menu - router-link.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }} - router-link.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }} - router-link.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }} - router-link.nav-item.dropdown(tag="li", :to="{name: 'market'}", :class="{'active': $route.path.startsWith('/shop')}") - a.nav-link(v-once) {{ $t('shops') }} - .dropdown-menu - router-link.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }} - router-link.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }} - router-link.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }} - router-link.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }} - router-link.nav-item(tag="li", :to="{name: 'party'}", v-if='this.user.party._id') - a.nav-link(v-once) {{ $t('party') }} - .nav-item(@click='openPartyModal()', v-if='!this.user.party._id') - a.nav-link(v-once) {{ $t('party') }} - router-link.nav-item.dropdown(tag="li", :to="{name: 'tavern'}", :class="{'active': $route.path.startsWith('/guilds')}") - a.nav-link(v-once) {{ $t('guilds') }} - .dropdown-menu - router-link.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }} - router-link.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }} - router-link.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }} - router-link.nav-item.dropdown(tag="li", :to="{name: 'groupPlan'}", :class="{'active': $route.path.startsWith('/group-plans')}") - a.nav-link(v-once) {{ $t('group') }} - .dropdown-menu - router-link.dropdown-item(v-for='group in groupPlans', :key='group._id', :to="{name: 'groupPlanDetailTaskInformation', params: {groupId: group._id}}") {{ group.name }} - router-link.nav-item.dropdown(tag="li", :to="{name: 'myChallenges'}", :class="{'active': $route.path.startsWith('/challenges')}") - a.nav-link(v-once) {{ $t('challenges') }} - .dropdown-menu - router-link.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }} - router-link.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }} - router-link.nav-item.dropdown(tag="li", :class="{'active': $route.path.startsWith('/help')}", :to="{name: 'faq'}") - a.nav-link(v-once) {{ $t('help') }} - .dropdown-menu - router-link.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }} - router-link.dropdown-item(:to="{name: 'overview'}") {{ $t('overview') }} - router-link.dropdown-item(to="/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac") {{ $t('reportBug') }} - router-link.dropdown-item(to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a") {{ $t('askAQuestion') }} + .svg-icon.gryphon.d-xs-block.d-xl-none + b-nav-toggle(target='menu_collapse').menu-toggle + .quick-menu.mobile-only.form-inline + a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')") + .top-menu-icon.svg-icon(v-html="icons.sync") + notification-menu.item-with-icon + user-dropdown.item-with-icon + b-collapse#menu_collapse.collapse.navbar-collapse + b-navbar-nav.menu-list + b-nav-item.topbar-item(tag="li", :to="{name: 'tasks'}", exact) {{ $t('tasks') }} + li.topbar-item(:class="{'active': $route.path.startsWith('/inventory')}") + router-link.nav-link(:to="{name: 'items'}") {{ $t('inventory') }} + .topbar-dropdown + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'items'}", exact) {{ $t('items') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'equipment'}") {{ $t('equipment') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'stable'}") {{ $t('stable') }} + li.topbar-item(:class="{'active': $route.path.startsWith('/shop')}") + router-link.nav-link(:to="{name: 'market'}") {{ $t('shops') }} + .topbar-dropdown + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'market'}", exact) {{ $t('market') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'quests'}") {{ $t('quests') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'seasonal'}") {{ $t('titleSeasonalShop') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'time'}") {{ $t('titleTimeTravelers') }} + b-nav-item.topbar-item(tag="li", :to="{name: 'party'}", v-if='this.user.party._id') {{ $t('party') }} + b-nav-item.topbar-item(@click='openPartyModal()', v-if='!this.user.party._id') {{ $t('party') }} + li.topbar-item(:class="{'active': $route.path.startsWith('/guilds')}") + router-link.nav-link(:to="{name: 'tavern'}") {{ $t('guilds') }} + .topbar-dropdown + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'tavern'}") {{ $t('tavern') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myGuilds'}") {{ $t('myGuilds') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'guildsDiscovery'}") {{ $t('guildsDiscovery') }} + li.topbar-item(:class="{'active': $route.path.startsWith('/group-plans')}") + router-link.nav-link(:to="{name: 'groupPlan'}") {{ $t('group') }} + .topbar-dropdown + router-link.topbar-dropdown-item.dropdown-item(v-for='group in groupPlans', :key='group._id', :to="{name: 'groupPlanDetailTaskInformation', params: {groupId: group._id}}") {{ group.name }} + li.topbar-item(:class="{'active': $route.path.startsWith('/challenges')}") + router-link.nav-link(:to="{name: 'myChallenges'}") {{ $t('challenges') }} + .topbar-dropdown + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'myChallenges'}") {{ $t('myChallenges') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'findChallenges'}") {{ $t('findChallenges') }} + li.topbar-item(:class="{'active': $route.path.startsWith('/help')}") + router-link.nav-link(:to="{name: 'faq'}") {{ $t('help') }} + .topbar-dropdown + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'faq'}") {{ $t('faq') }} + router-link.topbar-dropdown-item.dropdown-item(:to="{name: 'overview'}") {{ $t('overview') }} + router-link.topbar-dropdown-item.dropdown-item(to="/groups/guild/a29da26b-37de-4a71-b0c6-48e72a900dac") {{ $t('reportBug') }} + router-link.topbar-dropdown-item.dropdown-item(to="/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a") {{ $t('askAQuestion') }} a.dropdown-item(href="https://trello.com/c/odmhIqyW/440-read-first-table-of-contents", target='_blank') {{ $t('requestAF') }} a.dropdown-item(href="http://habitica.wikia.com/wiki/Contributing_to_Habitica", target='_blank') {{ $t('contributing') }} a.dropdown-item(href="http://habitica.wikia.com/wiki/Habitica_Wiki", target='_blank') {{ $t('wiki') }} - a.dropdown-item(@click='modForm()') {{ $t('contactForm') }} - .user-menu.d-flex.align-items-center + .currency-tray.form-inline .item-with-icon(v-if="userHourglasses > 0") .top-menu-icon.svg-icon(v-html="icons.hourglasses", v-b-tooltip.hover.bottom="$t('mysticHourglassesTooltip')") span {{ userHourglasses }} @@ -66,6 +66,7 @@ div .item-with-icon.gold .top-menu-icon.svg-icon(v-html="icons.gold", v-b-tooltip.hover.bottom="$t('gold')") span {{Math.floor(user.stats.gp * 100) / 100}} + .form-inline.desktop-only a.item-with-icon(@click="sync", v-b-tooltip.hover.bottom="$t('sync')") .top-menu-icon.svg-icon(v-html="icons.sync") notification-menu.item-with-icon @@ -76,16 +77,6 @@ div @import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/utils.scss'; - @media only screen and (max-width: 1305px) { - .nav-link { - padding: .8em 1em !important; - } - - .navbar-header { - margin-right: 5px !important; - } - } - @media only screen and (max-width: 1200px) { .gryphon { background-image: url('~assets/images/melior@3x.png'); @@ -94,57 +85,107 @@ div background-size: cover; color: $white; margin: 0 auto; - } - .svg-icon.gryphon.d-sm-block { - position: absolute; - left: calc(50% - 30px); - top: 1em; - } - - .nav-item .nav-link { + .topbar-item { font-size: 14px !important; - padding: 16px 12px !important; } } - @media only screen and (max-width: 990px) { - #nav_collapse { - margin-top: 0.6em; - flex-direction: row !important; - max-height: 650px; - overflow: auto; + @media only screen and (min-width: 992px) { + .mobile-only { + display: none !important; } - .navbar-nav { - width: 100%; - background: $purple-100; - } + .topbar { + max-height: 56px; - .user-menu { - flex-direction: column !important; - align-items: left !important; - background: $purple-100; - width: 100%; + .currency-tray { + margin-left: auto; + } - .item-with-icon { - width: 100%; - padding-bottom: 1em; + .topbar-item { + padding-top: 5px; + height: 56px; + + &.active:not(:hover) { + box-shadow: 0px -4px 0px $purple-300 inset; + } + } + + .topbar-dropdown { + position: absolute; } } } - #nav_collapse { - display: flex; + @media only screen and (max-width: 992px) { + .brand { + margin: 0; + } + + .gryphon { + position: absolute; + left: calc(50% - 30px); + top: 10px; + } + + #menu_collapse { + margin: 0.6em -16px -8px; + overflow: auto; + flex-direction: column; + background-color: $purple-100; + + .menu-list { + width: 100%; + order: 1; + text-align: center; + + .topbar-dropdown-item { + background: #432874; + border-bottom: #6133b4 solid 1px; + } + + .topbar-item { + &.active { + background: #6133b4; + } + + background: #4f2a93; + border-bottom: #6133b4 solid 1px; + } + } + } + + .currency-tray { + justify-content: center; + min-height: 40px; + background: #271b3d; + width: 100%; + } + + .desktop-only { + display: none !important; + } } - nav.navbar { - background: $purple-100 url(~assets/svg/for-css/bits.svg) right no-repeat; - padding-left: 25px; - padding-right: 12.5px; - height: 56px; + .menu-toggle { + border: none; + } + + #menu_collapse { + display: flex; + justify-content: space-between; + } + + .topbar { + background: $purple-100 url(~assets/svg/for-css/bits.svg) right top no-repeat; + min-height: 56px; box-shadow: 0 1px 2px 0 rgba($black, 0.24); + + a { + color: white !important; + } } .navbar-z-index { @@ -154,76 +195,75 @@ div &-modal { z-index: 1035; + z-index: 1042; // To stay above snakbar notifications and modals } } - .navbar-header { - margin-right: 48px; - - .logo { - width: 128px; - height: 28px; - } + .logo { + padding-left: 10px; + width: 128px; + height: 28px; } - .nav-item { - .nav-link { - font-size: 16px; - color: $white !important; - font-weight: bold; - line-height: 1.5; - padding: 16px 20px; - transition: none; + .quick-menu { + display: flex; + margin-left: auto; + } + + .currency-tray { + display: flex; + } + + .topbar-item { + font-size: 16px; + color: $white !important; + font-weight: bold; + transition: none; + + .topbar-dropdown { + display: none; // Display is set to block on hover. + } + + >a { + padding: .8em 1em !important; } &:hover { - .nav-link { - color: $white !important; + color: $white !important; + background: $purple-200; + + .topbar-dropdown { + display: block; // Open drop-down on hover. + margin-top: 0; // Remove gap between navbar and drop-down. background: $purple-200; - } - } + border-radius: 0px; + border: none; + box-shadow: none; + padding: 0px; - &.active:not(:hover) { - .nav-link { - box-shadow: 0px -4px 0px $purple-300 inset; - } - } - } + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; - // Make the dropdown menu open on hover - .dropdown:hover .dropdown-menu { - display: block; - margin-top: 0; // remove the gap so it doesn't close - } + .topbar-dropdown-item { + font-size: 16px; + box-shadow: none; + color: $white; + border: none; + line-height: 1.5; + display: list-item; - .dropdown-menu { - background: $purple-200; - border-radius: 0px; - border: none; - box-shadow: none; - padding: 0px; + &.active { + background: $purple-300; + } - border-bottom-right-radius: 5px; - border-bottom-left-radius: 5px; + &:hover { + background: $purple-300; - .dropdown-item { - font-size: 16px; - box-shadow: none; - color: $white; - border: none; - line-height: 1.5; - - &.active { - background: $purple-300; - } - - &:hover { - background: $purple-300; - color: $white; - - &:last-child { - border-bottom-right-radius: 5px; - border-bottom-left-radius: 5px; + &:last-child { + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; + } + } } } } @@ -380,6 +420,7 @@ export default { this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true}); }, + }, }; diff --git a/website/client/components/inventory/stable/index.vue b/website/client/components/inventory/stable/index.vue index 7298f60bc7..641e091b1d 100644 --- a/website/client/components/inventory/stable/index.vue +++ b/website/client/components/inventory/stable/index.vue @@ -935,10 +935,22 @@ } }, async feedAction (petKey, foodKey) { - const result = await this.$store.dispatch('common:feed', {pet: petKey, food: foodKey}); - if (result.message) this.text(result.message); - if (this.user.preferences.suppressModals.raisePet) return; - if (this.user.items.pets[petKey] === -1) this.$root.$emit('habitica::mount-raised', petKey); + try { + const result = await this.$store.dispatch('common:feed', {pet: petKey, food: foodKey}); + + if (result.message) this.text(result.message); + if (this.user.preferences.suppressModals.raisePet) return; + if (this.user.items.pets[petKey] === -1) this.$root.$emit('habitica::mount-raised', petKey); + } catch (e) { + const errorMessage = e.message || e; + + this.$store.dispatch('snackbars:add', { + title: 'Habitica', + text: errorMessage, + type: 'error', + timeout: true, + }); + } }, closeHatchPetDialog () { this.$root.$emit('bv::hide::modal', 'hatching-modal'); diff --git a/website/client/components/settings/site.vue b/website/client/components/settings/site.vue index 5cddf7bc33..dfd25f4f6d 100644 --- a/website/client/components/settings/site.vue +++ b/website/client/components/settings/site.vue @@ -345,7 +345,7 @@ export default { const newLang = e.target.value; this.user.preferences.language = newLang; await this.set('language'); - window.location.href = '/'; + setTimeout(() => window.location.reload(true)); }, async changeUser (attribute, updates) { await axios.put(`/api/v3/user/auth/update-${attribute}`, updates); diff --git a/website/client/components/snackbars/notification.vue b/website/client/components/snackbars/notification.vue index ec544e6c93..256806f6dc 100644 --- a/website/client/components/snackbars/notification.vue +++ b/website/client/components/snackbars/notification.vue @@ -130,7 +130,6 @@ export default { }; }, created () { - // @TODO the notifications always close even if timeout is false let timeout = this.notification.hasOwnProperty('timeout') ? this.notification.timeout : true; if (timeout) { let delay = this.notification.delay || 1500; diff --git a/website/client/components/static/home.vue b/website/client/components/static/home.vue index d516883663..3dd2bc4e85 100644 --- a/website/client/components/static/home.vue +++ b/website/client/components/static/home.vue @@ -7,11 +7,11 @@ #intro-signup.purple-1 .container .row - .col-12.col-sm-6.col-md-6.col-lg-6 + .col-12.col-md-6.col-lg-6 img(src='~assets/images/home/home-main@3x.png', width='357px') h1 {{$t('motivateYourself')}} p.section-main {{$t('timeToGetThingsDone', {userCountInMillions})}} - .col-12.col-sm-6.col-md-6.col-lg-6 + .col-12.col-md-6.col-lg-6 h3.text-center {{$t('singUpForFree')}} div.text-center button.social-button(@click='socialAuth("facebook")') @@ -42,15 +42,15 @@ h2 {{$t('gamifyYourLife')}} p.section-main {{$t('aboutHabitica')}} .row - .col-12.col-sm-4 + .col-12.col-md-4 img.track-habits(src='~assets/images/home/track-habits@3x.png', width='354px', height='228px') strong {{$t('trackYourGoals')}} p {{$t('trackYourGoalsDesc')}} - .col-12.col-sm-4 + .col-12.col-md-4 img(src='~assets/images/home/earn-rewards@3x.png', width='316px', height='244px') strong {{$t('earnRewards')}} p {{$t('earnRewardsDesc')}} - .col-12.col-sm-4 + .col-12.col-md-4 img(src='~assets/images/home/battle-monsters@3x.png', width='303px', height='244px') strong {{$t('battleMonsters')}} p {{$t('battleMonstersDesc')}} @@ -83,9 +83,9 @@ #level-up-anywhere.purple-3 .container .row - .col-12.col-sm-6.col-md-6.col-lg-6 + .col-12.col-md-6.col-lg-6 .iphones - .col-12.col-sm-6.col-md-6.col-lg-6.text-column + .col-12.col-md-6.col-lg-6.text-column h2 {{ $t('levelUpAnywhere') }} p {{ $t('levelUpAnywhereDesc') }} a.app.svg-icon(v-html='icons.googlePlay', href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', target='_blank') @@ -345,6 +345,9 @@ text-align: center; img { + max-width: 100%; + display: block; + margin: 0 auto; margin-top: 1em; margin-bottom: 1.5em; } @@ -387,6 +390,8 @@ .iphones { width: 436px; height: 520px; + max-width: 100%; + background-repeat: no-repeat; background-size: 100%; background-image: url('~assets/images/home/mobile-preview@3x.png'); } @@ -658,13 +663,10 @@ await hello(network).logout(); } catch (e) {} // eslint-disable-line - - const url = window.location.href; - - let auth = await hello(network).login({ + const auth = await hello(network).login({ scope: 'email', // explicitly pass the redirect url or it might redirect to /home - redirect_uri: url, // eslint-disable-line camelcase + redirect_uri: '', // eslint-disable-line camelcase }); await this.$store.dispatch('auth:socialAuth', { diff --git a/website/client/components/tasks/column.vue b/website/client/components/tasks/column.vue index 5092cd2c1f..565b7d25be 100644 --- a/website/client/components/tasks/column.vue +++ b/website/client/components/tasks/column.vue @@ -24,7 +24,7 @@ @focus="quickAddFocused = true", @blur="quickAddFocused = false", ) transition(name="quick-add-tip-slide") - .quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip')") + .quick-add-tip.small-text(v-show="quickAddFocused", v-html="$t('addMultipleTip', {taskType: $t(typeLabel)})") clear-completed-todos(v-if="activeFilter.label === 'complete2' && isUser === true") .column-background( v-if="isUser === true", @@ -370,7 +370,7 @@ export default { let rewards = inAppRewards(this.user); // Add season rewards if user is affected - // @TODO: Add buff coniditional + // @TODO: Add buff conditional const seasonalSkills = { snowball: 'salt', spookySparkles: 'opaquePotion', diff --git a/website/client/components/ui/drawerHeaderTabs.vue b/website/client/components/ui/drawerHeaderTabs.vue index 4952b112f6..ec0738bf97 100644 --- a/website/client/components/ui/drawerHeaderTabs.vue +++ b/website/client/components/ui/drawerHeaderTabs.vue @@ -1,44 +1,57 @@ diff --git a/website/client/libs/staffList.js b/website/client/libs/staffList.js new file mode 100644 index 0000000000..96eb262fc3 --- /dev/null +++ b/website/client/libs/staffList.js @@ -0,0 +1,97 @@ +export default [ + { + name: 'beffymaroo', + type: 'Staff', + uuid: '9fe7183a-4b79-4c15-9629-a1aee3873390', + }, + // { + // name: 'lefnire', + // type: 'Staff', + // uuid: '00000000-0000-4000-9000-000000000000', + // }, + { + name: 'Lemoness', + type: 'Staff', + uuid: '7bde7864-ebc5-4ee2-a4b7-1070d464cdb0', + }, + { + name: 'paglias', + type: 'Staff', + uuid: 'ed4c688c-6652-4a92-9d03-a5a79844174a', + }, + { + name: 'redphoenix', + type: 'Staff', + uuid: 'cb46ad54-8c78-4dbc-a8ed-4e3185b2b3ff', + }, + { + name: 'SabreCat', + type: 'Staff', + uuid: '7f14ed62-5408-4e1b-be83-ada62d504931', + }, + { + name: 'TheHollidayInn', + type: 'Staff', + uuid: '206039c6-24e4-4b9f-8a31-61cbb9aa3f66', + }, + { + name: 'viirus', + type: 'Staff', + uuid: 'a327d7e0-1c2e-41be-9193-7b30b484413f', + }, + { + name: 'It\'s Bailey', + type: 'Moderator', + uuid: '9da65443-ed43-4c21-804f-d260c1361596', + }, + { + name: 'Alys', + type: 'Moderator', + uuid: 'd904bd62-da08-416b-a816-ba797c9ee265', + }, + { + name: 'Blade', + type: 'Moderator', + uuid: '75f270e8-c5db-4722-a5e6-a83f1b23f76b', + }, + { + name: 'Breadstrings', + type: 'Moderator', + uuid: '3b675c0e-d7a6-440c-8687-bc67cd0bf4e9', + }, + { + name: 'Cantras', + type: 'Moderator', + uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d', + }, + // { + // name: 'Daniel the Bard', + // type: 'Moderator', + // uuid: '1f7c4a74-03a3-4b2c-b015-112d0acbd593', + // }, + { + name: 'deilann 5.0.5b', + type: 'Moderator', + uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec', + }, + { + name: 'Dewines', + type: 'Moderator', + uuid: '262a7afb-6b57-4d81-88e0-80d2e9f6cbdc', + }, + { + name: 'Fox_town', + type: 'Moderator', + uuid: 'a05f0152-d66b-4ef1-93ac-4adb195d0031', + }, + { + name: 'Megan', + type: 'Moderator', + uuid: '73e5125c-2c87-4004-8ccd-972aeac4f17a', + }, + { + name: 'shanaqui', + type: 'Moderator', + uuid: 'bb089388-28ae-4e42-a8fa-f0c2bfb6f779', + }, +]; diff --git a/website/client/mixins/spells.js b/website/client/mixins/spells.js index 4c40934630..f1d776c95d 100644 --- a/website/client/mixins/spells.js +++ b/website/client/mixins/spells.js @@ -133,23 +133,6 @@ export default { this.markdown(msg); // @TODO: mardown directive? - - // If using mpheal and there are other mages in the party, show extra notification - if (type === 'party' && spell.key === 'mpheal') { - // Counting mages - let magesCount = 0; - for (let i = 0; i < target.length; i++) { - if (target[i].stats.class === 'wizard') { - magesCount++; - } - } - // If there are mages, show message telling that the mpheal don't work on other mages - // The count must be bigger than 1 because the user casting the spell is a mage - if (magesCount > 1) { - this.markdown(this.$t('spellWizardNoEthOnMage')); - } - } - if (!beforeQuestProgress) return; let questProgress = this.questProgress() - beforeQuestProgress; if (questProgress > 0) { diff --git a/website/client/store/actions/shops.js b/website/client/store/actions/shops.js index 56bbf4ac9b..1900533e1a 100644 --- a/website/client/store/actions/shops.js +++ b/website/client/store/actions/shops.js @@ -1,7 +1,6 @@ import axios from 'axios'; import buyOp from 'common/script/ops/buy/buy'; import content from 'common/script/content/index'; -import purchaseOp from 'common/script/ops/buy/purchaseWithSpell'; import hourglassPurchaseOp from 'common/script/ops/buy/hourglassPurchase'; import sellOp from 'common/script/ops/sell'; import unlockOp from 'common/script/ops/unlock'; @@ -91,7 +90,7 @@ async function buyArmoire (store, params) { export function purchase (store, params) { const quantity = params.quantity || 1; const user = store.state.user.data; - let opResult = purchaseOp(user, {params, quantity}); + let opResult = buyOp(user, {params, quantity}); return { result: opResult, diff --git a/website/common/locales/en/settings.json b/website/common/locales/en/settings.json index 7fb1143a1e..dcee6c41a6 100644 --- a/website/common/locales/en/settings.json +++ b/website/common/locales/en/settings.json @@ -11,8 +11,8 @@ "dailyDueDefaultView": "Set Dailies default to 'due' tab", "dailyDueDefaultViewPop": "With this option set, the Dailies tasks will default to 'due' instead of 'all'", "reverseChatOrder": "Show chat messages in reverse order", - "startAdvCollapsed": "Advanced Options in tasks start collapsed", - "startAdvCollapsedPop": "With this option set, Advanced Options will be hidden when you first open a task for editing.", + "startAdvCollapsed": "Advanced Settings in tasks start collapsed", + "startAdvCollapsedPop": "With this option set, Advanced Settings will be hidden when you first open a task for editing.", "dontShowAgain": "Don't show this again", "suppressLevelUpModal": "Don't show popup when gaining a level", "suppressHatchPetModal": "Don't show popup when hatching a pet", @@ -85,7 +85,7 @@ "resetComplete": "Reset complete!", "fixValues": "Fix Values", "fixValuesText1": "If you've encountered a bug or made a mistake that unfairly changed your character (damage you shouldn't have taken, Gold you didn't really earn, etc.), you can manually correct your numbers here. Yes, this makes it possible to cheat: use this feature wisely, or you'll sabotage your own habit-building!", - "fixValuesText2": "Note that you cannot restore Streaks on individual tasks here. To do that, edit the Daily and go to Advanced Options, where you will find a Restore Streak field.", + "fixValuesText2": "Note that you cannot restore Streaks on individual tasks here. To do that, edit the Daily and go to Advanced Settings, where you will find a Restore Streak field.", "disabledWinterEvent": "Disabled during Winter Wonderland Event Pt.4 (since the rewards are gold-purchaseable).", "fix21Streaks": "21-Day Streaks", "discardChanges": "Discard Changes", diff --git a/website/common/locales/en/spells.json b/website/common/locales/en/spells.json index d67816a19a..850b49dd85 100644 --- a/website/common/locales/en/spells.json +++ b/website/common/locales/en/spells.json @@ -4,7 +4,6 @@ "spellWizardMPHealText": "Ethereal Surge", "spellWizardMPHealNotes": "You sacrifice Mana so the rest of your Party, except Mages, gains MP! (Based on: INT)", - "spellWizardNoEthOnMage": "Your Skill backfires when mixed with another's magic. Only non-Mages gain MP.", "spellWizardEarthText": "Earthquake", "spellWizardEarthNotes": "Your mental power shakes the earth and buffs your Party's Intelligence! (Based on: Unbuffed INT)", diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index afaba0032a..50ba001de6 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -5,7 +5,7 @@ "sureDeleteCompletedTodos": "Are you sure you want to delete your completed To-Dos?", "lotOfToDos": "Your most recent 30 completed To-Dos are shown here. You can see older completed To-Dos from Data > Data Display Tool or Data > Export Data > User Data.", "deleteToDosExplanation": "If you click the button below, all of your completed To-Dos and archived To-Dos will be permanently deleted, except for To-Dos from active challenges and Group Plans. Export them first if you want to keep a record of them.", - "addMultipleTip": "Tip: To add multiple Tasks, separate each one using a line break (Shift + Enter) and then press \"Enter.\"", + "addMultipleTip": "Tip: To add multiple <%= taskType %>, separate each one using a line break (Shift + Enter) and then press \"Enter.\"", "addsingle": "Add Single", "addATask": "Add a <%= type %>", "editATask": "Edit a <%= type %>", @@ -210,7 +210,6 @@ "yesterDailiesDescription": "If this setting is applied, Habitica will ask you if you meant to leave the Daily undone before calculating and applying damage to your avatar. This can protect you against unintentional damage.", "repeatDayError": "Please ensure that you have at least one day of the week selected.", "searchTasks": "Search titles and descriptions...", - "repeatDayError": "Please ensure that you have at least one day of the week selected.", "sessionOutdated": "Your session is outdated. Please refresh or sync.", "errorTemporaryItem": "This item is temporary and cannot be pinned." } diff --git a/website/common/script/ops/buy/buy.js b/website/common/script/ops/buy/buy.js index e3cb62bdca..9bcee0706e 100644 --- a/website/common/script/ops/buy/buy.js +++ b/website/common/script/ops/buy/buy.js @@ -7,7 +7,7 @@ import {BuyHealthPotionOperation} from './buyHealthPotion'; import {BuyMarketGearOperation} from './buyMarketGear'; import buyMysterySet from './buyMysterySet'; import {BuyQuestWithGoldOperation} from './buyQuest'; -import buySpecialSpell from './buySpecialSpell'; +import {BuySpellOperation} from './buySpell'; import purchaseOp from './purchase'; import hourglassPurchase from './hourglassPurchase'; import errorMessage from '../../libs/errorMessage'; @@ -70,9 +70,12 @@ module.exports = function buy (user, req = {}, analytics) { buyRes = buyOp.purchase(); break; } - case 'special': - buyRes = buySpecialSpell(user, req, analytics); + case 'special': { + const buyOp = new BuySpellOperation(user, req, analytics); + + buyRes = buyOp.purchase(); break; + } default: { const buyOp = new BuyMarketGearOperation(user, req, analytics); diff --git a/website/common/script/ops/buy/buyQuest.js b/website/common/script/ops/buy/buyQuest.js index b7e9f783b9..1fd60949fd 100644 --- a/website/common/script/ops/buy/buyQuest.js +++ b/website/common/script/ops/buy/buyQuest.js @@ -25,6 +25,10 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation { user.achievements.quests.taskwoodsTerror3; } + getItemKey () { + return this.key; + } + getItemValue (item) { return item.goldValue; } @@ -61,13 +65,4 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation { }), ]; } - - analyticsData () { - return { - itemKey: this.key, - itemType: 'Market', - acquireMethod: 'Gold', - goldCost: this.getItemValue(this.item.goldValue), - }; - } } diff --git a/website/common/script/ops/buy/buySpecialSpell.js b/website/common/script/ops/buy/buySpecialSpell.js deleted file mode 100644 index d63638acfd..0000000000 --- a/website/common/script/ops/buy/buySpecialSpell.js +++ /dev/null @@ -1,48 +0,0 @@ -import i18n from '../../i18n'; -import content from '../../content/index'; -import get from 'lodash/get'; -import pick from 'lodash/pick'; -import splitWhitespace from '../../libs/splitWhitespace'; -import { - BadRequest, - NotAuthorized, - NotFound, -} from '../../libs/errors'; -import errorMessage from '../../libs/errorMessage'; - -module.exports = function buySpecialSpell (user, req = {}, analytics) { - let key = get(req, 'params.key'); - let quantity = req.quantity || 1; - - if (!key) throw new BadRequest(errorMessage('missingKeyParam')); - - let item = content.special[key]; - if (!item) throw new NotFound(errorMessage('spellNotFound', {spellId: key})); - - if (user.stats.gp < item.value * quantity) { - throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language)); - } - user.stats.gp -= item.value * quantity; - - user.items.special[key] += quantity; - - if (analytics) { - analytics.track('acquire item', { - uuid: user._id, - itemKey: item.key, - itemType: 'Market', - goldCost: item.goldValue, - quantityPurchased: quantity, - acquireMethod: 'Gold', - category: 'behavior', - headers: req.headers, - }); - } - - return [ - pick(user, splitWhitespace('items stats')), - i18n.t('messageBought', { - itemText: item.text(req.language), - }, req.language), - ]; -}; diff --git a/website/common/script/ops/buy/buySpell.js b/website/common/script/ops/buy/buySpell.js new file mode 100644 index 0000000000..2a26d0defb --- /dev/null +++ b/website/common/script/ops/buy/buySpell.js @@ -0,0 +1,47 @@ +import content from '../../content/index'; +import get from 'lodash/get'; +import pick from 'lodash/pick'; +import splitWhitespace from '../../libs/splitWhitespace'; +import { + BadRequest, + NotFound, +} from '../../libs/errors'; +import {AbstractGoldItemOperation} from './abstractBuyOperation'; +import errorMessage from '../../libs/errorMessage'; + +export class BuySpellOperation extends AbstractGoldItemOperation { + constructor (user, req, analytics) { + super(user, req, analytics); + } + + getItemKey () { + return this.key; + } + + multiplePurchaseAllowed () { + return true; + } + + extractAndValidateParams (user, req) { + let key = this.key = get(req, 'params.key'); + if (!key) throw new BadRequest(errorMessage('missingKeyParam')); + + let item = content.special[key]; + if (!item) throw new NotFound(errorMessage('spellNotFound', {spellId: key})); + + this.canUserPurchase(user, item); + } + + executeChanges (user, item, req) { + user.items.special[item.key] += this.quantity; + + this.subtractCurrency(user, item, this.quantity); + + return [ + pick(user, splitWhitespace('items stats')), + this.i18n('messageBought', { + itemText: item.text(req.language), + }), + ]; + } +} diff --git a/website/common/script/ops/buy/purchaseWithSpell.js b/website/common/script/ops/buy/purchaseWithSpell.js deleted file mode 100644 index 7f0a087190..0000000000 --- a/website/common/script/ops/buy/purchaseWithSpell.js +++ /dev/null @@ -1,12 +0,0 @@ -import buy from './buy'; -import get from 'lodash/get'; - -module.exports = function purchaseWithSpell (user, req = {}, analytics) { - const type = get(req.params, 'type'); - - if (type === 'spells') { - req.type = 'special'; - } - - return buy(user, req, analytics); -}; diff --git a/website/server/libs/payments/stripe.js b/website/server/libs/payments/stripe.js index c2c218682d..ff7483a6f5 100644 --- a/website/server/libs/payments/stripe.js +++ b/website/server/libs/payments/stripe.js @@ -1,7 +1,5 @@ -import stripeModule from 'stripe'; -import nconf from 'nconf'; -import cc from 'coupon-code'; import moment from 'moment'; + import logger from '../logger'; import { BadRequest, @@ -10,38 +8,22 @@ import { } from '../errors'; import payments from './payments'; import { model as User } from '../../models/user'; -import { model as Coupon } from '../../models/coupon'; import { model as Group, basicFields as basicGroupFields, } from '../../models/group'; import shared from '../../../common'; +import stripeConstants from './stripe/constants'; +import { checkout } from './stripe/checkout'; +import { getStripeApi, setStripeApi } from './stripe/api'; -let stripe = stripeModule(nconf.get('STRIPE_API_KEY')); const i18n = shared.i18n; let api = {}; -api.constants = { - // CURRENCY_CODE: 'USD', - // SELLER_NOTE: 'Habitica Payment', - // SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', - // SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', - // STORE_NAME: 'Habitica', - // - // GIFT_TYPE_GEMS: 'gems', - // GIFT_TYPE_SUBSCRIPTION: 'subscription', - // - // METHOD_BUY_GEMS: 'buyGems', - // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', - PAYMENT_METHOD: 'Stripe', - // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', -}; - -api.setStripeApi = function setStripeApi (stripeInc) { - stripe = stripeInc; -}; +api.constants = Object.assign({}, stripeConstants); +api.setStripeApi = setStripeApi; /** * Allows for purchasing a user subscription, group subscription or gems with Stripe @@ -56,110 +38,7 @@ api.setStripeApi = function setStripeApi (stripeInc) { * @param options.headers The request headers to store on analytics * @return undefined */ -api.checkout = async function checkout (options, stripeInc) { - let { - token, - user, - gift, - sub, - groupId, - email, - headers, - coupon, - } = options; - let response; - let subscriptionId; - - // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; - if (stripeInc) stripeApi = stripeInc; - - if (!token) throw new BadRequest('Missing req.body.id'); - - if (gift) { - const member = await User.findById(gift.uuid).exec(); - gift.member = member; - } - - if (sub) { - if (sub.discount) { - if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); - coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); - if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); - } - - let customerObject = { - email, - metadata: { uuid: user._id }, - card: token, - plan: sub.key, - }; - - if (groupId) { - customerObject.quantity = sub.quantity; - const groupFields = basicGroupFields.concat(' purchased'); - const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); - const membersCount = await group.getMemberCount(); - customerObject.quantity = membersCount + sub.quantity - 1; - } - - response = await stripeApi.customers.create(customerObject); - - if (groupId) subscriptionId = response.subscriptions.data[0].id; - } else { - let amount = 500; // $5 - - if (gift) { - if (gift.type === 'subscription') { - amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; - } else { - if (gift.gems.amount <= 0) { - throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); - } - amount = `${gift.gems.amount / 4 * 100}`; - } - } - - if (!gift || gift.type === 'gems') { - const receiver = gift ? gift.member : user; - const receiverCanGetGems = await receiver.canGetGems(); - if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); - } - - response = await stripeApi.charges.create({ - amount, - currency: 'usd', - card: token, - }); - } - - if (sub) { - await payments.createSubscription({ - user, - customerId: response.id, - paymentMethod: this.constants.PAYMENT_METHOD, - sub, - headers, - groupId, - subscriptionId, - }); - } else { - let method = 'buyGems'; - let data = { - user, - customerId: response.id, - paymentMethod: this.constants.PAYMENT_METHOD, - gift, - }; - - if (gift) { - if (gift.type === 'subscription') method = 'createSubscription'; - data.paymentMethod = 'Gift'; - } - - await payments[method](data); - } -}; +api.checkout = checkout; /** * Edits a subscription created by Stripe @@ -176,7 +55,7 @@ api.editSubscription = async function editSubscription (options, stripeInc) { let customerId; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; if (groupId) { @@ -220,7 +99,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc) let customerId; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; if (groupId) { @@ -271,7 +150,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc) }; api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) { - let stripeApi = stripe; + let stripeApi = getStripeApi(); let plan = shared.content.subscriptionBlocks.group_monthly; await stripeApi.subscriptions.update( @@ -298,7 +177,7 @@ api.handleWebhooks = async function handleWebhooks (options, stripeInc) { let {requestBody} = options; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; // Verify the event by fetching it from Stripe diff --git a/website/server/libs/payments/stripe/api.js b/website/server/libs/payments/stripe/api.js new file mode 100644 index 0000000000..43a99402b1 --- /dev/null +++ b/website/server/libs/payments/stripe/api.js @@ -0,0 +1,14 @@ +import stripeModule from 'stripe'; +import nconf from 'nconf'; + +let stripe = stripeModule(nconf.get('STRIPE_API_KEY')); + +function setStripeApi (stripeInc) { + stripe = stripeInc; +} + +function getStripeApi () { + return stripe; +} + +module.exports = { getStripeApi, setStripeApi }; diff --git a/website/server/libs/payments/stripe/checkout.js b/website/server/libs/payments/stripe/checkout.js new file mode 100644 index 0000000000..8dafa3536c --- /dev/null +++ b/website/server/libs/payments/stripe/checkout.js @@ -0,0 +1,146 @@ +import cc from 'coupon-code'; + +import { getStripeApi } from './api'; +import { model as User } from '../../../models/user'; +import { model as Coupon } from '../../../models/coupon'; +import { + model as Group, + basicFields as basicGroupFields, +} from '../../../models/group'; +import shared from '../../../../common'; +import { + BadRequest, + NotAuthorized, +} from '../../errors'; +import payments from './../payments'; +import stripeConstants from './constants'; + +function getGiftAmount (gift) { + if (gift.type === 'subscription') { + return `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; + } + + if (gift.gems.amount <= 0) { + throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); + } + + return `${gift.gems.amount / 4 * 100}`; +} + +async function buyGems (gift, user, token, stripeApi) { + let amount = 500; // $5 + + if (gift) amount = getGiftAmount(gift); + + if (!gift || gift.type === 'gems') { + const receiver = gift ? gift.member : user; + const receiverCanGetGems = await receiver.canGetGems(); + if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); + } + + const response = await stripeApi.charges.create({ + amount, + currency: 'usd', + card: token, + }); + + return response; +} + +async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) { + if (sub.discount) { + if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); + coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); + if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); + } + + let customerObject = { + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + }; + + if (groupId) { + customerObject.quantity = sub.quantity; + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + const membersCount = await group.getMemberCount(); + customerObject.quantity = membersCount + sub.quantity - 1; + } + + const response = await stripeApi.customers.create(customerObject); + + let subscriptionId; + if (groupId) subscriptionId = response.subscriptions.data[0].id; + + return { subResponse: response, subId: subscriptionId }; +} + +async function applyGemPayment (user, response, gift) { + let method = 'buyGems'; + const data = { + user, + customerId: response.id, + paymentMethod: stripeConstants.PAYMENT_METHOD, + gift, + }; + + if (gift) { + if (gift.type === 'subscription') method = 'createSubscription'; + data.paymentMethod = 'Gift'; + } + + await payments[method](data); +} + +async function checkout (options, stripeInc) { + let { + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + } = options; + let response; + let subscriptionId; + + // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? + let stripeApi = getStripeApi(); + if (stripeInc) stripeApi = stripeInc; + + if (!token) throw new BadRequest('Missing req.body.id'); + + if (gift) { + const member = await User.findById(gift.uuid).exec(); + gift.member = member; + } + + if (sub) { + const { subId, subResponse } = await buySubscription(sub, coupon, email, user, token, groupId, stripeApi); + subscriptionId = subId; + response = subResponse; + } else { + response = await buyGems(gift, user, token, stripeApi); + } + + if (sub) { + await payments.createSubscription({ + user, + customerId: response.id, + paymentMethod: this.constants.PAYMENT_METHOD, + sub, + headers, + groupId, + subscriptionId, + }); + return; + } + + await applyGemPayment(user, response, gift); +} + +module.exports = { checkout }; diff --git a/website/server/libs/payments/stripe/constants.js b/website/server/libs/payments/stripe/constants.js new file mode 100644 index 0000000000..00bddb1e87 --- /dev/null +++ b/website/server/libs/payments/stripe/constants.js @@ -0,0 +1,15 @@ +module.exports = { + // CURRENCY_CODE: 'USD', + // SELLER_NOTE: 'Habitica Payment', + // SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', + // SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', + // STORE_NAME: 'Habitica', + // + // GIFT_TYPE_GEMS: 'gems', + // GIFT_TYPE_SUBSCRIPTION: 'subscription', + // + // METHOD_BUY_GEMS: 'buyGems', + // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', + PAYMENT_METHOD: 'Stripe', + // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', +}; diff --git a/website/server/middlewares/redirects.js b/website/server/middlewares/redirects.js index 9bc40cb439..0de427c2e9 100644 --- a/website/server/middlewares/redirects.js +++ b/website/server/middlewares/redirects.js @@ -1,11 +1,11 @@ import nconf from 'nconf'; +import url from 'url'; const IS_PROD = nconf.get('IS_PROD'); const IGNORE_REDIRECT = nconf.get('IGNORE_REDIRECT') === 'true'; const BASE_URL = nconf.get('BASE_URL'); -let baseUrlSplit = BASE_URL.split('//'); -const BASE_URL_HOST = baseUrlSplit[1]; +const BASE_URL_HOST = url.parse(BASE_URL).hostname; function isHTTP (req) { return ( // eslint-disable-line no-extra-parens diff --git a/website/server/models/group.js b/website/server/models/group.js index 8d98a1b1e7..5ab0b154c9 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -346,7 +346,11 @@ schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, use // Convert to timestamps because Android expects it toJSON.chat.forEach(chat => { - chat.timestamp = chat.timestamp ? chat.timestamp.getTime() : new Date().getTime(); + // old chats are saved with a numeric timestamp + // new chats use `Date` which then has to be converted to the numeric timestamp + if (chat.timestamp && chat.timestamp.getTime) { + chat.timestamp = chat.timestamp.getTime(); + } }); return toJSON; @@ -1537,4 +1541,4 @@ if (!nconf.get('IS_TEST')) { privacy: 'public', }).save(); }); -} \ No newline at end of file +} diff --git a/website/static/audio/airuTheme/ToDo.mp3 b/website/static/audio/airuTheme/Todo.mp3 similarity index 100% rename from website/static/audio/airuTheme/ToDo.mp3 rename to website/static/audio/airuTheme/Todo.mp3 diff --git a/website/static/audio/airuTheme/ToDo.ogg b/website/static/audio/airuTheme/Todo.ogg similarity index 100% rename from website/static/audio/airuTheme/ToDo.ogg rename to website/static/audio/airuTheme/Todo.ogg diff --git a/website/static/audio/arashiTheme/ToDo.mp3 b/website/static/audio/arashiTheme/Todo.mp3 similarity index 100% rename from website/static/audio/arashiTheme/ToDo.mp3 rename to website/static/audio/arashiTheme/Todo.mp3 diff --git a/website/static/audio/arashiTheme/ToDo.ogg b/website/static/audio/arashiTheme/Todo.ogg similarity index 100% rename from website/static/audio/arashiTheme/ToDo.ogg rename to website/static/audio/arashiTheme/Todo.ogg diff --git a/website/static/audio/beatscribeNesTheme/ToDo.mp3 b/website/static/audio/beatscribeNesTheme/Todo.mp3 similarity index 100% rename from website/static/audio/beatscribeNesTheme/ToDo.mp3 rename to website/static/audio/beatscribeNesTheme/Todo.mp3 diff --git a/website/static/audio/beatscribeNesTheme/ToDo.ogg b/website/static/audio/beatscribeNesTheme/Todo.ogg similarity index 100% rename from website/static/audio/beatscribeNesTheme/ToDo.ogg rename to website/static/audio/beatscribeNesTheme/Todo.ogg diff --git a/website/static/audio/danielTheBard/ToDo.mp3 b/website/static/audio/danielTheBard/Todo.mp3 similarity index 100% rename from website/static/audio/danielTheBard/ToDo.mp3 rename to website/static/audio/danielTheBard/Todo.mp3 diff --git a/website/static/audio/danielTheBard/ToDo.ogg b/website/static/audio/danielTheBard/Todo.ogg similarity index 100% rename from website/static/audio/danielTheBard/ToDo.ogg rename to website/static/audio/danielTheBard/Todo.ogg diff --git a/website/static/audio/farvoidTheme/ToDo.mp3 b/website/static/audio/farvoidTheme/Todo.mp3 similarity index 100% rename from website/static/audio/farvoidTheme/ToDo.mp3 rename to website/static/audio/farvoidTheme/Todo.mp3 diff --git a/website/static/audio/farvoidTheme/ToDo.ogg b/website/static/audio/farvoidTheme/Todo.ogg similarity index 100% rename from website/static/audio/farvoidTheme/ToDo.ogg rename to website/static/audio/farvoidTheme/Todo.ogg diff --git a/website/static/audio/gokulTheme/ToDo.mp3 b/website/static/audio/gokulTheme/Todo.mp3 similarity index 100% rename from website/static/audio/gokulTheme/ToDo.mp3 rename to website/static/audio/gokulTheme/Todo.mp3 diff --git a/website/static/audio/gokulTheme/ToDo.ogg b/website/static/audio/gokulTheme/Todo.ogg similarity index 100% rename from website/static/audio/gokulTheme/ToDo.ogg rename to website/static/audio/gokulTheme/Todo.ogg diff --git a/website/static/audio/lunasolTheme/ToDo.mp3 b/website/static/audio/lunasolTheme/Todo.mp3 similarity index 100% rename from website/static/audio/lunasolTheme/ToDo.mp3 rename to website/static/audio/lunasolTheme/Todo.mp3 diff --git a/website/static/audio/lunasolTheme/ToDo.ogg b/website/static/audio/lunasolTheme/Todo.ogg similarity index 100% rename from website/static/audio/lunasolTheme/ToDo.ogg rename to website/static/audio/lunasolTheme/Todo.ogg diff --git a/website/static/audio/luneFoxTheme/ToDo.mp3 b/website/static/audio/luneFoxTheme/Todo.mp3 similarity index 100% rename from website/static/audio/luneFoxTheme/ToDo.mp3 rename to website/static/audio/luneFoxTheme/Todo.mp3 diff --git a/website/static/audio/luneFoxTheme/ToDo.ogg b/website/static/audio/luneFoxTheme/Todo.ogg similarity index 100% rename from website/static/audio/luneFoxTheme/ToDo.ogg rename to website/static/audio/luneFoxTheme/Todo.ogg diff --git a/website/static/audio/maflTheme/ToDo.mp3 b/website/static/audio/maflTheme/Todo.mp3 similarity index 100% rename from website/static/audio/maflTheme/ToDo.mp3 rename to website/static/audio/maflTheme/Todo.mp3 diff --git a/website/static/audio/maflTheme/ToDo.ogg b/website/static/audio/maflTheme/Todo.ogg similarity index 100% rename from website/static/audio/maflTheme/ToDo.ogg rename to website/static/audio/maflTheme/Todo.ogg diff --git a/website/static/audio/pizildenTheme/ToDo.mp3 b/website/static/audio/pizildenTheme/Todo.mp3 similarity index 100% rename from website/static/audio/pizildenTheme/ToDo.mp3 rename to website/static/audio/pizildenTheme/Todo.mp3 diff --git a/website/static/audio/pizildenTheme/ToDo.ogg b/website/static/audio/pizildenTheme/Todo.ogg similarity index 100% rename from website/static/audio/pizildenTheme/ToDo.ogg rename to website/static/audio/pizildenTheme/Todo.ogg diff --git a/website/static/audio/rosstavoTheme/ToDo.mp3 b/website/static/audio/rosstavoTheme/Todo.mp3 similarity index 100% rename from website/static/audio/rosstavoTheme/ToDo.mp3 rename to website/static/audio/rosstavoTheme/Todo.mp3 diff --git a/website/static/audio/rosstavoTheme/ToDo.ogg b/website/static/audio/rosstavoTheme/Todo.ogg similarity index 100% rename from website/static/audio/rosstavoTheme/ToDo.ogg rename to website/static/audio/rosstavoTheme/Todo.ogg diff --git a/website/static/audio/spacePenguinTheme/ToDo.mp3 b/website/static/audio/spacePenguinTheme/Todo.mp3 similarity index 100% rename from website/static/audio/spacePenguinTheme/ToDo.mp3 rename to website/static/audio/spacePenguinTheme/Todo.mp3 diff --git a/website/static/audio/spacePenguinTheme/ToDo.ogg b/website/static/audio/spacePenguinTheme/Todo.ogg similarity index 100% rename from website/static/audio/spacePenguinTheme/ToDo.ogg rename to website/static/audio/spacePenguinTheme/Todo.ogg diff --git a/website/static/audio/triumphTheme/ToDo.mp3 b/website/static/audio/triumphTheme/Todo.mp3 similarity index 100% rename from website/static/audio/triumphTheme/ToDo.mp3 rename to website/static/audio/triumphTheme/Todo.mp3 diff --git a/website/static/audio/triumphTheme/ToDo.ogg b/website/static/audio/triumphTheme/Todo.ogg similarity index 100% rename from website/static/audio/triumphTheme/ToDo.ogg rename to website/static/audio/triumphTheme/Todo.ogg diff --git a/website/static/audio/wattsTheme/ToDo.mp3 b/website/static/audio/wattsTheme/Todo.mp3 similarity index 100% rename from website/static/audio/wattsTheme/ToDo.mp3 rename to website/static/audio/wattsTheme/Todo.mp3 diff --git a/website/static/audio/wattsTheme/ToDo.ogg b/website/static/audio/wattsTheme/Todo.ogg similarity index 100% rename from website/static/audio/wattsTheme/ToDo.ogg rename to website/static/audio/wattsTheme/Todo.ogg