Merge branch 'develop' into release

This commit is contained in:
SabreCat
2018-05-24 18:26:21 +00:00
66 changed files with 904 additions and 832 deletions

22
package-lock.json generated
View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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');

View File

@@ -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")
</template>
<style lang='scss' scoped>
@@ -127,7 +125,7 @@ div
}
#app-header {
margin-top: 96px !important;
margin-top: 40px !important;
}
}
@@ -220,10 +218,9 @@ export default {
selectedItemToBuy: null,
selectedSpellToBuy: null,
sound: {
oggSource: '',
mp3Source: '',
},
audioSource: null,
audioSuffix: null,
loading: true,
currentTipNumber: 0,
bannerHidden: false,
@@ -260,10 +257,21 @@ export default {
}
let file = `/static/audio/${theme}/${sound}`;
this.sound = {
oggSource: `${file}.ogg`,
mp3Source: `${file}.mp3`,
};
if (this.audioSuffix === null) {
this.audioSource = document.createElement('source');
if (this.$refs.sound.canPlayType('audio/ogg')) {
this.audioSuffix = '.ogg';
this.audioSource.type = 'audio/ogg';
} else {
this.audioSuffix = '.mp3';
this.audioSource.type = 'audio/mp3';
}
this.audioSource.src = file + this.audioSuffix;
this.$refs.sound.appendChild(this.audioSource);
} else {
this.audioSource.src = file + this.audioSuffix;
}
this.$refs.sound.load();
});
@@ -317,7 +325,7 @@ export default {
title: 'Habitica',
text: errorMessage,
type: 'error',
timeout: true,
timeout: false,
});
}

View File

@@ -433,12 +433,10 @@ export default {
await hello(network).logout();
} catch (e) {} // eslint-disable-line
const url = window.location.href;
let 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', {

View File

@@ -30,7 +30,6 @@
:categories="challenge.categories",
:owner="isOwner",
:member="isMember",
v-once
)
.challenge-description(v-markdown='challenge.summary')
.well-wrapper(v-if="fullLayout")

View File

