Compare commits

..

48 Commits

Author SHA1 Message Date
Keith Holliday b10f056a73 4.39.1 2018-04-23 21:04:03 -05:00
Keith Holliday eeb890466a Converted date to timestamp (#10276)
* Converted date to timestamp

* Added existence check

* Updated test to include timestamp
2018-04-23 21:03:24 -05:00
Sabe Jones 3b35a0a203 4.39.0 2018-04-23 17:21:12 +00:00
Sabe Jones d787ad43d3 chore(i18n): update locales 2018-04-23 17:20:54 +00:00
Keith Holliday 7d7fe6047c Move Chat to Model (#9703)
* Began moving group chat to separate model

* Fixed lint issue

* Updated delete chat with new model

* Updated flag chat to support model

* Updated like chat to use model

* Fixed duplicate code and chat messages

* Added note about concat chat

* Updated clear flags to user new model

* Updated more chat checks when loading get group

* Fixed spell test and back save

* Moved get chat to json method

* Updated flagging with new chat model

* Added missing await

* Fixed chat user styles. Fixed spell group test

* Added new model to quest chat and group plan chat

* Removed extra timestamps. Added limit check for group plans

* Updated tests

* Synced id fields

* Fixed id creation

* Add meta and fixed tests

* Fixed group quest accept test

* Updated puppeteer

* Added migration

* Export vars

* Updated comments
2018-04-23 12:17:16 -05:00
Sabe Jones 0ec1a91774 4.38.0 2018-04-19 19:29:33 +00:00
Sabe Jones adf3281bef chore(i18n): update locales 2018-04-19 19:28:43 +00:00
SabreCat ea86b35833 chore(news): Bailey 2018-04-19 19:25:45 +00:00
Alys ade14edcd7 add partial documentation for dueDate parameter in /api/v3/tasks/user and related code 2018-04-18 23:22:11 +10:00
Sabe Jones 3a1888739a Merge branch 'release' into develop 2018-04-17 20:07:12 +00:00
Sabe Jones 3b54ce4949 4.37.2 2018-04-17 20:06:46 +00:00
Sabe Jones 4a8aaf7389 chore(i18n): update locales 2018-04-17 19:55:10 +00:00
SabreCat 45eec47b7f chore(news): Bailey
Also disable some costly analytics
2018-04-17 19:52:36 +00:00
Keith Holliday 4b9af8aa86 Added analytics to front. Fixed group plan tracking (#10262) 2018-04-17 12:43:52 -05:00
SabreCat 631bbcb786 Merge branch 'fix-hippocrite' into develop 2018-04-17 01:37:20 +00:00
Matteo Pagliazzi 76a10d6cf9 start removing inbox from some routes (#10259) 2018-04-16 18:43:09 +02:00
Keith Holliday a1c9ebd661 Prevent dropdown from closing when clicking search (#10252) 2018-04-15 19:18:40 -05:00
SabreCat 9f06d78db6 Revert "moving developer-only strings to api messages (#10188)"
This reverts commit a42cb0e3ab. Testing hypothesis that this was causing Staging to break.
2018-04-15 17:09:15 +00:00
Alys ac98aa9271 replace Lemoness's email address with admin in sample config file
This is for consistency with the production server and to ensure
that contributors' screenshots in PRs match what will be seen
in production.
2018-04-15 13:34:42 +10:00
negue 455f7ac59b round priority on update too (#10186)
* round priority on update too

* move the fix to Task sanitizeTransform

* refactor the task.priority parsing
2018-04-14 16:16:25 +02:00
negue a42cb0e3ab moving developer-only strings to api messages (#10188)
* move translatable string to apiMessages

* use apiMessages instead of res.t for groupIdRequired / keepOrRemove

* move pageMustBeNumber to apiMessages

* change apimessages

* move missingKeyParam to apiMessages

* move more strings to apiMessages

* fix lint

* revert lodash imports to fix tests

* fix webhook test

* fix test

* rollback key change of `keepOrRemove`

* remove unneeded `req.language` param

*  extract more messages from i18n

* add missing `missingTypeParam` message
2018-04-14 16:13:13 +02:00
Alys d05d2fb9d7 removed a slur that has legit uses - TRIGGER / CONTENT WARNING: slurs, swearwords, assault, etc 2018-04-14 21:51:06 +10:00
negue 6c4c5b4697 always check for the quantity not (#10251) 2018-04-13 21:04:08 +02:00
Keith Holliday 5da87640e4 Apple pay tests (#10248)
* Added more tests for verifyGemPurchase

* Added more tests for subscribe

* Added user is subscribed check

* Reverted gulp task

* Added existence check
2018-04-13 12:41:41 -05:00
Kip Raske fa044ffb44 Feature/sortable reward area (#9930)
* Client POC

We need to wrap each draggable region it its own div or else the
"draggable" element will conflict with each other. This screws up the
styling but that is totally fixable

* Ah that ref was being used after all, changing back

* Scaffold out a new callback for when we drag these things

Next is going to be the hard part: I need to save the sort order for
these to the database. I don't even know if there is a schema but hey
this is the best place to start

* Firefox caching is the problem: don't actually need the wrapper div

So I guess I should try this in chrome and see how it works then come
back to firefox and figure out what the heck is going on

* Scaffolding out our API call to save the sort order

The endpoint doesn't exist yet so we will need to add that

* Ok we are now calling our API endpoint to reorder these things

Of course it doesn't exist yet so you get a 404 when you try, but that
is ok

* Defining api endpoint, a work in progress

In particular I really had ought to use _id for these too, it appears
that the primary way we detect order doesn't even use "key" at all.

* Switching to using the pinned item UUID

This has much better results, but of course the server and client logic
don't match now. Will have to keep working on my splice to make sure
that they are the same

* I thought this would fix our server/client mismatch but it is not it

Something is really wrong with my logic somewhere, maybe I need to
update the db step?

* Moving this logic to the "user" rather than "tasks" and key off path

Path is unique and is less finiky than dealing with string comparisons
with ids. Unfortunately everything is still not working... I suppose
user.update() doesn't care about the position?

* This client code caused quite a lot of problems if you dragged fast

We don't really need it it seems, so off it goes

* Updating markup and CSS so it actually looks good.

Everything is working horray!!

I did just notice the following bug: the popover text sometimes makes it
very annoying to drag because you can't drop over it@

* Cleaning up my comments in the API section user.js

I had a lot of TODOS that are mostly done now

* Fixing a spacing code standards thing

* Turns out we never use type, so we should remove this from the API call

* Adding pinnedItemsOrder into the user schema

And disabling my call in the frontend before I do any more damage

* Halfway to using pinnedItemsOrder

This isn't working yet but it is not going to break it horribly like it
was before.

* Hooking up inAppRewards to always produce sorted information

It is suspicially working right now even though I have not added the
seasonal stuff logic yet...

* Updating the comments in user.js in movedPinnedItem

It turns out that my bandaid fix to just get the ball rolling perfectly
does what I need it to do when we have a length discrepancy. So we are
getting much closer to the final product, just need lots of testing

* Cleaning up code standards kinds of things

* Yay, this fixes the popover issue

I hope this is the right "vue" way to do things, because I tried a bunch
of other things that definately were not the right way to do it. And
this appears to work too

* ** Partial Work ** Starting tests on api call for draggable items

Doesn't work, doesn't compile so don't include in PR!

* Test failing still...

This is worth a save. The api call grabs the seasonal items too, so we
can't get away from using the common functions and calls here to get the
actual list of items

* Okay have the first test passing

Need to clean up my linter problems though

* Planning out the next two tests and fixing my format problems

* 2nd Test case written, this time with the "more" odd case

* Making sure that we didn't mess with pinned items

* Huh... this test doesn't give me the expected result

Drat, I guess I found a bug

* Throw an error when we put garbage in our api call.

Well, before we got user.pinnedItemsOrder filled with a bunch of "null"
entries which is not ideal. it still worked, but isn't this confusing
enough already?

* Cleaning up the multitude of linting problems thanks gulp :)

* Writing tests for inAppRewards.js, but something is wrong

* Fixing my linting errors in inAppRewards tests

These tests still do not run though, so they may fail and I would not
know

* Applying Negue's fixes to inAppRewards.js test

It never occured to me that we shouldn't try to reach the database while
in the common tests. Well, we shouldn't do that, we should use the
common.helpers instead. Thanks!
2018-04-13 15:22:06 +02:00
Tyler Nychka 5449652bd2 pinned items fixes #10012 (#10216)
* Don't unpin non-gear items

Assumes that multiple of bundles, quests, eggs, potions can be bought

* Added tests

* Changed type checking and made variables global

* Lint fix
2018-04-13 15:19:44 +02:00
Philip Karpiak c12ae9ea25 Fix #10202 - Send DELETE request when detaching social auth (#10207) 2018-04-13 15:16:49 +02:00
greenkeeper[bot] 734a300b92 fix(package): update sass-loader to version 7.0.0 (#10250) 2018-04-13 15:15:08 +02:00
negue 1109ae308d convert buyQuest (gold) to the purchase refactoring / check quantity to be a number (#10244) 2018-04-13 15:14:51 +02:00
negue 8f1d241e83 if a pet is still hatchable show the hatchable - icon instead of the "you already own the mount"-icon (#10243) 2018-04-13 15:13:42 +02:00
Matteo Pagliazzi acbca4d1dc upgrade deps 2018-04-12 21:55:24 +02:00
Matteo Pagliazzi 1ea9be8aa2 Preparatory Work for Smaller user doc (WIP) (#10245)
* protect all paths in user.pre(save using this.isDirectSelected to see if a field is available

* fix linting

* authWithHeaders: specify user fields to exclude instead of the ones to include, add comments, doc and improve test

* add more options to unit helper generateReq and add tests for excluding fields in authWithHeaders
2018-04-12 21:17:47 +02:00
Sabe Jones ace02893e5 4.37.1 2018-04-12 18:38:18 +00:00
Sabe Jones 1c3e043fac chore(i18n): update locales 2018-04-12 18:37:36 +00:00
Matteo Pagliazzi 71c9e7a685 Tasks Modal: add setter for repeatsOn (#10247)
* fix for 10236, add setter to repeatsOn

* remove console.log
2018-04-12 13:30:56 -05:00
Sabe Jones fa945c7689 Merge branch 'release' into develop 2018-04-11 01:38:10 +00:00
Isaac Lim 9db7141853 Added meta image for social media sharing (#10193)
* Add meta image for social media sharing

* Meta Image in Images

* Update index.html
2018-04-09 08:34:12 +02:00
Brian Fenton ec2a1927a0 adding name attribute to radio inputs so browser inforces selecting a single item from the named set (#10236) 2018-04-09 08:31:33 +02:00
Matteo Pagliazzi 1c1b0f00ad reorganize payments files (#10235) 2018-04-08 16:27:03 +02:00
Alys fb4d3e44d3 improve code and tests for banned words and slurs (#10211)
* remove removePunctuationFromString function from test code

It's not needed now that the test banned words don't contain underscores.

* prevent tests accidentally throwing messageGroupChatSpam

This commit makes the user for most tests have contributor tiers so
that the user can't trigger the messageGroupChatSpam error message
(for posting messages too quickly).

This is useful when some of the tests fail due to broken code
because that makes more messages be posted than expected. If the user
doesn't have tiers, the messageGroupChatSpam error message would be
triggered, which gives misleading information about the test failure.

* add tests for banned swear and slur words posted in mixed case

* allow banned word error message to show bad words in the same case the user typed them

* stop using randomly-chosen real banned words in tests

The test modified in this commit had been using real banned words,
which meant that those words were being displayed to the contributors
when the test failed.

NB the 'check all banned words are matched' test also uses the real
banned words but the test failure messages don't show the words.

* improve translatability of bannedWordUsed error message
2018-04-08 15:31:37 +02:00
Alys 37fd062cf9 increase Hourglasses and gemCapExtra promptly when multi-month subscription renews - fixes #4819 (#10147)
* allow Hourglasses and gemCapExtra to increase promptly after a multi-month subscription has renewed

* fix existing Hourglass and Gem Cap tests that were wrong

The scenario originally used for these two tests was a six-month recurring
subscription (you can tell that from the starting offset having a non-zero value).
For recurring subscriptions, we do NOT want to increase the consecutive month
benefits as soon as the sixth month starts because the user has already been
given a full six months' benefits in advance and they might cancel the
subscription before it renews later in the sixth month.
Therefore we want to give the extra benefits at the beginning of the seventh
month (ideally we'd give them mid-month in the sixth month when the renewal
happens but we don't have support for tracking renewal dates).
So, the two changed tests were actually not correct for the case
where the offset started as non-zero.

These tests are correct for one-month recurring subscriptions (when the offset
is never set to anything above zero). The user isn't meant to get any consecutive
month benefits until a multiple of 3 months has been reached.

* add tests for one-month recurring subscription before 3x months are reached

* add tests for 3-, 6-, and 12-month recurring subscriptions

The 3-month tests are the most thorough, stepping through the
expected start and end values of consecutive data for a 7-month
range.

The 6-month tests are a bit less thorough since the same code is
used for all multi-month periods.
The discount Google subscription code is used to ensure we keep
support for it.

The 12-month tests are less thorough still, since again the same
code is used.

I'm about to try some more tests with `useFakeTimers`, which should
be a better way to test the code since they won't rely on me having
set the initial values correctly for each test. :) But I wanted to
work through these cases manually first to ensure my understanding
of how the values should change does actually match the code.

* add tests for 1-, 3-, 6-, and 12-month recurring subscriptions using clock changes to simulate passing months

Also fixed the clock call in an unrelated test because it was forming
the date incorrectly (`unix()` can't be used to create a date).

Also changed email@email.email to email@example.com because
email@email.email is potentially a real email address.

* add tests for 3-month gift subscriptions - no extra consecutive benefits given

* add tests for consecutive benefits for 6-month recurring subscription that has incorrect consecutive month data because it started before issue #4819 was fixed

* fix lint errors

* remove outdated subscription tests
2018-04-08 15:26:25 +02:00
Matteo Pagliazzi 485c3c5c46 disable failing test 2018-04-08 14:58:51 +02:00
negue 5007393f24 enable hair style edit during intro (#10227) 2018-04-08 14:52:26 +02:00
Alys e111ac730c enable translated pet names in hatching success message (#10231) 2018-04-08 14:50:36 +02:00
Philip Karpiak e7c78eabce Wrap creator icon + text in @click event (#10221)
Perviously only clicking the icon would activate tabs in the creator, which was confusing
2018-04-06 12:56:33 -05:00
Philip Karpiak 5da7699548 Add tooltip to character buff icon (#10156)
* Add tooltip to character buff icon

* Add tooltips for task streak, challenge and broken challenge

* Add tooltips for task menu and due date

* Challenge icon tooltip displays the challenge short name
2018-04-06 12:53:39 -05:00
Keith Holliday f42955a0ba Added initial account banned modal (#9868)
* Added initial account banned modal

* Fixed check for non logged in user
2018-04-06 08:33:38 -05:00
Neel Mehta 558dd2e4bf fix hippo-crite scroll image size 2018-03-27 23:04:34 -04:00
259 changed files with 3052 additions and 1826 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ RUN npm install -g gulp-cli mocha
# Clone Habitica repo and install dependencies
RUN mkdir -p /usr/src/habitrpg
WORKDIR /usr/src/habitrpg
RUN git clone --branch v4.36.0 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN git clone --branch v4.37.2 https://github.com/HabitRPG/habitica.git /usr/src/habitrpg
RUN npm install
RUN gulp build:prod --force
+2 -2
View File
@@ -98,9 +98,9 @@
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"COMMUNITY_MANAGER_EMAIL" : "admin@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
"PRESS_ENQUIRY_EMAIL" : "admin@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
+52
View File
@@ -0,0 +1,52 @@
// @migrationName = 'MigrateGroupChat';
// @authorName = 'TheHollidayInn'; // in case script author needs to know when their ...
// @authorUuid = ''; // ... own data is done
/*
* This migration move ass chat off of groups and into their own model
*/
import { model as Group } from '../../website/server/models/group';
import { model as Chat } from '../../website/server/models/chat';
async function moveGroupChatToModel (skip = 0) {
const groups = await Group.find({})
.limit(50)
.skip(skip)
.sort({ _id: -1 })
.exec();
if (groups.length === 0) {
console.log('End of groups');
process.exit();
}
const promises = groups.map(group => {
const chatpromises = group.chat.map(message => {
const newChat = new Chat();
Object.assign(newChat, message);
newChat._id = message.id;
newChat.groupId = group._id;
return newChat.save();
});
group.chat = [];
chatpromises.push(group.save());
return chatpromises;
});
const reducedPromises = promises.reduce((acc, curr) => {
acc = acc.concat(curr);
return acc;
}, []);
console.log(reducedPromises);
await Promise.all(reducedPromises);
moveGroupChatToModel(skip + 50);
}
module.exports = moveGroupChatToModel;
+1 -1
View File
@@ -17,5 +17,5 @@ function setUpServer () {
setUpServer();
// Replace this with your migration
const processUsers = require('./20180125_clean_new_notifications.js');
const processUsers = require('./groups/migrate-chat.js');
processUsers();
+294 -434
View File
File diff suppressed because it is too large Load Diff
+30 -30
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.37.0",
"version": "4.39.1",
"main": "./website/server/index.js",
"dependencies": {
"@slack/client": "^3.8.1",
@@ -9,8 +9,8 @@
"amazon-payments": "^0.2.6",
"amplitude": "^3.5.0",
"apidoc": "^0.17.5",
"autoprefixer": "^8.1.0",
"aws-sdk": "^2.211.0",
"autoprefixer": "^8.2.0",
"aws-sdk": "^2.224.1",
"axios": "^0.18.0",
"axios-progress-bar": "^1.1.8",
"babel-core": "^6.0.0",
@@ -27,19 +27,19 @@
"babel-runtime": "^6.11.6",
"bcrypt": "^1.0.2",
"body-parser": "^1.15.0",
"bootstrap": "^4.0.0",
"bootstrap-vue": "^2.0.0-rc.2",
"bootstrap": "^4.1.0",
"bootstrap-vue": "^2.0.0-rc.6",
"compression": "^1.7.2",
"cookie-session": "^1.2.0",
"coupon-code": "^0.4.5",
"cross-env": "^5.1.4",
"css-loader": "^0.28.11",
"csv-stringify": "^2.0.4",
"csv-stringify": "^2.1.0",
"cwait": "^1.1.1",
"domain-middleware": "~0.1.0",
"express": "^4.16.3",
"express-basic-auth": "^1.1.4",
"express-validator": "^5.0.3",
"express-validator": "^5.1.2",
"extract-text-webpack-plugin": "^3.0.2",
"glob": "^7.1.2",
"got": "^8.3.0",
@@ -50,9 +50,9 @@
"gulp.spritesmith": "^6.9.0",
"habitica-markdown": "^1.3.0",
"hellojs": "^1.15.1",
"html-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^3.2.0",
"image-size": "^0.6.2",
"in-app-purchase": "^1.8.9",
"in-app-purchase": "^1.9.0",
"intro.js": "^2.6.0",
"jquery": ">=3.0.0",
"js2xmlparser": "^3.0.0",
@@ -60,14 +60,14 @@
"memwatch-next": "^0.3.0",
"merge-stream": "^1.0.0",
"method-override": "^2.3.5",
"moment": "^2.21.0",
"moment": "^2.22.0",
"moment-recur": "^1.0.7",
"mongoose": "^5.0.10",
"mongoose": "^5.0.14",
"morgan": "^1.7.0",
"nconf": "^0.10.0",
"node-gcm": "^0.14.4",
"node-sass": "^4.8.2",
"nodemailer": "^4.6.3",
"node-sass": "^4.8.3",
"nodemailer": "^4.6.4",
"ora": "^2.0.0",
"pageres": "^4.1.1",
"passport": "^0.4.0",
@@ -75,17 +75,17 @@
"passport-google-oauth20": "1.0.0",
"paypal-ipn": "3.0.0",
"paypal-rest-sdk": "^1.8.1",
"popper.js": "^1.14.1",
"popper.js": "^1.14.3",
"postcss-easy-import": "^3.0.0",
"ps-tree": "^1.0.0",
"pug": "^2.0.1",
"pug": "^2.0.3",
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
"pusher": "^1.3.0",
"rimraf": "^2.4.3",
"sass-loader": "^6.0.7",
"sass-loader": "^7.0.0",
"shelljs": "^0.8.1",
"stackimpact": "^1.2.1",
"stripe": "^5.5.0",
"stackimpact": "^1.3.0",
"stripe": "^5.8.0",
"superagent": "^3.4.3",
"svg-inline-loader": "^0.8.0",
"svg-url-loader": "^2.3.2",
@@ -98,10 +98,10 @@
"validator": "^9.4.1",
"vinyl-buffer": "^1.0.1",
"vue": "^2.5.16",
"vue-loader": "^14.2.1",
"vue-loader": "^14.2.2",
"vue-mugen-scroll": "^0.2.1",
"vue-router": "^3.0.0",
"vue-style-loader": "^4.0.2",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
@@ -141,21 +141,21 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.12",
"@vue/test-utils": "^1.0.0-beta.13",
"babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"chalk": "^2.3.2",
"chromedriver": "^2.36.0",
"chromedriver": "^2.37.0",
"connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.0",
"cross-spawn": "^6.0.5",
"eslint": "^4.19.0",
"eslint": "^4.19.1",
"eslint-config-habitrpg": "^4.0.0",
"eslint-friendly-formatter": "^4.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.2",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-mocha": "^5.0.0",
"eventsource-polyfill": "^0.9.6",
"expect.js": "^0.3.1",
@@ -168,24 +168,24 @@
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-sinon-chai": "^1.3.3",
"karma-sinon-chai": "^1.3.4",
"karma-sinon-stub-promise": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.32",
"karma-webpack": "^3.0.0",
"lcov-result-merger": "^2.0.0",
"mocha": "^5.0.4",
"mocha": "^5.0.5",
"monk": "^6.0.5",
"nightwatch": "^0.9.20",
"puppeteer": "^1.2.0",
"puppeteer": "^1.3.0",
"require-again": "^2.0.0",
"selenium-server": "^3.11.0",
"sinon": "^4.4.5",
"sinon": "^4.5.0",
"sinon-chai": "^3.0.0",
"sinon-stub-promise": "^4.0.0",
"webpack-bundle-analyzer": "^2.11.1",
"webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.21.2"
"webpack-hot-middleware": "^2.22.0"
},
"optionalDependencies": {
"node-rdkafka": "^2.3.0"
@@ -53,16 +53,26 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
it('allows creator to delete a their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array');
expect(messages).to.not.include(nextMessage);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
});
it('allows admin to delete another user\'s message', async () => {
await admin.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
let messages = await user.get(`/groups/${groupWithChat._id}/chat/`);
expect(messages).is.an('array');
expect(messages).to.not.include(nextMessage);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);
const messageFromUser = returnedMessages.find(returnedMessage => {
return returnedMessage.id === nextMessage.id;
});
expect(returnedMessages).is.an('array');
expect(messageFromUser).to.not.exist;
});
it('returns empty when previous message parameter is passed and the last message was deleted', async () => {
@@ -71,9 +81,9 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
});
it('returns the update chat when previous message parameter is passed and the chat is updated', async () => {
let deleteResult = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
const updatedChat = await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}?previousMsg=${message.id}`);
expect(deleteResult[0].id).to.eql(message.id);
expect(updatedChat[0].id).to.eql(message.id);
});
});
});
@@ -23,16 +23,17 @@ describe('GET /groups/:groupId/chat', () => {
privacy: 'public',
}, {
chat: [
{text: 'Hello', flags: {}},
{text: 'Welcome to the Guild', flags: {}},
{text: 'Hello', flags: {}, id: 1},
{text: 'Welcome to the Guild', flags: {}, id: 2},
],
});
});
it('returns Guild chat', async () => {
let chat = await user.get(`/groups/${group._id}/chat`);
const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat).to.eql(group.chat);
expect(chat[0].id).to.eql(group.chat[0].id);
expect(chat[1].id).to.eql(group.chat[1].id);
});
});
+33 -9
View File
@@ -11,7 +11,7 @@ import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
import { getMatchesByWordArray, removePunctuationFromString } from '../../../../../website/server/libs/stringUtils';
import { getMatchesByWordArray } from '../../../../../website/server/libs/stringUtils';
import bannedWords from '../../../../../website/server/libs/bannedWords';
import guildsAllowingBannedWords from '../../../../../website/server/libs/guildsAllowingBannedWords';
import * as email from '../../../../../website/server/libs/email';
@@ -24,10 +24,10 @@ describe('POST /chat', () => {
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
let testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
let testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
let bannedWordErrorMessage = t('bannedWordUsed').split('.');
bannedWordErrorMessage[0] += ` (${removePunctuationFromString(testBannedWordMessage.toLowerCase())})`;
bannedWordErrorMessage = bannedWordErrorMessage.join('.');
let testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
let bannedWordErrorMessage = t('bannedWordUsed', {swearWordsUsed: testBannedWordMessage});
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@@ -39,6 +39,7 @@ describe('POST /chat', () => {
members: 2,
});
user = groupLeader;
await user.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
member = members[0];
additionalMember = members[1];
@@ -136,9 +137,19 @@ describe('POST /chat', () => {
});
});
it('checks error message has the banned words used', async () => {
let randIndex = Math.floor(Math.random() * (bannedWords.length + 1));
let testBannedWords = bannedWords.slice(randIndex, randIndex + 2).map((w) => w.replace(/\\/g, ''));
it('errors when word is typed in mixed case', async () => {
let substrLength = Math.floor(testBannedWordMessage.length / 2);
let chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase() + testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', {swearWordsUsed: chatMessage}),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
let testBannedWords = [testBannedWordMessage.toUpperCase(), testBannedWordMessage1.toLowerCase()];
let chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage}))
.to.eventually.be.rejected
@@ -320,6 +331,17 @@ describe('POST /chat', () => {
members[0].flags.chatRevoked = false;
await members[0].update({'flags.chatRevoked': false});
});
it('errors when slur is typed in mixed case', async () => {
let substrLength = Math.floor(testSlurMessage1.length / 2);
let chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase() + testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
});
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
@@ -359,9 +381,11 @@ describe('POST /chat', () => {
});
it('creates a chat', async () => {
let message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage});
const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`);
expect(message.message.id).to.exist;
expect(newMessage.message.id).to.exist;
expect(groupMessages[0].id).to.exist;
});
it('creates a chat with user styles', async () => {
@@ -11,7 +11,7 @@ import {
each,
} from 'lodash';
import { model as User } from '../../../../../website/server/models/user';
import * as payments from '../../../../../website/server/libs/payments';
import * as payments from '../../../../../website/server/libs/payments/payments';
describe('POST /groups/:groupId/leave', () => {
let typesOfGroups = {
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments : amazon #subscribeCancel', () => {
let endpoint = '/amazon/subscribe/cancel?noRedirect=true';
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments - amazon - #checkout', () => {
let endpoint = '/amazon/checkout';
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/amazonPayments';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
describe('payments - amazon - #subscribe', () => {
let endpoint = '/amazon/subscribe';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #cancelSubscribe', () => {
let endpoint = '/iap/ios/subscribe/cancel?noRedirect=true';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #verify', () => {
let endpoint = '/iap/ios/verify';
@@ -1,5 +1,5 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/applePayments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #subscribe', () => {
let endpoint = '/iap/ios/subscribe';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #cancelSubscribe', () => {
let endpoint = '/iap/android/subscribe/cancel?noRedirect=true';
@@ -1,5 +1,5 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #subscribe', () => {
let endpoint = '/iap/android/subscribe';
@@ -1,5 +1,5 @@
import {generateUser} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/googlePayments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #verify', () => {
let endpoint = '/iap/android/verify';
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #checkout', () => {
let endpoint = '/paypal/checkout';
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #checkoutSuccess', () => {
let endpoint = '/paypal/checkout/success';
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
import shared from '../../../../../../website/common';
describe('payments : paypal #subscribe', () => {
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeCancel', () => {
let endpoint = '/paypal/subscribe/cancel';
@@ -2,7 +2,7 @@ import {
generateUser,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments : paypal #subscribeSuccess', () => {
let endpoint = '/paypal/subscribe/success';
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../../../helpers/api-integration/v3';
import paypalPayments from '../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
describe('payments - paypal - #ipn', () => {
let endpoint = '/paypal/ipn';
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeCancel', () => {
let endpoint = '/stripe/subscribe/cancel?redirect=none';
@@ -2,7 +2,7 @@ import {
generateUser,
generateGroup,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #checkout', () => {
let endpoint = '/stripe/checkout';
@@ -3,7 +3,7 @@ import {
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
describe('payments - stripe - #subscribeEdit', () => {
let endpoint = '/stripe/subscribe/edit';
@@ -4,6 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';
@@ -155,10 +156,11 @@ describe('POST /groups/:groupId/quests/accept', () => {
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
@@ -4,6 +4,7 @@ import {
generateUser,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';
@@ -241,11 +242,13 @@ describe('POST /groups/:groupId/quests/force-start', () => {
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
const returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
});
});
@@ -5,6 +5,7 @@ import {
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
let questingGroup;
@@ -199,11 +200,11 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`);
await group.sync();
const groupChat = await Chat.find({ groupId: group._id }).exec();
expect(group.chat[0].text).to.exist;
expect(group.chat[0]._meta).to.exist;
expect(group.chat[0]._meta).to.have.all.keys(['participatingMembers']);
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await groupLeader.get(`/groups/${group._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
@@ -90,7 +90,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let stub = sandbox.stub(Group.prototype, 'sendChat');
let stub = sandbox.spy(Group.prototype, 'sendChat');
let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`);
await Promise.all([
@@ -5,6 +5,7 @@ import {
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { model as Chat } from '../../../../../website/server/models/chat';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;
@@ -185,11 +186,12 @@ describe('POST /groups/:groupId/quests/reject', () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await questingGroup.sync();
expect(questingGroup.chat[0].text).to.exist;
expect(questingGroup.chat[0]._meta).to.exist;
expect(questingGroup.chat[0]._meta).to.have.all.keys(['participatingMembers']);
const groupChat = await Chat.find({ groupId: questingGroup._id }).exec();
expect(groupChat[0].text).to.exist;
expect(groupChat[0]._meta).to.exist;
expect(groupChat[0]._meta).to.have.all.keys(['participatingMembers']);
let returnedGroup = await leader.get(`/groups/${questingGroup._id}`);
expect(returnedGroup.chat[0]._meta).to.be.undefined;
@@ -296,6 +296,16 @@ describe('PUT /tasks/:id', () => {
expect(fetchedDaily.text).to.eql('saved');
});
// This is a special case for iOS requests
it('will round a priority (difficulty)', async () => {
daily = await user.put(`/tasks/${daily._id}`, {
alias: 'alias',
priority: 0.10000000000005,
});
expect(daily.priority).to.eql(0.1);
});
});
context('habits', () => {
@@ -34,6 +34,8 @@ describe('GET /user', () => {
expect(returnedUser._id).to.equal(user._id);
expect(returnedUser.achievements).to.exist;
expect(returnedUser.items.mounts).to.exist;
// Notifications are always returned
expect(returnedUser.notifications).to.exist;
expect(returnedUser.stats).to.not.exist;
});
});
@@ -0,0 +1,148 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import getOfficialPinnedItems from '../../../../../website/common/script/libs/getOfficialPinnedItems.js';
describe('POST /user/move-pinned-item/:path/move/to/:position', () => {
let user;
let officialPinnedItems;
let officialPinnedItemPaths;
beforeEach(async () => {
user = await generateUser();
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItemPaths = [];
// officialPinnedItems are returned in { type: ..., path:... } format but we just need the paths for testPinnedItemsOrder
if (officialPinnedItems.length > 0) {
officialPinnedItemPaths = officialPinnedItems.map(item => item.path);
}
});
it('adjusts the order of pinned items with no order mismatch', async () => {
let testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'marketGear', path: 'gear.flat.weapon_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.head_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.armor_warrior_1' },
{ type: 'hatchingPotions', path: 'hatchingPotions.Golden' },
{ type: 'marketGear', path: 'gear.flat.shield_warrior_1' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'hatchingPotions.Golden' },
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'food', path: 'food.Saddle' },
];
let testPinnedItemsOrder = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'armoire',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
// For this test put seasonal items at the end so they stay out of the way
testPinnedItemsOrder = testPinnedItemsOrder.concat(officialPinnedItemPaths);
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
let res = await user.post('/user/move-pinned-item/armoire/move/to/5');
await user.sync();
expect(user.pinnedItemsOrder[5]).to.equal('armoire');
expect(user.pinnedItemsOrder[2]).to.equal('gear.flat.weapon_warrior_1');
// We have done nothing to change pinnedItems!
expect(user.pinnedItems).to.deep.equal(testPinnedItems);
let expectedResponse = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'armoire',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
expectedResponse = expectedResponse.concat(officialPinnedItemPaths);
expect(res).to.eql(expectedResponse);
});
it('adjusts the order of pinned items with order mismatch', async () => {
let testPinnedItems = [
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'potion' },
{ type: 'armoire', path: 'armoire' },
];
let testPinnedItemsOrder = [
'armoire',
'potion',
];
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
let res = await user.post('/user/move-pinned-item/armoire/move/to/1');
await user.sync();
// The basic test
expect(user.pinnedItemsOrder[1]).to.equal('armoire');
// potion is now the last item because the 2 unacounted for cards show up
// at the beginning of the order
expect(user.pinnedItemsOrder[user.pinnedItemsOrder.length - 1]).to.equal('potion');
let expectedResponse = [
'cardTypes.thankyou',
'cardTypes.greeting',
'potion',
];
// inAppRewards is used here and will by default put these seasonal items in the front like this:
expectedResponse = officialPinnedItemPaths.concat(expectedResponse);
// now put "armoire" in where we moved it:
expectedResponse.splice(1, 0, 'armoire');
expect(res).to.eql(expectedResponse);
});
it('cannot move pinned item that you do not have pinned', async () => {
let testPinnedItems = [
{ type: 'potion', path: 'potion' },
{ type: 'armoire', path: 'armoire' },
];
let testPinnedItemsOrder = [
'armoire',
'potion',
];
await user.update({
pinnedItems: testPinnedItems,
pinnedItemsOrder: testPinnedItemsOrder,
});
try {
await user.post('/user/move-pinned-item/cardTypes.thankyou/move/to/1');
} catch (err) {
expect(err).to.exist;
}
});
});
@@ -180,11 +180,13 @@ describe('POST /user/class/cast/:spellId', () => {
members: 1,
});
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
await groupLeader.post('/user/class/cast/earth');
await sleep(1);
await group.sync();
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
const groupMessages = await groupLeader.get(`/groups/${group._id}/chat`);
expect(groupMessages[0]).to.exist;
expect(groupMessages[0].uuid).to.equal('system');
});
it('Ethereal Surge does not recover mp of other mages', async () => {
@@ -226,7 +228,7 @@ describe('POST /user/class/cast/:spellId', () => {
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
await sleep(1);
await group.sync();
group = await groupLeader.get(`/groups/${group._id}`);
expect(group.chat[0]).to.exist;
expect(group.chat[0].uuid).to.equal('system');
+462 -33
View File
@@ -23,7 +23,7 @@ describe('cron', () => {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
@@ -82,7 +82,7 @@ describe('cron', () => {
});
it('does not reset plan.gemsBought within the month', () => {
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').unix());
let clock = sinon.useFakeTimers(moment().startOf('month').add(2, 'days').toDate());
user.purchased.plan.dateUpdated = moment().startOf('month').toDate();
user.purchased.plan.gemsBought = 10;
@@ -117,21 +117,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.offset).to.equal(1);
});
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(2);
});
it('does not award unearned plan.consecutive.trinkets if subscription ended during an absence', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.dateTerminated = moment().subtract(3, 'months').toDate();
@@ -143,21 +128,6 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.gemCapExtra multiple times if user has been absent with continuous subscription', () => {
user.purchased.plan.dateUpdated = moment().subtract(6, 'months').toDate();
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
@@ -184,6 +154,465 @@ describe('cron', () => {
expect(user.purchased.plan.consecutive.count).to.equal(0);
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
describe('for a 1-month recurring subscription', () => {
let clock;
// create a user that will be used for all of these tests without a reset before each
let user1 = new User({
auth: {
local: {
username: 'username1',
lowerCaseUsername: 'username1',
email: 'email1@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user1 has a 1-month recurring subscription starting today
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
it('does not increment consecutive benefits after the first month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
clock.restore();
});
it('does not increment consecutive benefits after the second month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(2);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(0);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
clock.restore();
});
it('increments consecutive benefits after the third month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(3);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits after the fourth month', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects e.g., from time zone oddness.
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(4);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user1, tasksByType, daysMissed, analytics});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
clock.restore();
});
});
describe('for a 3-month recurring subscription', () => {
let clock;
let user3 = new User({
auth: {
local: {
username: 'username3',
lowerCaseUsername: 'username3',
email: 'email3@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3 has a 3-month recurring subscription starting today
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the middle of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(2);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(3);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(4);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(5, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(5);
expect(user3.purchased.plan.consecutive.offset).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the second period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(6);
expect(user3.purchased.plan.consecutive.offset).to.equal(0);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(7);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(15);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(10, 'months').add(2, 'days').toDate());
cron({user: user3, tasksByType, daysMissed, analytics});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.offset).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
});
describe('for a 6-month recurring subscription', () => {
let clock;
let user6 = new User({
auth: {
local: {
username: 'username6',
lowerCaseUsername: 'username6',
email: 'email6@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6 has a 6-month recurring subscription starting today
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(6, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(6);
expect(user6.purchased.plan.consecutive.offset).to.equal(0);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(10);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(7);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(13);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(6);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(19, 'months').add(2, 'days').toDate());
cron({user: user6, tasksByType, daysMissed, analytics});
expect(user6.purchased.plan.consecutive.count).to.equal(19);
expect(user6.purchased.plan.consecutive.offset).to.equal(5);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
describe('for a 12-month recurring subscription', () => {
let clock;
let user12 = new User({
auth: {
local: {
username: 'username12',
lowerCaseUsername: 'username12',
email: 'email12@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user12 has a 12-month recurring subscription starting today
user12.purchased.plan.customerId = 'subscribedId';
user12.purchased.plan.dateUpdated = moment().toDate();
user12.purchased.plan.planId = 'basic_12mo';
user12.purchased.plan.consecutive.count = 0;
user12.purchased.plan.consecutive.offset = 12;
user12.purchased.plan.consecutive.trinkets = 4;
user12.purchased.plan.consecutive.gemCapExtra = 20;
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('does not increment consecutive benefits in the final month of the period that they already have benefits for', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(12, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(12);
expect(user12.purchased.plan.consecutive.offset).to.equal(0);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(20);
clock.restore();
});
it('increments consecutive benefits the month after the second paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(13, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(13);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(8);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits the month after the third paid period has started', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(25, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(25);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(12);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits correctly if user has been absent with continuous subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(37, 'months').add(2, 'days').toDate());
cron({user: user12, tasksByType, daysMissed, analytics});
expect(user12.purchased.plan.consecutive.count).to.equal(37);
expect(user12.purchased.plan.consecutive.offset).to.equal(11);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(16);
expect(user12.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
describe('for a 3-month gift subscription (non-recurring)', () => {
let clock;
let user3g = new User({
auth: {
local: {
username: 'username3g',
lowerCaseUsername: 'username3g',
email: 'email3g@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user3g has a 3-month gift subscription starting today
user3g.purchased.plan.customerId = 'Gift';
user3g.purchased.plan.dateUpdated = moment().toDate();
user3g.purchased.plan.dateTerminated = moment().add(3, 'months').toDate();
user3g.purchased.plan.planId = null;
user3g.purchased.plan.consecutive.count = 0;
user3g.purchased.plan.consecutive.offset = 3;
user3g.purchased.plan.consecutive.trinkets = 1;
user3g.purchased.plan.consecutive.gemCapExtra = 5;
it('does not increment consecutive benefits in the first month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.consecutive.offset).to.equal(2);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the second month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(2);
expect(user3g.purchased.plan.consecutive.offset).to.equal(1);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the third month of the gift subscription', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(3);
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(5);
clock.restore();
});
it('does not increment consecutive benefits in the month after the gift subscription has ended', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(4, 'months').add(2, 'days').toDate());
cron({user: user3g, tasksByType, daysMissed, analytics});
expect(user3g.purchased.plan.consecutive.count).to.equal(0); // subscription has been erased by now
expect(user3g.purchased.plan.consecutive.offset).to.equal(0);
expect(user3g.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user3g.purchased.plan.consecutive.gemCapExtra).to.equal(0); // erased
clock.restore();
});
});
describe('for a 6-month recurring subscription where the user has incorrect consecutive month data from prior bugs', () => {
let clock;
let user6x = new User({
auth: {
local: {
username: 'username6x',
lowerCaseUsername: 'username6x',
email: 'email6x@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
// user6x has a 6-month recurring subscription starting 8 months in the past before issue #4819 was fixed
user6x.purchased.plan.customerId = 'subscribedId';
user6x.purchased.plan.dateUpdated = moment().toDate();
user6x.purchased.plan.planId = 'basic_6mo';
user6x.purchased.plan.consecutive.count = 8;
user6x.purchased.plan.consecutive.offset = 0;
user6x.purchased.plan.consecutive.trinkets = 3;
user6x.purchased.plan.consecutive.gemCapExtra = 15;
it('increments consecutive benefits in the first month since the fix for #4819 goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(1, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(9);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('does not increment consecutive benefits in the second month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(2, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(10);
expect(user6x.purchased.plan.consecutive.offset).to.equal(4);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('does not increment consecutive benefits in the third month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(3, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(11);
expect(user6x.purchased.plan.consecutive.offset).to.equal(3);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(5);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
it('increments consecutive benefits in the seventh month after the fix goes live', () => {
clock = sinon.useFakeTimers(moment().zone(0).startOf('month').add(7, 'months').add(2, 'days').toDate());
cron({user: user6x, tasksByType, daysMissed, analytics});
expect(user6x.purchased.plan.consecutive.count).to.equal(15);
expect(user6x.purchased.plan.consecutive.offset).to.equal(5);
expect(user6x.purchased.plan.consecutive.trinkets).to.equal(7);
expect(user6x.purchased.plan.consecutive.gemCapExtra).to.equal(25);
clock.restore();
});
});
});
describe('end of the month perks when user is not subscribed', () => {
@@ -1348,7 +1777,7 @@ describe('recoverCron', () => {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
email: 'email@example.com',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
@@ -4,8 +4,8 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
import { createNonLeaderGroupMember } from '../paymentHelpers';
@@ -1,6 +1,6 @@
import { model as User } from '../../../../../../../website/server/models/user';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import payments from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import payments from '../../../../../../../website/server/libs/payments/payments';
describe('#upgradeGroupPlan', () => {
let spy, data, user, group, uuidString;
@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import applePayments from '../../../../../website/server/libs/applePayments';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import iapModule from '../../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../../website/server/libs/payments/payments';
import applePayments from '../../../../../../website/server/libs/payments/apple';
import iap from '../../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common';
import moment from 'moment';
const i18n = common.i18n;
@@ -57,6 +57,18 @@ describe('Apple Payments', () => {
});
});
it('should throw an error if getPurchaseData is invalid', async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_NO_ITEM_PURCHASED,
});
});
it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
@@ -69,27 +81,76 @@ describe('Apple Payments', () => {
user.canGetGems.restore();
});
it('purchases gems', async () => {
it('errors if amount does not exist', async () => {
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: 'badProduct',
transactionId: token,
}]);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
await expect(applePayments.verifyGemPurchase(user, receipt, headers))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_INVALID_ITEM,
});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: 5.25,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
const gemsCanPurchase = [
{
productId: 'com.habitrpg.ios.Habitica.4gems',
amount: 1,
},
{
productId: 'com.habitrpg.ios.Habitica.20gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.21gems',
amount: 5.25,
},
{
productId: 'com.habitrpg.ios.Habitica.42gems',
amount: 10.5,
},
{
productId: 'com.habitrpg.ios.Habitica.84gems',
amount: 21,
},
];
gemsCanPurchase.forEach(gemTest => {
it(`purchases ${gemTest.productId} gems`, async () => {
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: gemTest.productId,
transactionId: token,
}]);
sinon.stub(user, 'canGetGems').returnsPromise().resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: gemTest.amount,
headers,
});
expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore();
});
});
});
describe('subscribe', () => {
@@ -133,7 +194,16 @@ describe('Apple Payments', () => {
iapModule.validate.restore();
iapModule.isValidated.restore();
iapModule.getPurchaseData.restore();
payments.createSubscription.restore();
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if receipt is invalid', async () => {
@@ -149,26 +219,69 @@ describe('Apple Payments', () => {
});
});
it('creates a user subscription', async () => {
const subOptions = [
{
sku: 'subscription1month',
subKey: 'basic_earned',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.3month',
subKey: 'basic_3mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.6month',
subKey: 'basic_6mo',
},
{
sku: 'com.habitrpg.ios.habitica.subscription.12month',
subKey: 'basic_12mo',
},
];
subOptions.forEach(option => {
it(`creates a user subscription for ${option.sku}`, async () => {
iapModule.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({day: 1}).toDate(),
productId: option.sku,
transactionId: token,
}]);
sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
});
});
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
});
@@ -1,10 +1,10 @@
/* eslint-disable camelcase */
import iapModule from '../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../website/server/libs/payments';
import googlePayments from '../../../../../website/server/libs/googlePayments';
import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common';
import iapModule from '../../../../../../website/server/libs/inAppPurchases';
import payments from '../../../../../../website/server/libs/payments/payments';
import googlePayments from '../../../../../../website/server/libs/payments/google';
import iap from '../../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../../website/server/models/user';
import common from '../../../../../../website/common';
import moment from 'moment';
const i18n = common.i18n;
@@ -1,7 +1,7 @@
import moment from 'moment';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import * as api from '../../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
@@ -3,10 +3,10 @@ import stripeModule from 'stripe';
import nconf from 'nconf';
import * as sender from '../../../../../../../website/server/libs/email';
import * as api from '../../../../../../../website/server/libs/payments';
import amzLib from '../../../../../../../website/server/libs/amazonPayments';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import * as api from '../../../../../../../website/server/libs/payments/payments';
import amzLib from '../../../../../../../website/server/libs/payments/amazon';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import {
@@ -1,14 +1,14 @@
import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import * as api from '../../../../../website/server/libs/payments';
import analytics from '../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-v3-integration.helper';
import * as sender from '../../../../../../website/server/libs/email';
import * as api from '../../../../../../website/server/libs/payments/payments';
import analytics from '../../../../../../website/server/libs/analyticsService';
import notifications from '../../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../../website/server/models/user';
import { translate as t } from '../../../../../helpers/api-v3-integration.helper';
import {
generateGroup,
} from '../../../../helpers/api-unit.helper.js';
} from '../../../../../helpers/api-unit.helper.js';
describe('payments/index', () => {
let user, group, data, plan;
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import { model as User } from '../../../../../../../website/server/models/user';
describe('checkout success', () => {
@@ -1,7 +1,7 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import { model as User } from '../../../../../../../website/server/models/user';
import common from '../../../../../../../website/common';
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
@@ -1,6 +1,6 @@
/* eslint-disable camelcase */
import payments from '../../../../../../../website/server/libs/payments';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import payments from '../../../../../../../website/server/libs/payments/payments';
import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
@@ -2,7 +2,7 @@
import moment from 'moment';
import cc from 'coupon-code';
import paypalPayments from '../../../../../../../website/server/libs/paypalPayments';
import paypalPayments from '../../../../../../../website/server/libs/payments/paypal';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import common from '../../../../../../../website/common';
@@ -4,8 +4,8 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -6,8 +6,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Coupon } from '../../../../../../../website/server/models/coupon';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -1,8 +1,8 @@
import stripeModule from 'stripe';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -4,7 +4,7 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import common from '../../../../../../../website/common';
const i18n = common.i18n;
@@ -4,8 +4,8 @@ import {
generateGroup,
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
import common from '../../../../../../../website/common';
import logger from '../../../../../../../website/server/libs/logger';
import { v4 as uuid } from 'uuid';
@@ -5,8 +5,8 @@ import {
} from '../../../../../../helpers/api-unit.helper.js';
import { model as User } from '../../../../../../../website/server/models/user';
import { model as Group } from '../../../../../../../website/server/models/group';
import stripePayments from '../../../../../../../website/server/libs/stripePayments';
import payments from '../../../../../../../website/server/libs/payments';
import stripePayments from '../../../../../../../website/server/libs/payments/stripe';
import payments from '../../../../../../../website/server/libs/payments/payments';
describe('Stripe - Upgrade Group Plan', () => {
const stripe = stripeModule('test');
+40
View File
@@ -0,0 +1,40 @@
import {
generateRes,
generateReq,
} from '../../../../helpers/api-unit.helper';
import { authWithHeaders as authWithHeadersFactory } from '../../../../../website/server/middlewares/auth';
describe('auth middleware', () => {
let res, req, user;
beforeEach(async () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
});
describe('auth with headers', () => {
it('allows to specify a list of user field that we do not want to load', (done) => {
const authWithHeaders = authWithHeadersFactory(false, {
userFieldsToExclude: ['items', 'flags', 'auth.timestamps'],
});
req.headers['x-api-user'] = user._id;
req.headers['x-api-key'] = user.apiToken;
authWithHeaders(req, res, (err) => {
if (err) return done(err);
const userToJSON = res.locals.user.toJSON();
expect(userToJSON.items).to.not.exist;
expect(userToJSON.flags).to.not.exist;
expect(userToJSON.auth.timestamps).to.not.exist;
expect(userToJSON.auth).to.exist;
expect(userToJSON.notifications).to.exist;
expect(userToJSON.preferences).to.exist;
done();
});
});
});
});
+9 -23
View File
@@ -182,7 +182,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader);
await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
@@ -378,7 +378,7 @@ describe('Group Model', () => {
await party.startQuest(questLeader);
await party.save();
sendChatStub = sandbox.stub(Group.prototype, 'sendChat');
sendChatStub = sandbox.spy(Group.prototype, 'sendChat');
});
afterEach(() => sendChatStub.restore());
@@ -918,21 +918,8 @@ describe('Group Model', () => {
sandbox.spy(User, 'update');
});
it('puts message at top of chat array', () => {
let oldMessage = {
text: 'a message',
};
party.chat.push(oldMessage, oldMessage, oldMessage);
party.sendChat('a new message', {_id: 'user-id', profile: { name: 'user name' }});
expect(party.chat).to.have.a.lengthOf(4);
expect(party.chat[0].text).to.eql('a new message');
expect(party.chat[0].uuid).to.eql('user-id');
});
it('formats message', () => {
party.sendChat('a new message', {
const chatMessage = party.sendChat('a new message', {
_id: 'user-id',
profile: { name: 'user name' },
contributor: {
@@ -947,11 +934,11 @@ describe('Group Model', () => {
},
});
let chat = party.chat[0];
const chat = chatMessage;
expect(chat.text).to.eql('a new message');
expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number');
expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0);
@@ -962,13 +949,11 @@ describe('Group Model', () => {
});
it('formats message as system if no user is passed in', () => {
party.sendChat('a system message');
let chat = party.chat[0];
const chat = party.sendChat('a system message');
expect(chat.text).to.eql('a system message');
expect(validator.isUUID(chat.id)).to.eql(true);
expect(chat.timestamp).to.be.a('number');
expect(chat.timestamp).to.be.a('date');
expect(chat.likes).to.eql({});
expect(chat.flags).to.eql({});
expect(chat.flagCount).to.eql(0);
@@ -1375,7 +1360,8 @@ describe('Group Model', () => {
expect(updatedParticipatingMember.achievements.quests[quest.key]).to.eql(1);
});
it('gives out super awesome Masterclasser achievement to the deserving', async () => {
// Disable test, it fails on TravisCI, but only there
xit('gives out super awesome Masterclasser achievement to the deserving', async () => {
quest = questScrolls.lostMasterclasser4;
party.quest.key = quest.key;
+84
View File
@@ -0,0 +1,84 @@
import {
generateUser,
} from '../../helpers/common.helper';
import getOfficialPinnedItems from '../../../website/common/script/libs/getOfficialPinnedItems.js';
import inAppRewards from '../../../website/common/script/libs/inAppRewards';
describe('inAppRewards', () => {
let user;
let officialPinnedItems;
let officialPinnedItemPaths;
let testPinnedItems;
let testPinnedItemsOrder;
beforeEach(() => {
user = generateUser();
officialPinnedItems = getOfficialPinnedItems(user);
officialPinnedItemPaths = [];
// officialPinnedItems are returned in { type: ..., path:... } format but we just need the paths for testPinnedItemsOrder
if (officialPinnedItems.length > 0) {
officialPinnedItemPaths = officialPinnedItems.map(item => item.path);
}
testPinnedItems = [
{ type: 'armoire', path: 'armoire' },
{ type: 'potion', path: 'potion' },
{ type: 'marketGear', path: 'gear.flat.weapon_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.head_warrior_1' },
{ type: 'marketGear', path: 'gear.flat.armor_warrior_1' },
{ type: 'hatchingPotions', path: 'hatchingPotions.Golden' },
{ type: 'marketGear', path: 'gear.flat.shield_warrior_1' },
{ type: 'card', path: 'cardTypes.greeting' },
{ type: 'potion', path: 'hatchingPotions.Golden' },
{ type: 'card', path: 'cardTypes.thankyou' },
{ type: 'food', path: 'food.Saddle' },
];
testPinnedItemsOrder = [
'hatchingPotions.Golden',
'cardTypes.greeting',
'armoire',
'gear.flat.weapon_warrior_1',
'gear.flat.head_warrior_1',
'cardTypes.thankyou',
'gear.flat.armor_warrior_1',
'food.Saddle',
'gear.flat.shield_warrior_1',
'potion',
];
// For this test put seasonal items at the end so they stay out of the way
testPinnedItemsOrder = testPinnedItemsOrder.concat(officialPinnedItemPaths);
});
it('returns the pinned items in the correct order', () => {
user.pinnedItems = testPinnedItems;
user.pinnedItemsOrder = testPinnedItemsOrder;
let result = inAppRewards(user);
expect(result[2].path).to.eql('armoire');
expect(result[9].path).to.eql('potion');
});
it('does not return seasonal items which have been unpinned', () => {
if (officialPinnedItems.length === 0) {
return; // if no seasonal items, this test is not applicable
}
let testUnpinnedItem = officialPinnedItems[0];
let testUnpinnedPath = testUnpinnedItem.path;
let testUnpinnedItems = [
{ type: testUnpinnedItem.type, path: testUnpinnedPath},
];
user.pinnedItems = testPinnedItems;
user.pinnedItemsOrder = testPinnedItemsOrder;
user.unpinnedItems = testUnpinnedItems;
let result = inAppRewards(user);
let itemPaths = result.map(item => item.path);
expect(itemPaths).to.not.include(testUnpinnedPath);
});
});
+7 -1
View File
@@ -1,7 +1,7 @@
import {
generateUser,
} from '../../../helpers/common.helper';
import buyQuest from '../../../../website/common/script/ops/buy/buyQuest';
import {BuyQuestWithGoldOperation} from '../../../../website/common/script/ops/buy/buyQuest';
import {
BadRequest,
NotAuthorized,
@@ -13,6 +13,12 @@ describe('shared.ops.buyQuest', () => {
let user;
let analytics = {track () {}};
function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
+15
View File
@@ -1,4 +1,5 @@
import purchase from '../../../../website/common/script/ops/buy/purchase';
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
import {
BadRequest,
@@ -25,10 +26,12 @@ describe('shared.ops.purchase', () => {
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
});
context('failure conditions', () => {
@@ -174,6 +177,12 @@ describe('shared.ops.purchase', () => {
user.stats.gp = goldPoints;
user.purchased.plan.gemsBought = 0;
user.purchased.plan.customerId = 'customer-id';
user.pinnedItems.push({type: 'eggs', key: 'Wolf'});
user.pinnedItems.push({type: 'hatchingPotions', key: 'Base'});
user.pinnedItems.push({type: 'food', key: SEASONAL_FOOD});
user.pinnedItems.push({type: 'quests', key: 'gryphon'});
user.pinnedItems.push({type: 'gear', key: 'headAccessory_special_tigerEars'});
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
});
it('purchases gems', () => {
@@ -202,6 +211,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}}, analytics);
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce;
});
@@ -212,6 +222,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases food', () => {
@@ -221,6 +232,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases quests', () => {
@@ -230,6 +242,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
});
it('purchases gear', () => {
@@ -239,6 +252,7 @@ describe('shared.ops.purchase', () => {
purchase(user, {params: {type, key}});
expect(user.items.gear.owned[key]).to.be.true;
expect(pinnedGearUtils.removeItemByPath.calledOnce).to.equal(true);
});
it('purchases quest bundles', () => {
@@ -261,6 +275,7 @@ describe('shared.ops.purchase', () => {
expect(user.balance).to.equal(startingBalance - price);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
clock.restore();
});
});
+7 -2
View File
@@ -54,10 +54,15 @@ export function generateReq (options = {}) {
body: {},
query: {},
headers: {},
header: sandbox.stub().returns(null),
header (header) {
return this.headers[header];
},
session: {},
};
return defaultsDeep(options, defaultReq);
const req = defaultsDeep(options, defaultReq);
return req;
}
export function generateNext (func) {
+31 -1
View File
@@ -9,6 +9,7 @@ div
h2 {{$t('tipTitle', {tipNumber: currentTipNumber})}}
p {{currentTip}}
#app(:class='{"casting-spell": castingSpell}')
banned-account-modal
amazon-payments-modal(v-if='!isStaticPage')
snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage")
@@ -193,6 +194,9 @@ import amazonPaymentsModal from 'client/components/payments/amazonModal';
import spellsMixin from 'client/mixins/spells';
import svgClose from 'assets/svg/close.svg';
import bannedAccountModal from 'client/components/bannedAccountModal';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS.COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
mixins: [notifications, spellsMixin],
@@ -206,6 +210,7 @@ export default {
BuyModal,
SelectMembersModal,
amazonPaymentsModal,
bannedAccountModal,
},
data () {
return {
@@ -288,6 +293,8 @@ export default {
return response;
}, (error) => {
if (error.response.status >= 400) {
this.checkForBannedUser(error);
// Check for conditions to reset the user auth
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(error.response.data) !== -1) {
@@ -366,6 +373,11 @@ export default {
document.title = title;
});
this.$nextTick(() => {
// Load external scripts after the app has been rendered
Analytics.load();
});
if (this.isUserLoggedIn && !this.isStaticPage) {
// Load the user and the user tasks
Promise.all([
@@ -388,7 +400,6 @@ export default {
this.$nextTick(() => {
// Load external scripts after the app has been rendered
setupPayments();
Analytics.load();
});
}).catch((err) => {
console.error('Impossible to fetch user. Clean up localStorage and refresh.', err); // eslint-disable-line no-console
@@ -412,6 +423,25 @@ export default {
if (loadingScreen) document.body.removeChild(loadingScreen);
},
methods: {
checkForBannedUser (error) {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
const errorMessage = error.response.data.message;
// Case where user is not logged in
if (!parseSettings) {
return;
}
const bannedMessage = this.$t('accountSuspended', {
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
userId: parseSettings.auth.apiId,
});
if (errorMessage !== bannedMessage) return;
this.$root.$emit('bv::show::modal', 'banned-account');
},
initializeModalStack () {
// Manage modals
this.$root.$on('bv::show::modal', (modalId, data = {}) => {
@@ -1,54 +1,54 @@
.promo_armoire_background_201804 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -142px -244px;
background-position: -142px -587px;
width: 141px;
height: 441px;
}
.promo_hugabug_bundle {
.promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -532px 0px;
width: 141px;
height: 441px;
width: 325px;
height: 336px;
}
.promo_mystery_201803 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -915px -296px;
background-position: -695px -337px;
width: 114px;
height: 90px;
}
.promo_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -284px -244px;
background-position: -284px -587px;
width: 141px;
height: 441px;
}
.promo_seasonalshop_spring {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -674px -492px;
background-position: -532px -337px;
width: 162px;
height: 138px;
}
.promo_shimmer_pastel {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -674px -148px;
background-position: -426px -735px;
width: 354px;
height: 147px;
}
.promo_shiny_seeds {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -674px 0px;
background-position: -426px -587px;
width: 360px;
height: 147px;
}
.promo_spring_fling_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -244px;
background-position: 0px -587px;
width: 141px;
height: 588px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -915px -387px;
background-position: -532px -476px;
width: 114px;
height: 87px;
}
@@ -58,9 +58,9 @@
width: 531px;
height: 243px;
}
.scene_todos {
.scene_video_games {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -674px -296px;
width: 240px;
height: 195px;
background-position: 0px -244px;
width: 339px;
height: 342px;
}
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 226 KiB

@@ -73,7 +73,6 @@
</style>
<script>
import * as Analytics from 'client/libs/analytics';
import hello from 'hellojs';
import { setUpAxios } from 'client/libs/auth';
@@ -104,13 +103,6 @@ export default {
// windows: WINDOWS_CLIENT_ID,
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});
Analytics.track({
hitType: 'event',
eventCategory: 'group-plans-static',
eventAction: 'view',
eventLabel: 'view-auth-form',
});
},
methods: {
async socialAuth (network) {
@@ -0,0 +1,42 @@
<template lang="pug">
b-modal#banned-account(:title="$t('accountSuspendedTitle')", size='md', :hide-footer="true")
.modal-body
.row
.col-12
p(v-markdown='bannedMessage')
.modal-footer
.col-12.text-center
button.btn.btn-primary(@click='close()') {{$t('close')}}
</template>
<style scoped>
</style>
<script>
import markdownDirective from 'client/directives/markdown';
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS.COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
export default {
directives: {
markdown: markdownDirective,
},
computed: {
bannedMessage () {
const AUTH_SETTINGS = localStorage.getItem('habit-mobile-settings');
const parseSettings = JSON.parse(AUTH_SETTINGS);
const userId = parseSettings ? parseSettings.auth.apiId : '';
return this.$t('accountSuspended', {
userId,
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
});
},
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'banned-account');
},
},
};
</script>
+13 -13
View File
@@ -16,24 +16,24 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
button.btn.btn-secondary(v-once) {{$t('randomize')}}
#options-nav.container.section.text-center.customize-menu
.row
.menu-container(:class='{"col-3": !editing, "col-2 offset-1": editing, active: activeTopPage === "body"}')
.menu-item(@click='changeTopPage("body", "size")')
.menu-container(@click='changeTopPage("body", "size")', :class='{"col-3": !editing, "col-2 offset-1": editing, active: activeTopPage === "body"}')
.menu-item
.svg-icon(v-html='icons.bodyIcon')
strong(v-once) {{$t('bodyBody')}}
.menu-container(:class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "skin"}')
.menu-item(@click='changeTopPage("skin", "color")')
.menu-container(@click='changeTopPage("skin", "color")', :class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "skin"}')
.menu-item
.svg-icon(v-html='icons.skinIcon')
strong(v-once) {{$t('skin')}}
.menu-container(:class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "hair"}')
.menu-item(@click='changeTopPage("hair", "color")')
.menu-container(@click='changeTopPage("hair", "color")', :class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "hair"}')
.menu-item
.svg-icon(v-html='icons.hairIcon')
strong(v-once) {{$t('hair')}}
.menu-container(:class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "extra"}')
.menu-item(@click='changeTopPage("extra", "glasses")')
.menu-container(@click='changeTopPage("extra", "glasses")', :class='{"col-3": !editing, "col-2": editing, active: activeTopPage === "extra"}')
.menu-item
.svg-icon(v-html='icons.accessoriesIcon')
strong(v-once) {{$t('extra')}}
.menu-container.col-2(v-if='editing', :class='{active: activeTopPage === "backgrounds"}')
.menu-item(@click='changeTopPage("backgrounds", "2018")')
.menu-container.col-2(@click='changeTopPage("backgrounds", "2018")', v-if='editing', :class='{active: activeTopPage === "backgrounds"}')
.menu-item
.svg-icon(v-html='icons.backgroundsIcon')
strong(v-once) {{$t('backgrounds')}}
#body.section.customize-section(v-if='activeTopPage === "body"')
@@ -91,7 +91,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
strong(v-once) {{$t('color')}}
.col-3.text-center.sub-menu-item(@click='changeSubPage("bangs")', :class='{active: activeSubPage === "bangs"}')
strong(v-once) {{$t('bangs')}}
.col-3.text-center.sub-menu-item(@click='changeSubPage("style")', :class='{active: activeSubPage === "style"}', v-if='editing')
.col-3.text-center.sub-menu-item(@click='changeSubPage("style")', :class='{active: activeSubPage === "style"}')
strong(v-once) {{$t('style')}}
.col-3.text-center.sub-menu-item(@click='changeSubPage("facialhair")', :class='{active: activeSubPage === "facialhair"}', v-if='editing')
strong(v-once) {{$t('facialhair')}}
@@ -708,7 +708,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
span.price {
color: #24cc8f;
}
.gem {
width: 16px;
}
@@ -723,7 +723,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
span {
font-size: 14px;
}
.gem {
width: 20px;
}
@@ -126,14 +126,6 @@ export default {
return Boolean(this.newGroup.name);
},
},
mounted () {
Analytics.track({
hitType: 'event',
eventCategory: 'group-plans-static',
eventAction: 'view',
eventLabel: 'create-group',
});
},
methods: {
changePage (page) {
Analytics.track({
@@ -351,7 +351,7 @@ export default {
},
hatchPet (potion, egg) {
this.$store.dispatch('common:hatch', {egg: egg.key, hatchingPotion: potion.key});
this.text(this.$t('hatchedPet', {egg: egg.key, potion: potion.key}));
this.text(this.$t('hatchedPet', {egg: egg.text, potion: potion.text}));
if (this.user.preferences.suppressModals.hatchPet) return;
const newPet = createAnimal(egg, potion, 'pet', this.content, this.user.items);
this.$root.$emit('hatchedPet::open', newPet);
@@ -848,14 +848,14 @@
return `Pet Pet-${pet.key} ${pet.eggKey}`;
}
if (pet.mountOwned()) {
return `GreyedOut Pet Pet-${pet.key} ${pet.eggKey}`;
}
if (pet.isHatchable()) {
return 'PixelPaw';
}
if (pet.mountOwned()) {
return `GreyedOut Pet Pet-${pet.key} ${pet.eggKey}`;
}
return 'GreyedOut PixelPaw';
},
hasDrawerTabItems (index) {
@@ -875,7 +875,7 @@
this.closeHatchPetDialog();
this.$store.dispatch('common:hatch', {egg: pet.eggKey, hatchingPotion: pet.potionKey});
this.text(this.$t('hatchedPet', {egg: pet.eggKey, potion: pet.potionKey}));
this.text(this.$t('hatchedPet', {egg: pet.eggName, potion: pet.potionName}));
},
onDragStart (ev, food) {
this.currentDraggingFood = food;
+1 -1
View File
@@ -17,7 +17,7 @@
.d-flex.flex-column.profile-name-character
h3.character-name
| {{member.profile.name}}
.is-buffed(v-if="isBuffed")
.is-buffed(v-if="isBuffed", v-b-tooltip.hover.bottom="$t('buffed')")
.svg-icon(v-html="icons.buff")
span.small-text.character-level {{ characterLevel }}
.progress-container(v-b-tooltip.hover.bottom="$t('health')")
@@ -1,6 +1,7 @@
<template lang="pug">
b-dropdown.create-dropdown(:text="text", no-flip)
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(:disabled='true')
input.form-control(type='text', v-model='searchTerm')
b-dropdown-item(v-for="member in memberResults", :key="member._id", @click="selectMember(member)")
| {{ member.profile.name }}
</template>
+6 -11
View File
@@ -113,7 +113,7 @@
li(v-for='network in SOCIAL_AUTH_NETWORKS')
button.btn.btn-primary.mb-2(v-if='!user.auth[network.key].id', @click='socialAuth(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
button.btn.btn-primary.mb-2(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
button.btn.btn-danger(@click='deleteSocialAuth(network.key)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
button.btn.btn-danger(@click='deleteSocialAuth(network)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
hr
div(v-if='!user.auth.local.username')
p {{ $t('addLocalAuth') }}
@@ -194,10 +194,12 @@ import resetModal from './resetModal';
import deleteModal from './deleteModal';
import { SUPPORTED_SOCIAL_NETWORKS } from '../../../common/script/constants';
import changeClass from '../../../common/script/ops/changeClass';
import notificationsMixin from '../../mixins/notifications';
// @TODO: this needs our window.env fix
// import { availableLanguages } from '../../../server/libs/i18n';
export default {
mixins: [notificationsMixin],
components: {
restoreModal,
resetModal,
@@ -359,16 +361,9 @@ export default {
openDeleteModal () {
this.$root.$emit('bv::show::modal', 'delete');
},
async deleteSocialAuth (networkKey) {
// @TODO: What do we use this for?
// let networktoRemove = find(SOCIAL_AUTH_NETWORKS, function (network) {
// return network.key === networkKey;
// });
await axios.get(`/api/v3/user/auth/social/${networkKey}`);
// @TODO:
// Notification.text(env.t("detachedSocial", {network: network.name}));
// User.sync();
async deleteSocialAuth (network) {
await axios.delete(`/api/v3/user/auth/social/${network.key}`);
this.text(this.$t('detachedSocial', {network: network.name}));
},
async socialAuth (network) {
let auth = await hello(network).login({scope: 'email'});
@@ -195,6 +195,7 @@
</style>
<script>
import * as Analytics from 'client/libs/analytics';
import { setup as setupPayments } from 'client/libs/payments';
import amazonPaymentsModal from 'client/components/payments/amazonModal';
import StaticHeader from './header.vue';
@@ -227,10 +228,24 @@
},
methods: {
goToNewGroupPage () {
Analytics.track({
hitType: 'event',
eventCategory: 'group-plans-static',
eventAction: 'view',
eventLabel: 'view-auth-form',
});
this.$root.$emit('bv::show::modal', 'group-plan');
},
authenticate () {
this.modalPage = 'purchaseGroup';
Analytics.track({
hitType: 'event',
eventCategory: 'group-plans-static',
eventAction: 'view',
eventLabel: 'create-group',
});
},
},
};
+29 -3
View File
@@ -37,8 +37,9 @@
.small-text {{$t(`${type}sDesc`)}}
draggable.sortable-tasks(
ref="tasksList",
@update='sorted',
@update='taskSorted',
:options='{disabled: activeFilter.label === "scheduled"}',
class="sortable-tasks"
)
task(
v-for="task in taskList",
@@ -49,12 +50,19 @@
:group='group',
)
template(v-if="hasRewardsList")
.reward-items
draggable(
ref="rewardsList",
@update="rewardSorted",
@start="rewardDragStart",
@end="rewardDragEnd",
class="reward-items",
)
shopItem(
v-for="reward in inAppRewards",
:item="reward",
:key="reward.key",
:highlightBorder="reward.isSuggested",
:showPopover="showPopovers"
@click="openBuyDialog(reward)",
:popoverPosition="'left'"
)
@@ -319,6 +327,7 @@ export default {
quickAddText: '',
quickAddFocused: false,
quickAddRows: 1,
showPopovers: true,
selectedItemToBuy: {},
};
@@ -450,7 +459,7 @@ export default {
loadCompletedTodos: 'tasks:fetchCompletedTodos',
createTask: 'tasks:create',
}),
async sorted (data) {
async taskSorted (data) {
const filteredList = this.taskList;
const taskToMove = filteredList[data.oldIndex];
const taskIdToMove = taskToMove._id;
@@ -494,6 +503,23 @@ export default {
});
this.user.tasksOrder[`${this.type}s`] = newOrder;
},
async rewardSorted (data) {
const rewardsList = this.inAppRewards;
const rewardToMove = rewardsList[data.oldIndex];
let newOrder = await this.$store.dispatch('user:movePinnedItem', {
path: rewardToMove.path,
position: data.newIndex,
});
this.user.pinnedItemsOrder = newOrder;
},
rewardDragStart () {
// We need to stop popovers from interfering with our dragging
this.showPopovers = false;
},
rewardDragEnd () {
this.showPopovers = true;
},
quickAdd (ev) {
// Add a new line if Shift+Enter Pressed
if (ev.shiftKey) {
+6 -5
View File
@@ -19,7 +19,8 @@
menu-dropdown.task-dropdown(
v-if="isUser && !isRunningYesterdailies",
:right="task.type === 'reward'",
ref="taskDropdown"
ref="taskDropdown",
v-b-tooltip.hover.top="$t('showMore')"
)
div(slot="dropdown-toggle", draggable=false)
.svg-icon.dropdown-icon(v-html="icons.menu")
@@ -69,19 +70,19 @@
label.custom-control-label(v-markdown="item.text", :for="`checklist-${item.id}`")
.icons.small-text.d-flex.align-items-center
.d-flex.align-items-center(v-if="task.type === 'todo' && task.date", :class="{'due-overdue': isDueOverdue}")
.svg-icon.calendar(v-html="icons.calendar")
.svg-icon.calendar(v-html="icons.calendar", v-b-tooltip.hover.bottom="$t('dueDate')")
span {{dueIn}}
.icons-right.d-flex.justify-content-end
.d-flex.align-items-center(v-if="showStreak")
.svg-icon.streak(v-html="icons.streak")
.svg-icon.streak(v-html="icons.streak", v-b-tooltip.hover.bottom="$t('streakCounter')")
span(v-if="task.type === 'daily'") {{task.streak}}
span(v-if="task.type === 'habit'")
span.m-0(v-if="task.up") +{{task.counterUp}}
span.m-0(v-if="task.up && task.down") &nbsp;|&nbsp;
span.m-0(v-if="task.down") -{{task.counterDown}}
.d-flex.align-items-center(v-if="task.challenge && task.challenge.id")
.svg-icon.challenge(v-html="icons.challenge", v-if='!task.challenge.broken')
.svg-icon.challenge.broken(v-html="icons.brokenChallengeIcon", v-if='task.challenge.broken', @click='handleBrokenTask(task)')
.svg-icon.challenge(v-html="icons.challenge", v-if='!task.challenge.broken', v-b-tooltip.hover.bottom="`${task.challenge.shortName}`")
.svg-icon.challenge.broken(v-html="icons.brokenChallengeIcon", v-if='task.challenge.broken', @click='handleBrokenTask(task)', v-b-tooltip.hover.bottom="$t('brokenChaLink')")
.d-flex.align-items-center(v-if="hasTags", :id="`tags-icon-${task._id}`")
.svg-icon.tags(v-html="icons.tags")
b-popover(
@@ -129,10 +129,10 @@
label.d-block(v-once) {{ $t('repeatOn') }}
.form-radio
.custom-control.custom-radio.custom-control-inline
input.custom-control-input(type='radio', v-model="repeatsOn", value="dayOfMonth", id="repeat-dayOfMonth")
input.custom-control-input(type='radio', v-model="repeatsOn", value="dayOfMonth", id="repeat-dayOfMonth" name="repeatsOn")
label.custom-control-label(for="repeat-dayOfMonth") {{ $t('dayOfMonth') }}
.custom-control.custom-radio.custom-control-inline
input.custom-control-input(type='radio', v-model="repeatsOn", value="dayOfWeek", id="repeat-dayOfWeek")
input.custom-control-input(type='radio', v-model="repeatsOn", value="dayOfWeek", id="repeat-dayOfWeek" name="repeatsOn")
label.custom-control-label(for="repeat-dayOfWeek") {{ $t('dayOfWeek') }}
.tags-select.option(v-if="isUserTask")
@@ -797,6 +797,9 @@ export default {
return repeatsOn;
},
set (newRepeatsOn) {
this.calculateMonthlyRepeatDays(newRepeatsOn);
},
},
selectedTags () {
return this.getTagsFor(this.task);
@@ -857,10 +860,10 @@ export default {
weekdaysMin (dayNumber) {
return moment.weekdaysMin(dayNumber);
},
calculateMonthlyRepeatDays () {
calculateMonthlyRepeatDays (newRepeatsOn) {
if (!this.task) return;
const task = this.task;
const repeatsOn = this.repeatsOn;
const repeatsOn = newRepeatsOn || this.repeatsOn;
if (task.frequency === 'monthly') {
if (repeatsOn === 'dayOfMonth') {
+1
View File
@@ -10,6 +10,7 @@
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
</head>
<body>
<div id="loading-screen">
+1 -1
View File
@@ -138,4 +138,4 @@ export function load () {
gaScript.async = 1;
gaScript.src = '//www.google-analytics.com/analytics.js';
firstScript.parentNode.insertBefore(gaScript, firstScript);
}
}
+4 -1
View File
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import getStore from 'client/store';
import * as Analytics from 'client/libs/analytics';
// import * as Analytics from 'client/libs/analytics';
// import EmptyView from './components/emptyView';
@@ -342,12 +342,15 @@ router.beforeEach(function routerGuard (to, from, next) {
});
}
/*
Analytics.track({
hitType: 'pageview',
eventCategory: 'navigation',
eventAction: 'navigate',
page: to.name || to.path,
});
*/
next();
});
+5
View File
@@ -111,6 +111,11 @@ export function togglePinnedItem (store, params) {
return addedItem;
}
export async function movePinnedItem (store, params) {
let response = await axios.post(`/api/v3/user/move-pinned-item/${params.path}/move/to/${params.position}`);
return response.data.data;
}
export function castSpell (store, params) {
let spellUrl = `/api/v3/user/class/cast/${params.key}`;
+3 -3
View File
@@ -167,9 +167,9 @@
"questEggBadgerText": "Язовец",
"questEggBadgerMountText": "Язовец",
"questEggBadgerAdjective": "немирен",
"questEggSquirrelText": "Squirrel",
"questEggSquirrelMountText": "Squirrel",
"questEggSquirrelAdjective": "bushy-tailed",
"questEggSquirrelText": "Катерица",
"questEggSquirrelMountText": "Катерица",
"questEggSquirrelAdjective": "рунтава",
"eggNotes": "Намерете излюпваща отвара, която да излеете върху това яйце и от него ще се излюпи <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
"hatchingPotionBase": "Нормален цвят",
"hatchingPotionWhite": "Бял цвят",
+2 -1
View File
@@ -286,7 +286,8 @@
"passwordResetEmailHtml": "Ако сте заявили нулиране на паролата си за <strong><%= username %></strong> в Хабитика, <a href=\"<%= passwordResetLink %>\">натиснете тук</a>, за да зададете нова. Тази връзка ще загуби давност след 24 часа.<br/><br>Ако не сте заявили нулиране на паролата си, не обръщайте внимание на това писмо.",
"invalidLoginCredentialsLong": "Опа, потребителското име/е-пощата или паролата е грешна.\n— Уверете се, че всичко е изписано правилно. Потребителското име и паролата са чувствителни към регистъра;\n— Може да сте се вписали чрез Фейсбук или Гугъл, а не чрез е-поща. Проверете това, като опитате да влезете чрез Фейсбук или Гугъл;\n— Ако сте забравили паролата си, натиснете „Забравена парола“.",
"invalidCredentials": "Няма профил, който използва тези данни за вход.",
"accountSuspended": "Достъпът до акаунта е преустановен. За да получите помощ, моля, свържете се с <%= communityManagerEmail %>, посочвайки своя потребителски идентификатор „<%= userId %>“.",
"accountSuspended": "Този акаунт, с потребителски идентификатор „<%= userId %>“, е блокиран за нарушаване на [Обществените правила](https://habitica.com/static/community-guidelines) или [Условията за ползване](https://habitica.com/static/terms). За повече подробности, или ако искате да помолите за отблокиране, моля, пишете на управителя за общността на адрес <%= communityManagerEmail %> или помолете свой родител или настойник да пише. Моля, копирайте потребителския си идентификатор в е-писмото, и напишете името на профила си.",
"accountSuspendedTitle": "Достъпът до акаунта е преустановен",
"unsupportedNetwork": "Тази мрежа не се поддържа в момента.",
"cantDetachSocial": "Профилът няма друг начин за удостоверяване, така че този начин за влизане не може да бъде премахнат.",
"onlySocialAttachLocal": "Местното удостоверяване може да бъде добавено само към профил от социална мрежа.",
+1 -1
View File
@@ -34,7 +34,7 @@
"communityGuidelines": "Обществени правила",
"communityGuidelinesRead1": "Моля, прочетете нашите",
"communityGuidelinesRead2": "преди да пишете.",
"bannedWordUsed": "Опа! Изглежда тази публикация съдържа ругатня, религиозна клетва или намеква за употреба на пристрастяващо вещество или друга тема, подходяща само за възрастни. Хабитика се използва от най-различни потребители, затова държим на това съобщенията ва чата да бъдат подходящи за всички. Редактирайте съобщението си и ще можете да го публикувате!",
"bannedWordUsed": "Опа! Изглежда тази публикация съдържа ругатня, религиозна клетва или намеква за употреба на пристрастяващо вещество или друга тема, подходяща само за възрастни (<%= swearWordsUsed %>). Хабитика се използва от най-различни потребители, затова държим на това съобщенията в чата да бъдат подходящи за всички. Редактирайте съобщението си и ще можете да го публикувате!",
"bannedSlurUsed": "Публикацията Ви съдържа неприлични думи или изказ, затова привилегиите Ви в чата Ви бяха отнети.",
"party": "Група",
"createAParty": "Създаване на група",
+6 -6
View File
@@ -595,10 +595,10 @@
"dysheartenerArtCredit": "Графиките са от @AnnDeLune",
"hugabugText": "Пакет мисии „Любими буболечки“",
"hugabugNotes": "Съдържа: „КРИТИЧНИЯТ БРЪМБАР“, „Охлювът на черноработната утайка“ и „Сбогом, пеперудке“. Наличен до 31 март.",
"questSquirrelText": "The Sneaky Squirrel",
"questSquirrelNotes": "You wake up and find youve overslept! Why didnt your alarm go off? … How did an acorn get stuck in the ringer?<br><br>When you try to make breakfast, the toaster is stuffed with acorns. When you go to retrieve your mount, @Shtut is there, trying unsuccessfully to unlock their stable. They look into the keyhole. “Is that an acorn in there?”<br><br>@randomdaisy cries out, “Oh no! I knew my pet squirrels had gotten out, but I didnt know theyd made such trouble! Can you help me round them up before they make any more of a mess?”<br><br>Following the trail of mischievously placed oak nuts, you track and catch the wayward sciurines, with @Cantras helping secure each one safely at home. But just when you think your task is almost complete, an acorn bounces off your helm! You look up to see a mighty beast of a squirrel, crouched in defense of a prodigious pile of seeds.<br><br>“Oh dear,” says @randomdaisy, softly. “Shes always been something of a resource guarder. Well have to proceed very carefully!” You circle up with your party, ready for trouble!",
"questSquirrelCompletion": "With a gentle approach, offers of trade, and a few soothing spells, youre able to coax the squirrel away from its hoard and back to the stables, which @Shtut has just finished de-acorning. Theyve set aside a few of the acorns on a worktable. “These ones are squirrel eggs! Maybe you can raise some that dont play with their food quite so much.”",
"questSquirrelBoss": "Sneaky Squirrel",
"questSquirrelDropSquirrelEgg": "Squirrel (Egg)",
"questSquirrelUnlockText": "Unlocks purchasable Squirrel eggs in the Market"
"questSquirrelText": "Хитрата катерица",
"questSquirrelNotes": "Събуждаш се и разбираш, че си се успал! Защо не е звъннал будилникът?… И защо в звънеца му има затъкнат жълъд?<br><br>Когато се опитваш да си направиш закуска, откриваш, че и тостерът е пълен с жълъди. Когато отиваш да вземеш превоза си, @Shtut също е там и се опитва неуспешно да отключи конюшнята си. Поглежда в ключалката и възкликва — „Това жълъд ли е?“<br><br>@randomdaisy се вайка — „О, не! Разбрах, че любимците ми катерици са се измъкнали, но не очаквах, че ще свършат толкова много бели! Ще ми помогнеш ли да ги съберем, преди да създадат още бъркотии?“<br><br>Следвайки следата от дяволито разпилените дъбови плодчета, успяваш да проследиш и откриеш своенравните катерици, а @Cantras помага една по една те да бъдат прибрани вкъщи. Но тъкмо когато вече си мислиш, че задачата е почти завършена, един жълъд се удря в шлема ти и отскача! Поглеждаш нагоре и виждаш огромна катерица – истински звяр, клекнала в защитна поза, опитвайки се опази внушителна купчина от зърна.<br><br>„О, да му се не види“ — вайка се @randomdaisy, — „тя винаги е била нещо като пазител на запасите. Ще трябва да сме много внимателни!“ Групата ви внимателни заобикаля катерицата, като всички са нащрек!",
"questSquirrelCompletion": "С внимателен подход, подкупи и няколко успокояващи заклинания, успявате да придумате катерицата да се отмести от плячката си и да се насочи към конюшнята, където @Shtut вече е успял да премахне всички жълъди. Няколко от жълъдите са подредени на една работна маса. „Това са яйца на катерица! Може би ще успееш да отгледаш някоя катерица, която няма да си играе толкова много с храната си.“",
"questSquirrelBoss": "Хитра катерица",
"questSquirrelDropSquirrelEgg": "Катерица (яйце)",
"questSquirrelUnlockText": "Отключва възможността за купуване на яйца на катерица от пазара."
}
+7 -7
View File
@@ -339,11 +339,11 @@
"backgroundElegantBalconyNotes": "Podívej se na krajinu z Elegantního balkónu",
"backgroundDrivingACoachText": "Jízda na kočáře",
"backgroundDrivingACoachNotes": "Užívej si Jízdu na kočáře přes pole plném květin.",
"backgrounds042018": "SET 47: Released April 2018",
"backgroundTulipGardenText": "Tulip Garden",
"backgroundTulipGardenNotes": "Tiptoe through a Tulip Garden.",
"backgroundFlyingOverWildflowerFieldText": "Field of Wildflowers",
"backgroundFlyingOverWildflowerFieldNotes": "Soar above a Field of Wildflowers.",
"backgroundFlyingOverAncientForestText": "Ancient Forest",
"backgroundFlyingOverAncientForestNotes": "Fly over the canopy of an Ancient Forest."
"backgrounds042018": "SET 47: Vydáno v dubnu 2018",
"backgroundTulipGardenText": "Zahrada tulipánů",
"backgroundTulipGardenNotes": "Opatrně našlapuj přes Zahradu tulipánů.",
"backgroundFlyingOverWildflowerFieldText": "Pole divokých květin",
"backgroundFlyingOverWildflowerFieldNotes": "Vznes se nad Pole divokých květin.",
"backgroundFlyingOverAncientForestText": "Prastarý les",
"backgroundFlyingOverAncientForestNotes": "Přeleť přes nebesa Prastarého lesa."
}
+2 -1
View File
@@ -286,7 +286,8 @@
"passwordResetEmailHtml": "If you requested a password reset for <strong><%= username %></strong> on Habitica, <a href=\"<%= passwordResetLink %>\">click here</a> to set a new one. The link will expire after 24 hours.<br/><br>If you haven't requested a password reset, please ignore this email.",
"invalidLoginCredentialsLong": "Uh-oh - your email address / login name or password is incorrect.\n- Make sure they are typed correctly. Your login name and password are case-sensitive.\n- You may have signed up with Facebook or Google-sign-in, not email so double-check by trying them.\n- If you forgot your password, click \"Forgot Password\".",
"invalidCredentials": "There is no account that uses those credentials.",
"accountSuspended": "Account has been suspended, please contact <%= communityManagerEmail %> with your User ID \"<%= userId %>\" for assistance.",
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the [Community Guidelines](https://habitica.com/static/community-guidelines) or [Terms of Service](https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please copy your User ID into the email and include your Profile Name.",
"accountSuspendedTitle": "Account has been suspended",
"unsupportedNetwork": "Tato síť není momentálně dostupná.",
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",
"onlySocialAttachLocal": "Local authentication can be added to only a social account.",
+1 -1
View File
@@ -34,7 +34,7 @@
"communityGuidelines": "zásady komunity",
"communityGuidelinesRead1": "Prosíme, přečti si naše",
"communityGuidelinesRead2": "než začneš chatovat.",
"bannedWordUsed": "Oops! Vypadá to, že příspěvek obsahuje sprosté slovo, náboženskou přísahu, nebo referenci na návykovou látku či dospělé téma. Habitica má uživatele z různých prostředí a věkových kategorií, takže se snažíme držet náš chat co nejvíce přístupný. Nebojte se tedy upravit svoji zprávu tak, aby jste ji mohli zveřejnit!",
"bannedWordUsed": "Oops! Looks like this post contains a swearword, religious oath, or reference to an addictive substance or adult topic (<%= swearWordsUsed %>). Habitica has users from all backgrounds, so we keep our chat very clean. Feel free to edit your message so you can post it!",
"bannedSlurUsed": "Tvůj příspěvek obsahoval nevhodný jazyk, takže ti byl zrušen přístup na chat.",
"party": "Družina",
"createAParty": "Vytvořit družinu",
+3 -3
View File
@@ -136,10 +136,10 @@
"mysterySet201708": "Set Lávového válečníka",
"mysterySet201709": "Set Studenta kouzel",
"mysterySet201710": "Imperious Imp Set",
"mysterySet201711": "Carpet Rider Set",
"mysterySet201711": "Set Jezdce koberců",
"mysterySet201712": "Candlemancer Set",
"mysterySet201801": "Frost Sprite Set",
"mysterySet201802": "Love Bug Set",
"mysterySet201801": "Set Mrazivého skřítka",
"mysterySet201802": "Set Zamilovaného brouka",
"mysterySet201803": "Daring Dragonfly Set",
"mysterySet301404": "Standardní steampunkový set",
"mysterySet301405": "Set steampunkových doplňků",
+4 -4
View File
@@ -100,7 +100,7 @@
"hideTags": "Schovat",
"showTags": "Ukázat",
"editTags2": "Upravit štítky",
"toRequired": "You must supply a \"to\" property",
"toRequired": "Musíš zadat hodnotu vlastnictví",
"startDate": "Datum začátku",
"startDateHelpTitle": "Kdy by měl tento úkol začít?",
"startDateHelp": "Nastav datum, ve kterém začne úkol platit. Nebude muset být splněn dříve.",
@@ -177,10 +177,10 @@
"taskApprovalWasNotRequested": "Pouze úkol, který čeká na schválení, může být označen jako potřebující více práce",
"approvals": "Schválení",
"approvalRequired": "Potřebuje schválení",
"repeatZero": "Daily is never due",
"repeatZero": "Denní úkol nikdy nemá splatnost",
"repeatType": "Typ opakování",
"repeatTypeHelpTitle": "Jaký druh opakování je toto?",
"repeatTypeHelp": "Select \"Daily\" if you want this task to repeat every day or every third day, etc. Select \"Weekly\"if you want it to repeat on certain days of the week. If you select \"Monthly\" or \"Yearly\", adjust the Start Date to control which day of the month or year the task will be due on.",
"repeatTypeHelp": "Vyber \"Denně\", pokud chceš, aby se tento úkol opakoval každý den, každý třetí den, etc.. Vyber \"Týdně\" pokud chceš, aby se opakoval v určitý den v každém týdnu. Pokud vybereš \"Měsíčně\" nebo \"Ročně\", nastav si startovní datum pro kontrolu, kdy má být úkol ten měsíc či rok splněn. ",
"weekly": "Týdně",
"monthly": "Měsíčně",
"yearly": "Ročně",
@@ -212,5 +212,5 @@
"repeatDayError": "Prosím, ujisti se, že máš alespoň jeden den v týdnu vybraný.",
"searchTasks": "Vyhledat názvy a popisy...",
"sessionOutdated": "Tvá relace je zastaralá. Prosím, zkus ji obnovit nebo synchronizovat.",
"errorTemporaryItem": "This item is temporary and cannot be pinned."
"errorTemporaryItem": "Tento předmět je dočasný a nemůže být připnut."
}
+2 -1
View File
@@ -286,7 +286,8 @@
"passwordResetEmailHtml": "Hvis du har anmodet om nulstilling af kodeordet til <strong><%= username %></strong> på Habitica, så <a href=\"<%= passwordResetLink %>\">klik her</a> for at vælge et nyt kodeord. Linket vil være gyldigt i 24 timer.<br/><br>Hvis du ikke har anmodet om nulstilling af kodeord, så venligst ignorer denne email.",
"invalidLoginCredentialsLong": "Uh-oh - your email address / login name or password is incorrect.\n- Make sure they are typed correctly. Your login name and password are case-sensitive.\n- You may have signed up with Facebook or Google-sign-in, not email so double-check by trying them.\n- If you forgot your password, click \"Forgot Password\".",
"invalidCredentials": "Der er ingen konto med disse legitimationsoplysninger.",
"accountSuspended": "Konto suspenderet, kontakt <%= communityManagerEmail %> med dit bruger ID \"<%= userId %>\" for at få hjælp.",
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the [Community Guidelines](https://habitica.com/static/community-guidelines) or [Terms of Service](https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please copy your User ID into the email and include your Profile Name.",
"accountSuspendedTitle": "Account has been suspended",
"unsupportedNetwork": "Dette netværk understøttes ikke i øjeblikket.",
"cantDetachSocial": "Kontoen mangler en anden godkendelsesmetode; kan ikke udføre denne godkendelsesmetode.",
"onlySocialAttachLocal": "Lokal godkendelse kan kun føjes til en social konto.",
+1 -1
View File
@@ -34,7 +34,7 @@
"communityGuidelines": "Retningslinjer for Fællesskabet",
"communityGuidelinesRead1": "Læs venligst vores",
"communityGuidelinesRead2": "før du chatter.",
"bannedWordUsed": "Ups! Det ser ud til at denne besked indeholder et bandeord, religiøs ed, eller en reference til en vanedannende substans eller voksen emne. Habitica har brugere fra alle baggrunde, så vi holder vores chat meget ren. Føl dig endelig fri til at rette din besked, så du kan sende den!",
"bannedWordUsed": "Oops! Looks like this post contains a swearword, religious oath, or reference to an addictive substance or adult topic (<%= swearWordsUsed %>). Habitica has users from all backgrounds, so we keep our chat very clean. Feel free to edit your message so you can post it!",
"bannedSlurUsed": "Your post contained inappropriate language, and your chat privileges have been revoked.",
"party": "Gruppe",
"createAParty": "Opret en Gruppe",
+3 -3
View File
@@ -167,9 +167,9 @@
"questEggBadgerText": "Dachs-Jungtier",
"questEggBadgerMountText": "Dachs-Reittier",
"questEggBadgerAdjective": "eifriges",
"questEggSquirrelText": "Squirrel",
"questEggSquirrelMountText": "Squirrel",
"questEggSquirrelAdjective": "bushy-tailed",
"questEggSquirrelText": "Eichörnchen",
"questEggSquirrelMountText": "Eichörnchen",
"questEggSquirrelAdjective": "mit einem flauschigen Schwanz",
"eggNotes": "Finde ein Schlüpfelixier, das Du über dieses Ei gießen kannst, damit ein <%= eggAdjective(locale) %> <%= eggText(locale) %> schlüpfen kann.",
"hatchingPotionBase": "Normales",
"hatchingPotionWhite": "Weißes",
+2 -1
View File
@@ -286,7 +286,8 @@
"passwordResetEmailHtml": "Wenn Du das Passwort für <strong><%= username %></strong> auf Habitica zurücksetzen möchtest, folge bitte <a href=\"<%= passwordResetLink %>\">diesem Link </a>, um ein neues zu setzen. Dieser Link wird in 24 Stunden ungültig.<br/><br>Wenn du kein Passwort-Reset angefordert hast, kannst Du diese E-Mail ignorieren.",
"invalidLoginCredentialsLong": "Uh-oh - your email address / login name or password is incorrect.\n- Make sure they are typed correctly. Your login name and password are case-sensitive.\n- You may have signed up with Facebook or Google-sign-in, not email so double-check by trying them.\n- If you forgot your password, click \"Forgot Password\".",
"invalidCredentials": "Es gibt kein Konto, das diese Anmeldedaten verwendet.",
"accountSuspended": "Dein Account wurde gesperrt, bitte kontaktiere <%= communityManagerEmail %> zur Hilfe und gib Deine Benutzer-ID \"<%= userId %>\" an.",
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the [Community Guidelines](https://habitica.com/static/community-guidelines) or [Terms of Service](https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please copy your User ID into the email and include your Profile Name.",
"accountSuspendedTitle": "Account has been suspended",
"unsupportedNetwork": "Dieses Netzwerk wird aktuell nicht unterstützt.",
"cantDetachSocial": "Der Account hat nur noch diese Authentifizierung, sie kann nicht getrennt werden.",
"onlySocialAttachLocal": "Lokale Authentifizierung kann nur zu einem Social-Media-Konto hinzugefügt werden.",

Some files were not shown because too many files have changed in this diff Show More