@@ -0,0 +1,222 @@
<template lang="pug">
.row.chat-row
.col-12
h3(v-once) {{ label }}
.row
textarea(:placeholder='placeholder',
v-model='newMessage',
:class='{"user-entry": 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
slot(
name="additionRow",
)
.row
.hr.col-12
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
</template>
<script>
import debounce from 'lodash/debounce';
import autocomplete from '../chat/autoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessage from '../chat/chatMessages';
export default {
props: ['label', 'group', 'placeholder'],
components: {
autocomplete,
communityGuidelines,
chatMessage,
},
data () {
return {
newMessage: '',
chat: {
submitDisable: false,
submitTimeout: null,
},
coords: {
TOP: 0,
LEFT: 0,
},
};
},
methods: {
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {
let carPos = text.selectionEnd;
let div = document.createElement('div');
let span = document.createElement('span');
let copyStyle = getComputedStyle(text);
[].forEach.call(copyStyle, (prop) => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
document.body.appendChild(div);
div.textContent = text.value.substr(0, carPos);
span.textContent = text.value.substr(carPos) || '.';
div.appendChild(span);
this.coords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
this._updateCarretPosition(eventUpdate);
}, 250),
_updateCarretPosition (eventUpdate) {
let text = eventUpdate.target;
this.getCoord(eventUpdate, text);
},
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
let response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
// @TODO: I would like to not reload everytime we send. Realtime/Firebase?
let chat = await this.$store.dispatch('chat:getChat', {groupId: this.group._id});
this.group.chat = chat;
},
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
selectedAutocomplete (newText) {
this.newMessage = newText;
},
fetchRecentMessages () {
this.$emit('fetchRecentMessages');
},
reverseChat () {
this.group.chat.reverse();
},
},
beforeRouteUpdate (to, from, next) {
// Reset chat
this.newMessage = '';
this.coords = {
TOP: 0,
LEFT: 0,
};
next();
},
};
</script>
<style scoped lang="scss">
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/variables.scss';
.chat-actions {
margin-top: 1em;
.chat-receive-actions {
padding-left: 0;
button {
margin-bottom: 1em;
&:not(:last-child) {
margin-right: 1em;
}
}
}
.chat-send-actions {
padding-right: 0;
}
}
.chat-row {
position: relative;
textarea {
height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: .5em;
}
.user-entry {
font-style: normal;
color: $black;
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
}
</style>

View File

@@ -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 @@
<script>
// @TODO: Break this down into components
import debounce from 'lodash/debounce';
import extend from 'lodash/extend';
import groupUtilities from 'client/mixins/groupsUtilities';
import styleHelper from 'client/mixins/styleHelper';
@@ -271,13 +261,11 @@ import startQuestModal from './startQuestModal';
import questDetailsModal from './questDetailsModal';
import groupFormModal from './groupFormModal';
import inviteModal from './inviteModal';
import chatMessage from '../chat/chatMessages';
import autocomplete from '../chat/autoComplete';
import groupChallenges from '../challenges/groupChallenges';
import groupGemsModal from 'client/components/groups/groupGemsModal';
import questSidebarSection from 'client/components/groups/questSidebarSection';
import markdownDirective from 'client/directives/markdown';
import communityGuidelines from './communityGuidelines';
import chat from './chat';
import sidebarSection from '../sidebarSection';
import userLink from '../userLink';
@@ -300,16 +288,14 @@ export default {
membersModal,
startQuestModal,
groupFormModal,
chatMessage,
inviteModal,
groupChallenges,
autocomplete,
questDetailsModal,
groupGemsModal,
questSidebarSection,
communityGuidelines,
sidebarSection,
userLink,
chat,
},
directives: {
markdown: markdownDirective,
@@ -337,11 +323,6 @@ export default {
submitDisable: false,
submitTimeout: null,
},
newMessage: '',
coords: {
TOP: 0,
LEFT: 0,
},
};
},
computed: {
@@ -392,13 +373,6 @@ export default {
beforeRouteUpdate (to, from, next) {
this.$set(this, 'searchId', to.params.groupId);
// Reset chat
this.newMessage = '';
this.coords = {
TOP: 0,
LEFT: 0,
};
next();
},
watch: {
@@ -450,40 +424,6 @@ export default {
return this.$store.dispatch('members:getGroupMembers', payload);
},
// @TODO: abstract autocomplete
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {
let carPos = text.selectionEnd;
let div = document.createElement('div');
let span = document.createElement('span');
let copyStyle = getComputedStyle(text);
[].forEach.call(copyStyle, (prop) => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
document.body.appendChild(div);
div.textContent = text.value.substr(0, carPos);
span.textContent = text.value.substr(carPos) || '.';
div.appendChild(span);
this.coords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
this._updateCarretPosition(eventUpdate);
}, 250),
_updateCarretPosition (eventUpdate) {
let text = eventUpdate.target;
this.getCoord(eventUpdate, text);
},
selectedAutocomplete (newText) {
this.newMessage = newText;
},
showMemberModal () {
this.$store.state.memberModalOptions.groupId = this.group._id;
this.$store.state.memberModalOptions.group = this.group;
@@ -492,44 +432,9 @@ export default {
this.$store.state.memberModalOptions.fetchMoreMembers = this.loadMembers;
this.$root.$emit('bv::show::modal', 'members-modal');
},
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
if (!this.newMessage) return;
let response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
},
fetchRecentMessages () {
this.fetchGuild();
},
reverseChat () {
this.group.chat.reverse();
},
updateGuild () {
this.$store.state.editingGroup = this.group;
this.$root.$emit('bv::show::modal', 'guild-form');

View File

@@ -7,26 +7,12 @@
.col-6.title-details
h1(v-once) {{ $t('welcomeToTavern') }}
.row.chat-row
.col-12
h3(v-once) {{ $t('tavernChat') }}
.row
textarea(:placeholder="$t('tavernCommunityGuidelinesPlaceholder')", v-model='newMessage', :class='{"user-entry": 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
.row
.hr.col-12
chat-message(:chat.sync='group.chat', :group-id='group._id', :group-name='group.name')
chat(
:label="$t('tavernChat')",
:group="group",
:placeholder="$t('tavernCommunityGuidelinesPlaceholder')",
@fetchRecentMessages="fetchRecentMessages()"
)
.col-12.col-sm-4.sidebar
.section
.grassy-meadow-backdrop
@@ -179,69 +165,6 @@
@import '~client/assets/scss/colors.scss';
@import '~client/assets/scss/variables.scss';
.chat-actions {
margin-top: 1em;
.chat-receive-actions {
padding-left: 0;
button {
margin-bottom: 1em;
&:not(:last-child) {
margin-right: 1em;
}
}
}
.chat-send-actions {
padding-right: 0;
}
}
.chat-row {
position: relative;
textarea {
height: 150px;
width: 100%;
background-color: $white;
border: solid 1px $gray-400;
font-size: 16px;
font-style: italic;
line-height: 1.43;
color: $gray-300;
padding: .5em;
}
.user-entry {
font-style: normal;
color: $black;
}
.hr {
width: 100%;
height: 20px;
border-bottom: 1px solid $gray-500;
text-align: center;
margin: 2em 0;
}
.hr-middle {
font-size: 16px;
font-weight: bold;
font-family: 'Roboto Condensed';
line-height: 1.5;
text-align: center;
color: $gray-200;
background-color: $gray-700;
padding: .2em;
margin-top: .2em;
display: inline-block;
width: 100px;
}
}
h1 {
color: $purple-200;
}
@@ -505,17 +428,15 @@
</style>
<script>
import debounce from 'lodash/debounce';
import { mapState } from 'client/libs/store';
import { goToModForm } from 'client/libs/modform';
import { TAVERN_ID } from '../../../common/script/constants';
import chatMessage from '../chat/chatMessages';
import autocomplete from '../chat/autoComplete';
import communityGuidelines from './communityGuidelines';
import worldBossInfoModal from '../world-boss/worldBossInfoModal';
import worldBossRageModal from '../world-boss/worldBossRageModal';
import sidebarSection from '../sidebarSection';
import chat from './chat';
import challengeIcon from 'assets/svg/challenge.svg';
import chevronIcon from 'assets/svg/chevron-red.svg';
@@ -538,15 +459,14 @@ import tierNPC from 'assets/svg/tier-npc.svg';
import tierStaff from 'assets/svg/tier-staff.svg';
import quests from 'common/script/content/quests';
import staffList from '../../libs/staffList';
export default {
components: {
chatMessage,
autocomplete,
communityGuidelines,
worldBossInfoModal,
worldBossRageModal,
sidebarSection,
chat,
},
data () {
return {
@@ -571,118 +491,13 @@ export default {
tierNPC,
tierStaff,
}),
chat: {
submitDisable: false,
submitTimeout: null,
},
group: {
chat: [],
},
sections: {
worldBoss: true,
},
staff: [
{
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',
},
],
newMessage: '',
coords: {
TOP: 0,
LEFT: 0,
},
staff: staffList,
};
},
computed: {
@@ -699,81 +514,10 @@ export default {
modForm () {
goToModForm(this.user);
},
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
getCoord (e, text) {
let carPos = text.selectionEnd;
let div = document.createElement('div');
let span = document.createElement('span');
let copyStyle = getComputedStyle(text);
[].forEach.call(copyStyle, (prop) => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
document.body.appendChild(div);
div.textContent = text.value.substr(0, carPos);
span.textContent = text.value.substr(carPos) || '.';
div.appendChild(span);
this.coords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
updateCarretPosition: debounce(function updateCarretPosition (eventUpdate) {
this._updateCarretPosition(eventUpdate);
}, 250),
_updateCarretPosition (eventUpdate) {
let text = eventUpdate.target;
this.getCoord(eventUpdate, text);
},
selectedAutocomplete (newText) {
this.newMessage = newText;
},
toggleSleep () {
this.$store.dispatch('user:sleep');
},
disableMessageSendShortcut () {
// Some users were experiencing accidental sending of messages after pasting
// So, after pasting, disable the shortcut for a second.
this.chat.submitDisable = true;
if (this.chat.submitTimeout) {
// If someone pastes during the disabled period, prevent early re-enable
clearTimeout(this.chat.submitTimeout);
this.chat.submitTimeout = null;
}
this.chat.submitTimeout = window.setTimeout(() => {
this.chat.submitTimeout = null;
this.chat.submitDisable = false;
}, 500);
},
async sendMessageShortcut () {
// If the user recently pasted in the text field, don't submit
if (!this.chat.submitDisable) {
this.sendMessage();
}
},
async sendMessage () {
let response = await this.$store.dispatch('chat:postChat', {
group: this.group,
message: this.newMessage,
});
this.group.chat.unshift(response.message);
this.newMessage = '';
// @TODO: I would like to not reload everytime we send. Realtime/Firebase?
let chat = await this.$store.dispatch('chat:getChat', {groupId: this.group._id});
this.group.chat = chat;
},
async fetchRecentMessages () {
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
},
reverseChat () {
this.group.chat.reverse();
},
pendingDamage () {
if (!this.user.party.quest.progress.up) return 0;
return this.$options.filters.floor(this.user.party.quest.progress.up, 10);
@@ -806,6 +550,10 @@ export default {
startingPage: 'profile',
});
},
async fetchRecentMessages () {
this.group = await this.$store.dispatch('guilds:getGroup', {groupId: TAVERN_ID});
},
},
};
</script>

View File

@@ -55,7 +55,6 @@ div
}
#app-header {
margin-top: 56px;
padding-left: 24px;
padding-top: 9px;
padding-bottom: 8px;

View File

@@ -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 {
.topbar-item {
font-size: 14px !important;
}
}
@media only screen and (min-width: 992px) {
.mobile-only {
display: none !important;
}
.topbar {
max-height: 56px;
.currency-tray {
margin-left: auto;
}
.topbar-item {
padding-top: 5px;
height: 56px;
&.active:not(:hover) {
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
.topbar-dropdown {
position: absolute;
}
}
}
@media only screen and (max-width: 992px) {
.brand {
margin: 0;
}
.gryphon {
position: absolute;
left: calc(50% - 30px);
top: 1em;
top: 10px;
}
.nav-item .nav-link {
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;
#menu_collapse {
margin: 0.6em -16px -8px;
overflow: auto;
}
flex-direction: column;
background-color: $purple-100;
.navbar-nav {
.menu-list {
width: 100%;
background: $purple-100;
order: 1;
text-align: center;
.topbar-dropdown-item {
background: #432874;
border-bottom: #6133b4 solid 1px;
}
.user-menu {
flex-direction: column !important;
align-items: left !important;
background: $purple-100;
.topbar-item {
&.active {
background: #6133b4;
}
background: #4f2a93;
border-bottom: #6133b4 solid 1px;
}
}
}
.currency-tray {
justify-content: center;
min-height: 40px;
background: #271b3d;
width: 100%;
.item-with-icon {
width: 100%;
padding-bottom: 1em;
}
.desktop-only {
display: none !important;
}
}
#nav_collapse {
.menu-toggle {
border: none;
}
#menu_collapse {
display: flex;
justify-content: space-between;
}
nav.navbar {
background: $purple-100 url(~assets/svg/for-css/bits.svg) right no-repeat;
padding-left: 25px;
padding-right: 12.5px;
height: 56px;
.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,49 +195,46 @@ div
&-modal {
z-index: 1035;
z-index: 1042; // To stay above snakbar notifications and modals
}
}
.navbar-header {
margin-right: 48px;
.logo {
padding-left: 10px;
width: 128px;
height: 28px;
}
.quick-menu {
display: flex;
margin-left: auto;
}
.nav-item {
.nav-link {
.currency-tray {
display: flex;
}
.topbar-item {
font-size: 16px;
color: $white !important;
font-weight: bold;
line-height: 1.5;
padding: 16px 20px;
transition: none;
.topbar-dropdown {
display: none; // Display is set to block on hover.
}
>a {
padding: .8em 1em !important;
}
&:hover {
.nav-link {
color: $white !important;
background: $purple-200;
}
}
&.active:not(:hover) {
.nav-link {
box-shadow: 0px -4px 0px $purple-300 inset;
}
}
}
// Make the dropdown menu open on hover
.dropdown:hover .dropdown-menu {
display: block;
margin-top: 0; // remove the gap so it doesn't close
}
.dropdown-menu {
.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;
@@ -206,12 +244,13 @@ div
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
.dropdown-item {
.topbar-dropdown-item {
font-size: 16px;
box-shadow: none;
color: $white;
border: none;
line-height: 1.5;
display: list-item;
&.active {
background: $purple-300;
@@ -219,7 +258,6 @@ div
&:hover {
background: $purple-300;
color: $white;
&:last-child {
border-bottom-right-radius: 5px;
@@ -228,6 +266,8 @@ div
}
}
}
}
}
.dropdown + .dropdown {
margin-left: 0px;
@@ -380,6 +420,7 @@ export default {
this.$root.$emit('bv::show::modal', 'buy-gems', {alreadyTracked: true});
},
},
};
</script>

View File

@@ -935,10 +935,22 @@
}
},
async feedAction (petKey, foodKey) {
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');

View File

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

View File

@@ -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;

View File

@@ -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', {

View File

@@ -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',

View File

@@ -1,44 +1,57 @@
<template lang="pug">
div.header-tabs
.drawer-tab-container
.drawer-tab(v-for="(tab, index) in tabs")
.header-tabs
ul.drawer-tab-container
li.drawer-tab(v-for="(tab, index) in tabs")
a.drawer-tab-text(
@click="changeTab(index)",
:class="{'drawer-tab-text-active': selectedTabPosition === index}",
:title="tab.label"
) {{ tab.label }}
span.right-item
aside.help-item
slot(name="right-item")
</template>
<style lang="scss" scoped>
.drawer-tab-text {
overflow-x: hidden;
display: block;
overflow-x: hidden;
text-overflow: ellipsis;
}
.drawer-tab {
white-space: nowrap;
overflow-x: hidden;
flex: inherit;
overflow-x: hidden;
white-space: nowrap;
}
.drawer-tab-container {
max-width: 50%;
margin: 0 auto;
grid-column-start: 2;
grid-column-end: 3;
justify-self: center;
margin: 0;
padding: 0;
}
.right-item {
position: absolute;
.help-item {
grid-column-start: 3;
position: relative;
right: -11px;
text-align: right;
top: -2px;
}
.header-tabs {
position: relative;
display: flex;
display: grid;
grid-template-columns: 1fr auto 1fr;
}
// MS Edge
@supports (-ms-ime-align: auto) {
.help-item {
align-self: center;
top: 1px;
}
}
</style>

View File

@@ -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',
},
];

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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",

View File

@@ -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)",

View File

@@ -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": "<strong>Tip:</strong> To add multiple Tasks, separate each one using a line break (Shift + Enter) and then press \"Enter.\"",
"addMultipleTip": "<strong>Tip:</strong> 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."
}

View File

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

View File

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

View File

@@ -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),
];
};

View File

@@ -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),
}),
];
}
}

View File

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

View File

@@ -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

View File

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

View File

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

View File

@@ -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)',
};

View File

@@ -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

View File

@@ -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;