Merge branch 'develop' of https://github.com/HabitRPG/habitica into negue/flagpm

This commit is contained in:
negue
2019-01-27 18:14:51 +01:00
807 changed files with 41837 additions and 36396 deletions

2
.nvmrc
View File

@@ -1 +1 @@
8 10

View File

@@ -1,6 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- '8' - '10'
services: services:
- mongodb - mongodb
cache: cache:

View File

@@ -1,4 +1,4 @@
FROM node:8 FROM node:10
ENV ADMIN_EMAIL admin@habitica.com ENV ADMIN_EMAIL admin@habitica.com
ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e ENV AMAZON_PAYMENTS_CLIENT_ID amzn1.application-oa2-client.68ed9e6904ef438fbc1bf86bf494056e
@@ -8,6 +8,7 @@ ENV BASE_URL https://habitica.com
ENV FACEBOOK_KEY 128307497299777 ENV FACEBOOK_KEY 128307497299777
ENV GA_ID UA-33510635-1 ENV GA_ID UA-33510635-1
ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com ENV GOOGLE_CLIENT_ID 1035232791481-32vtplgnjnd1aufv3mcu1lthf31795fq.apps.googleusercontent.com
ENV LOGGLY_CLIENT_TOKEN ab5663bf-241f-4d14-8783-7d80db77089a
ENV NODE_ENV production ENV NODE_ENV production
ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA ENV STRIPE_PUB_KEY pk_85fQ0yMECHNfHTSsZoxZXlPSwSNfA

View File

@@ -1,4 +1,4 @@
FROM node:8 FROM node:10
# Install global packages # Install global packages
RUN npm install -g gulp-cli mocha RUN npm install -g gulp-cli mocha

View File

@@ -1,116 +1,82 @@
{ {
"PORT":3000, "ADMIN_EMAIL": "you@example.com",
"ENABLE_CONSOLE_LOGS_IN_PROD":"false", "AMAZON_PAYMENTS_CLIENT_ID": "CLIENT_ID",
"IP":"0.0.0.0", "AMAZON_PAYMENTS_MODE": "sandbox",
"WEB_CONCURRENCY":1, "AMAZON_PAYMENTS_MWS_KEY": "MWS_KEY",
"BASE_URL":"http://localhost:3000", "AMAZON_PAYMENTS_MWS_SECRET": "MWS_SECRET",
"FACEBOOK_KEY":"123456789012345", "AMAZON_PAYMENTS_SELLER_ID": "SELLER_ID",
"FACEBOOK_SECRET":"aaaabbbbccccddddeeeeffff00001111", "AMPLITUDE_KEY": "AMPLITUDE_KEY",
"GOOGLE_CLIENT_ID":"123456789012345", "AMPLITUDE_SECRET": "AMPLITUDE_SECRET",
"GOOGLE_CLIENT_SECRET":"aaaabbbbccccddddeeeeffff00001111", "BASE_URL": "http://localhost:3000",
"PLAY_API": { "CRON_SAFE_MODE": "false",
"CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111", "CRON_SEMI_SAFE_MODE": "false",
"CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111", "DISABLE_REQUEST_LOGGING": "true",
"ACCESS_TOKEN":"aaaabbbbccccddddeeeeffff00001111", "EMAILS_COMMUNITY_MANAGER_EMAIL": "admin@habitica.com",
"REFRESH_TOKEN":"aaaabbbbccccddddeeeeffff00001111" "EMAILS_PRESS_ENQUIRY_EMAIL": "admin@habitica.com",
}, "EMAILS_TECH_ASSISTANCE_EMAIL": "admin@habitica.com",
"NODE_DB_URI":"mongodb://localhost/habitrpg", "EMAIL_SERVER_AUTH_PASSWORD": "password",
"TEST_DB_URI":"mongodb://localhost/habitrpg_test", "EMAIL_SERVER_AUTH_USER": "user",
"NODE_ENV":"development", "EMAIL_SERVER_URL": "http://example.com",
"ENABLE_CONSOLE_LOGS_IN_TEST": "false", "ENABLE_CONSOLE_LOGS_IN_PROD": "false",
"CRON_SAFE_MODE":"false", "ENABLE_CONSOLE_LOGS_IN_TEST": "false",
"CRON_SEMI_SAFE_MODE":"false", "FACEBOOK_KEY": "123456789012345",
"MAINTENANCE_MODE": "false", "FACEBOOK_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"SESSION_SECRET":"YOUR SECRET HERE", "FLAG_REPORT_EMAIL": "email@example.com, email2@example.com",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891", "GA_ID": "GA_ID",
"SESSION_SECRET_IV": "12345678912345678912345678912345", "GOOGLE_CLIENT_ID": "123456789012345",
"ADMIN_EMAIL": "you@example.com", "GOOGLE_CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"SMTP_USER":"user@example.com", "IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
"SMTP_PASS":"password", "IGNORE_REDIRECT": "true",
"SMTP_SERVICE":"Gmail", "ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"SMTP_HOST":"example.com", "LOGGLY_CLIENT_TOKEN": "token",
"SMTP_PORT": 587, "LOGGLY_SUBDOMAIN": "example-subdomain",
"SMTP_TLS": true, "LOGGLY_TOKEN": "example-token",
"STRIPE_API_KEY":"aaaabbbbccccddddeeeeffff00001111", "MAINTENANCE_MODE": "false",
"STRIPE_PUB_KEY":"22223333444455556666777788889999", "NODE_DB_URI": "mongodb://localhost/habitrpg",
"NEW_RELIC_LICENSE_KEY":"NEW_RELIC_LICENSE_KEY", "NODE_ENV": "development",
"NEW_RELIC_NO_CONFIG_FILE":"true", "PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", "PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
"NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", "PAYPAL_BILLING_PLANS_basic_3mo": "basic_3mo",
"GA_ID": "GA_ID", "PAYPAL_BILLING_PLANS_basic_6mo": "basic_6mo",
"AMPLITUDE_KEY": "AMPLITUDE_KEY", "PAYPAL_BILLING_PLANS_basic_earned": "basic_earned",
"AMPLITUDE_SECRET": "AMPLITUDE_SECRET", "PAYPAL_BILLING_PLANS_google_6mo": "google_6mo",
"AMAZON_PAYMENTS": { "PAYPAL_CLIENT_ID": "client_id",
"SELLER_ID": "SELLER_ID", "PAYPAL_CLIENT_SECRET": "client_secret",
"CLIENT_ID": "CLIENT_ID", "PAYPAL_EXPERIENCE_PROFILE_ID": "xp_profile_id",
"MWS_KEY": "", "PAYPAL_MODE": "sandbox",
"MWS_SECRET": "", "PLAY_API_ACCESS_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
"MODE": "sandbox" "PLAY_API_CLIENT_ID": "aaaabbbbccccddddeeeeffff00001111",
}, "PLAY_API_CLIENT_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"FLAG_REPORT_EMAIL": "email@mod.com,email2@mod.com", "PLAY_API_REFRESH_TOKEN": "aaaabbbbccccddddeeeeffff00001111",
"EMAIL_SERVER": { "PORT": 3000,
"url": "http://example.com", "PUSH_CONFIGS_APN_ENABLED": "false",
"authUser": "user", "PUSH_CONFIGS_APN_KEY": "xxxxxxxxxx",
"authPassword": "password" "PUSH_CONFIGS_APN_KEY_ID": "xxxxxxxxxx",
}, "PUSH_CONFIGS_APN_TEAM_ID": "aaabbbcccd",
"S3":{ "PUSH_CONFIGS_FCM_SERVER_API_KEY": "aaabbbcccd",
"bucket":"bucket", "S3_ACCESS_KEY_ID": "accessKeyId",
"accessKeyId":"accessKeyId", "S3_BUCKET": "bucket",
"secretAccessKey":"secretAccessKey" "S3_SECRET_ACCESS_KEY": "secretAccessKey",
}, "SESSION_SECRET": "YOUR SECRET HERE",
"SLACK_URL": "https://hooks.slack.com/services/some-url", "SESSION_SECRET_IV": "12345678912345678912345678912345",
"TRANSIFEX_SLACK_CHANNEL": "transifex", "SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"PAYPAL":{ "SITE_HTTP_AUTH_ENABLED": "false",
"billing_plans": { "SITE_HTTP_AUTH_PASSWORD": "password",
"basic_earned":"basic_earned", "SITE_HTTP_AUTH_USERNAME": "admin",
"basic_3mo":"basic_3mo", "SLACK_FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"basic_6mo":"basic_6mo", "SLACK_FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"google_6mo":"google_6mo", "SLACK_SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id",
"basic_12mo":"basic_12mo" "SLACK_URL": "https://hooks.slack.com/services/some-url",
}, "SMTP_HOST": "example.com",
"mode":"sandbox", "SMTP_PASS": "password",
"client_id":"client_id", "SMTP_PORT": 587,
"client_secret":"client_secret", "SMTP_SERVICE": "Gmail",
"experience_profile_id": "" "SMTP_TLS": "true",
}, "SMTP_USER": "user@example.com",
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/", "STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"LOGGLY_TOKEN": "token", "STRIPE_PUB_KEY": "22223333444455556666777788889999",
"LOGGLY_CLIENT_TOKEN": "token", "TEST_DB_URI": "mongodb://localhost/habitrpg_test",
"LOGGLY_ACCOUNT": "account", "TRANSIFEX_SLACK_CHANNEL": "transifex",
"PUSH_CONFIGS": { "WEB_CONCURRENCY": 1,
"GCM_SERVER_API_KEY": "", "SKIP_SSL_CHECK_KEY": "key"
"APN_ENABLED": "false",
"APN_KEY_ID": "xxxxxxxxxx",
"APN_KEY": "xxxxxxxxxx",
"APN_TEAM_ID": "aaabbbcccd",
"FCM_SERVER_API_KEY": ""
},
"SITE_HTTP_AUTH": {
"ENABLED": "false",
"USERNAME": "admin",
"PASSWORD": "password"
},
"SLACK": {
"FLAGGING_URL": "https://hooks.slack.com/services/id/id/id",
"FLAGGING_FOOTER_LINK": "https://habitrpg.github.io/flag-o-rama/",
"SUBSCRIPTIONS_URL": "https://hooks.slack.com/services/id/id/id"
},
"ITUNES_SHARED_SECRET": "aaaabbbbccccddddeeeeffff00001111",
"EMAILS" : {
"COMMUNITY_MANAGER_EMAIL" : "admin@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "admin@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
"SUBDOMAIN" : "exmaple-subdomain"
},
"KAFKA": {
"GROUP_ID": "",
"CLOUDKARAFKA_BROKERS": "",
"CLOUDKARAFKA_USERNAME": "",
"CLOUDKARAFKA_PASSWORD": "",
"CLOUDKARAFKA_TOPIC_PREFIX": ""
},
"MIGRATION_CONNECT_STRING": "mongodb://localhost:27017/habitrpg?auto_reconnect=true"
} }

View File

@@ -167,7 +167,7 @@ gulp.task('test:content:safe', gulp.series('test:prepare:build', (cb) => {
gulp.task('test:api:unit', (done) => { gulp.task('test:api:unit', (done) => {
let runner = exec( let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'), testBin('istanbul cover --dir coverage/api-unit node_modules/mocha/bin/_mocha -- test/api/unit --recursive --require ./test/helpers/start-server'),
(err) => { (err) => {
if (err) { if (err) {
process.exit(1); process.exit(1);
@@ -180,12 +180,12 @@ gulp.task('test:api:unit', (done) => {
}); });
gulp.task('test:api:unit:watch', () => { gulp.task('test:api:unit:watch', () => {
return gulp.watch(['website/server/libs/*', 'test/api/v3/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit', done => done())); return gulp.watch(['website/server/libs/*', 'test/api/unit/**/*', 'website/server/controllers/**/*'], gulp.series('test:api:unit', done => done()));
}); });
gulp.task('test:api-v3:integration', (done) => { gulp.task('test:api-v3:integration', (done) => {
let runner = exec( let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'), testBin('istanbul cover --dir coverage/api-v3-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v3/integration --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024}, {maxBuffer: 500 * 1024},
(err) => { (err) => {
if (err) { if (err) {
@@ -217,7 +217,7 @@ gulp.task('test:api-v3:integration:separate-server', (done) => {
gulp.task('test:api-v4:integration', (done) => { gulp.task('test:api-v4:integration', (done) => {
let runner = exec( let runner = exec(
testBin('node_modules/.bin/istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'), testBin('istanbul cover --dir coverage/api-v4-integration --report lcovonly node_modules/mocha/bin/_mocha -- test/api/v4 --recursive --require ./test/helpers/start-server'),
{maxBuffer: 500 * 1024}, {maxBuffer: 500 * 1024},
(err) => { (err) => {
if (err) { if (err) {
@@ -254,4 +254,4 @@ gulp.task('test:api-v3', gulp.series(
'test:api:unit', 'test:api:unit',
'test:api-v3:integration', 'test:api-v3:integration',
done => done() done => done()
)); ));

View File

@@ -0,0 +1,110 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20181231_nye';
import { model as User } from '../../../website/server/models/user';
import mongoose from 'mongoose';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {'flags.newStuff': true};
let push;
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2018',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2017',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2016',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2015',
_id: uuid(),
},
];
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye2014',
_id: uuid(),
},
];
} else {
set['items.gear.owned.head_special_nye'] = false;
push = [
{
type: 'marketGear',
path: 'gear.flat.head_special_nye',
_id: uuid(),
},
];
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -17,7 +17,7 @@ function setUpServer () {
setUpServer(); setUpServer();
// Replace this with your migration // Replace this with your migration
const processUsers = require('./users/20181122_turkey_day.js'); const processUsers = require('./archive/2018/20181231_nye.js');
processUsers() processUsers()
.then(function success () { .then(function success () {
process.exit(0); process.exit(0);

View File

@@ -0,0 +1,61 @@
/* eslint-disable no-console */
import { sendTxn } from '../../../website/server/libs/email';
import { model as User } from '../../website/server/models/user';
import moment from 'moment';
import nconf from 'nconf';
const BASE_URL = nconf.get('BASE_URL');
const EMAIL_SLUG = 'mandrill-email-slug'; // Set email template to send
const MIGRATION_NAME = 'bulk-email';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
sendTxn(
user,
EMAIL_SLUG,
[{name: 'BASE_URL', content: BASE_URL}] // Add variables from template
);
return await User.update({_id: user._id}, {$set: {migration: MIGRATION_NAME}}).exec();
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: moment().subtract(2, 'weeks').toDate()}, // customize or remove to target different populations
};
const fields = {
_id: 1,
auth: 1,
preferences: 1,
profile: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
const MIGRATION_NAME = 'mystery_items_201811'; const MIGRATION_NAME = 'mystery_items_201812';
const MYSTERY_ITEMS = ['head_mystery_201811', 'weapon_mystery_201811']; const MYSTERY_ITEMS = ['headAccessory_mystery_201812', 'back_mystery_201812'];
import { model as User } from '../../website/server/models/user'; import { model as User } from '../../website/server/models/user';
import { model as UserNotification } from '../../website/server/models/userNotification'; import { model as UserNotification } from '../../website/server/models/userNotification';

View File

@@ -0,0 +1,81 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20181203_take_this';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push;
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.back_special_takeThis !== 'undefined') {
push = false;
} else if (typeof user.items.gear.owned.body_special_takeThis !== 'undefined') {
set['items.gear.owned.back_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.back_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.head_special_takeThis !== 'undefined') {
set['items.gear.owned.body_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.body_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.armor_special_takeThis !== 'undefined') {
set['items.gear.owned.head_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.head_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.weapon_special_takeThis !== 'undefined') {
set['items.gear.owned.armor_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.armor_special_takeThis', _id: uuid()}};
} else if (typeof user.items.gear.owned.shield_special_takeThis !== 'undefined') {
set['items.gear.owned.weapon_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.weapon_special_takeThis', _id: uuid()}};
} else {
set['items.gear.owned.shield_special_takeThis'] = false;
push = {pinnedItems: {type: 'marketGear', path: 'gear.flat.shield_special_takeThis', _id: uuid()}};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
} else {
return await User.update({_id: user._id}, {$set: set}).exec();
}
}
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
challenges: '00708425-d477-41a5-bf27-6270466e7976',
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

11652
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.74.0", "version": "4.80.8",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@slack/client": "^3.8.1", "@slack/client": "^3.8.1",
@@ -33,7 +33,7 @@
"compression": "^1.7.2", "compression": "^1.7.2",
"cookie-session": "^1.2.0", "cookie-session": "^1.2.0",
"coupon-code": "^0.4.5", "coupon-code": "^0.4.5",
"cross-env": "^5.1.5", "cross-env": "^5.2.0",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"csv-stringify": "^4.3.1", "csv-stringify": "^4.3.1",
"cwait": "^1.1.1", "cwait": "^1.1.1",
@@ -67,7 +67,7 @@
"nconf": "^0.10.0", "nconf": "^0.10.0",
"node-gcm": "^1.0.2", "node-gcm": "^1.0.2",
"node-sass": "^4.9.0", "node-sass": "^4.9.0",
"nodemailer": "^4.6.4", "nodemailer": "^5.0.0",
"ora": "^3.0.0", "ora": "^3.0.0",
"pageres": "^4.1.1", "pageres": "^4.1.1",
"passport": "^0.4.0", "passport": "^0.4.0",
@@ -80,7 +80,7 @@
"ps-tree": "^1.0.0", "ps-tree": "^1.0.0",
"pug": "^2.0.3", "pug": "^2.0.3",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"sass-loader": "^7.0.0", "sass-loader": "^7.0.3",
"shelljs": "^0.8.2", "shelljs": "^0.8.2",
"short-uuid": "^3.0.0", "short-uuid": "^3.0.0",
"smartbanner.js": "^1.9.1", "smartbanner.js": "^1.9.1",
@@ -90,7 +90,7 @@
"svg-url-loader": "^2.3.2", "svg-url-loader": "^2.3.2",
"svgo": "^1.0.5", "svgo": "^1.0.5",
"svgo-loader": "^2.1.0", "svgo-loader": "^2.1.0",
"universal-analytics": "^0.4.16", "universal-analytics": "^0.4.17",
"update": "^0.7.4", "update": "^0.7.4",
"upgrade": "^1.1.0", "upgrade": "^1.1.0",
"url-loader": "^1.0.0", "url-loader": "^1.0.0",
@@ -107,15 +107,15 @@
"vuedraggable": "^2.15.0", "vuedraggable": "^2.15.0",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
"webpack": "^3.12.0", "webpack": "^3.12.0",
"webpack-merge": "^4.0.0", "webpack-merge": "^4.1.3",
"winston": "^2.4.2", "winston": "^2.4.3",
"winston-loggly-bulk": "^2.0.2", "winston-loggly-bulk": "^2.0.2",
"xml2js": "^0.4.4" "xml2js": "^0.4.4"
}, },
"private": true, "private": true,
"engines": { "engines": {
"node": "^8.9.4", "node": "^10",
"npm": "^5.6.0" "npm": "^6"
}, },
"scripts": { "scripts": {
"lint": "eslint --ext .js,.vue .", "lint": "eslint --ext .js,.vue .",
@@ -144,13 +144,13 @@
"apidoc": "gulp apidoc" "apidoc": "gulp apidoc"
}, },
"devDependencies": { "devDependencies": {
"@vue/test-utils": "^1.0.0-beta.16", "@vue/test-utils": "^1.0.0-beta.19",
"babel-plugin-istanbul": "^4.1.6", "babel-plugin-istanbul": "^4.1.6",
"babel-plugin-syntax-object-rest-spread": "^6.13.0", "babel-plugin-syntax-object-rest-spread": "^6.13.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"chalk": "^2.4.1", "chalk": "^2.4.1",
"chromedriver": "^2.38.3", "chromedriver": "^2.40.0",
"connect-history-api-fallback": "^1.1.0", "connect-history-api-fallback": "^1.1.0",
"coveralls": "^3.0.1", "coveralls": "^3.0.1",
"cross-spawn": "^6.0.5", "cross-spawn": "^6.0.5",
@@ -164,7 +164,7 @@
"expect.js": "^0.3.1", "expect.js": "^0.3.1",
"http-proxy-middleware": "^0.19.0", "http-proxy-middleware": "^0.19.0",
"istanbul": "^1.1.0-alpha.1", "istanbul": "^1.1.0-alpha.1",
"karma": "^3.0.0", "karma": "^3.1.3",
"karma-babel-preprocessor": "^7.0.0", "karma-babel-preprocessor": "^7.0.0",
"karma-chai-plugins": "^0.9.0", "karma-chai-plugins": "^0.9.0",
"karma-chrome-launcher": "^2.2.0", "karma-chrome-launcher": "^2.2.0",
@@ -180,7 +180,7 @@
"mocha": "^5.1.1", "mocha": "^5.1.1",
"monk": "^6.0.6", "monk": "^6.0.6",
"nightwatch": "^0.9.21", "nightwatch": "^0.9.21",
"puppeteer": "^1.4.0", "puppeteer": "^1.5.0",
"require-again": "^2.0.0", "require-again": "^2.0.0",
"selenium-server": "^3.12.0", "selenium-server": "^3.12.0",
"sinon": "^6.3.5", "sinon": "^6.3.5",
@@ -190,8 +190,5 @@
"webpack-dev-middleware": "^2.0.5", "webpack-dev-middleware": "^2.0.5",
"webpack-hot-middleware": "^2.22.2" "webpack-hot-middleware": "^2.22.2"
}, },
"optionalDependencies": { "optionalDependencies": {}
"memwatch-next": "^0.3.0",
"node-rdkafka": "^2.3.0"
}
} }

View File

@@ -27,12 +27,13 @@ async function _deleteAmplitudeData (userId, email) {
if (response) console.log(`${response.status} ${response.statusText}`); if (response) console.log(`${response.status} ${response.statusText}`);
} }
async function _deleteHabiticaData (user) { async function _deleteHabiticaData (user, email) {
await User.update( await User.update(
{_id: user._id}, {_id: user._id},
{$set: { {$set: {
'auth.local.passwordHashMethod': 'bcrypt', 'auth.local.email': email,
'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW', 'auth.local.hashed_password': '$2a$10$QDnNh1j1yMPnTXDEOV38xOePEWFd4X8DSYwAM8XTmqmacG5X0DKjW',
'auth.local.passwordHashMethod': 'bcrypt',
}} }}
); );
const response = await axios.delete( const response = await axios.delete(
@@ -75,7 +76,7 @@ async function _processEmailAddress (email) {
} else { } else {
for (const user of users) { for (const user of users) {
await _deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop await _deleteAmplitudeData(user._id, email); // eslint-disable-line no-await-in-loop
await _deleteHabiticaData(user); // eslint-disable-line no-await-in-loop await _deleteHabiticaData(user, email); // eslint-disable-line no-await-in-loop
} }
} }
} }

View File

@@ -12,28 +12,27 @@ const nconf = require('nconf');
const _ = require('lodash'); const _ = require('lodash');
const paypal = require('paypal-rest-sdk'); const paypal = require('paypal-rest-sdk');
const blocks = require('../website/common').content.subscriptionBlocks; const blocks = require('../website/common').content.subscriptionBlocks;
const live = nconf.get('PAYPAL:mode') === 'live'; const live = nconf.get('PAYPAL_MODE') === 'live';
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json'))); nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../config.json')));
let OP = 'create'; // list create update remove let OP = 'create'; // list get update create create-webprofile
paypal.configure({ paypal.configure({
mode: nconf.get('PAYPAL:mode'), // sandbox or live mode: nconf.get('PAYPAL_MODE'), // sandbox or live
client_id: nconf.get('PAYPAL:client_id'), client_id: nconf.get('PAYPAL_CLIENT_ID'),
client_secret: nconf.get('PAYPAL:client_secret'), client_secret: nconf.get('PAYPAL_CLIENT_SECRET'),
}); });
// https://developer.paypal.com/docs/api/#billing-plans-and-agreements // https://developer.paypal.com/docs/api/#billing-plans-and-agreements
let billingPlanTitle = 'Habitica Subscription'; let billingPlanTitle = 'Habitica Subscription';
let billingPlanAttributes = { let billingPlanAttributes = {
name: billingPlanTitle,
description: billingPlanTitle, description: billingPlanTitle,
type: 'INFINITE', type: 'INFINITE',
merchant_preferences: { merchant_preferences: {
auto_bill_amount: 'yes', auto_bill_amount: 'yes',
cancel_url: live ? 'https://habitica.com' : 'http://localhost:3000', cancel_url: live ? 'https://habitica.com' : 'http://localhost:3000',
return_url: `${live ? 'https://habitica.com' : 'http://localhost:3000' }/paypal/subscribe/success`, return_url: `${live ? 'https://habitica.com' : 'http://localhost:3000'}/paypal/subscribe/success`,
}, },
payment_definitions: [{ payment_definitions: [{
type: 'REGULAR', type: 'REGULAR',
@@ -45,7 +44,7 @@ let billingPlanAttributes = {
_.each(blocks, (block) => { _.each(blocks, (block) => {
block.definition = _.cloneDeep(billingPlanAttributes); block.definition = _.cloneDeep(billingPlanAttributes);
_.merge(block.definition.payment_definitions[0], { _.merge(block.definition.payment_definitions[0], {
name: `${billingPlanTitle } ($${block.price} every ${block.months} months, recurring)`, name: `${billingPlanTitle} ($${block.price} every ${block.months} months, recurring)`,
frequency_interval: `${block.months}`, frequency_interval: `${block.months}`,
amount: { amount: {
currency: 'USD', currency: 'USD',
@@ -63,7 +62,7 @@ switch (OP) {
}); });
break; break;
case 'get': case 'get':
paypal.billingPlan.get(nconf.get('PAYPAL:billing_plans:12'), (err, plan) => { paypal.billingPlan.get(nconf.get('PAYPAL_BILLING_PLANS_basic_12mo'), (err, plan) => {
console.log({err, plan}); console.log({err, plan});
}); });
break; break;
@@ -75,7 +74,7 @@ switch (OP) {
cancel_url: 'https://habitica.com', cancel_url: 'https://habitica.com',
}, },
}; };
paypal.billingPlan.update(nconf.get('PAYPAL:billing_plans:12'), updatePayload, (err, res) => { paypal.billingPlan.update(nconf.get('PAYPAL_BILLING_PLANS_basic_12mo'), updatePayload, (err, res) => {
console.log({err, plan: res}); console.log({err, plan: res});
}); });
break; break;
@@ -101,9 +100,6 @@ switch (OP) {
}); });
}); });
break; break;
case 'remove': break;
case 'create-webprofile': case 'create-webprofile':
let webexpinfo = { let webexpinfo = {
name: 'HabiticaProfile', name: 'HabiticaProfile',
@@ -116,4 +112,4 @@ switch (OP) {
console.log(error, result); console.log(error, result);
}); });
break; break;
} }

View File

@@ -5,7 +5,9 @@ import {
BadRequest, BadRequest,
InternalServerError, InternalServerError,
NotFound, NotFound,
NotificationNotFound,
} from '../../../../website/server/libs/errors'; } from '../../../../website/server/libs/errors';
import i18n from '../../../../website/common/script/i18n';
describe('Custom Errors', () => { describe('Custom Errors', () => {
describe('CustomError', () => { describe('CustomError', () => {
@@ -66,6 +68,23 @@ describe('Custom Errors', () => {
expect(notAuthorizedError.message).to.eql('Custom Error Message'); expect(notAuthorizedError.message).to.eql('Custom Error Message');
}); });
describe('NotificationNotFound', () => {
it('is an instance of NotFound', () => {
const notificationNotFoundErr = new NotificationNotFound();
expect(notificationNotFoundErr).to.be.an.instanceOf(NotFound);
});
it('it returns an http code of 404', () => {
const notificationNotFoundErr = new NotificationNotFound();
expect(notificationNotFoundErr.httpCode).to.eql(404);
});
it('returns a standard message', () => {
const notificationNotFoundErr = new NotificationNotFound();
expect(notificationNotFoundErr.message).to.eql(i18n.t('messageNotificationNotFound'));
});
});
}); });
describe('BadRequest', () => { describe('BadRequest', () => {

View File

@@ -6,6 +6,7 @@ import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user'; import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common'; import common from '../../../../../website/common';
import moment from 'moment'; import moment from 'moment';
import {mockFindById, restoreFindById} from '../../../../helpers/mongoose.helper';
const i18n = common.i18n; const i18n = common.i18n;
@@ -49,7 +50,7 @@ describe('Apple Payments', () => {
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false); .returns(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers)) await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -61,7 +62,7 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub.restore(); iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]); iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData').returns([]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers)) await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -71,7 +72,7 @@ describe('Apple Payments', () => {
it('errors if the user cannot purchase gems', async () => { it('errors if the user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').resolves(false); sinon.stub(user, 'canGetGems').resolves(false);
await expect(applePayments.verifyGemPurchase(user, receipt, headers)) await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -89,7 +90,7 @@ describe('Apple Payments', () => {
transactionId: token, transactionId: token,
}]); }]);
await expect(applePayments.verifyGemPurchase(user, receipt, headers)) await expect(applePayments.verifyGemPurchase({user, receipt, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -131,7 +132,7 @@ describe('Apple Payments', () => {
}]); }]);
sinon.stub(user, 'canGetGems').resolves(true); sinon.stub(user, 'canGetGems').resolves(true);
await applePayments.verifyGemPurchase(user, receipt, headers); await applePayments.verifyGemPurchase({user, receipt, headers});
expect(iapSetupStub).to.be.calledOnce; expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce;
@@ -151,6 +152,38 @@ describe('Apple Payments', () => {
user.canGetGems.restore(); user.canGetGems.restore();
}); });
}); });
it('gifts gems', async () => {
const receivingUser = new User();
await receivingUser.save();
mockFindById(receivingUser);
iapGetPurchaseDataStub.restore();
iapGetPurchaseDataStub = sinon.stub(iapModule, 'getPurchaseData')
.returns([{productId: gemsCanPurchase[0].productId,
transactionId: token,
}]);
const gift = {uuid: receivingUser._id};
await applePayments.verifyGemPurchase({user, gift, 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: receivingUser,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
amount: gemsCanPurchase[0].amount,
headers,
});
restoreFindById();
});
}); });
describe('subscribe', () => { describe('subscribe', () => {

View File

@@ -6,6 +6,7 @@ import iap from '../../../../../website/server/libs/inAppPurchases';
import {model as User} from '../../../../../website/server/models/user'; import {model as User} from '../../../../../website/server/models/user';
import common from '../../../../../website/common'; import common from '../../../../../website/common';
import moment from 'moment'; import moment from 'moment';
import {mockFindById, restoreFindById} from '../../../../helpers/mongoose.helper';
const i18n = common.i18n; const i18n = common.i18n;
@@ -44,7 +45,7 @@ describe('Google Payments', () => {
iapIsValidatedStub = sinon.stub(iapModule, 'isValidated') iapIsValidatedStub = sinon.stub(iapModule, 'isValidated')
.returns(false); .returns(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -55,7 +56,7 @@ describe('Google Payments', () => {
it('should throw an error if productId is invalid', async () => { it('should throw an error if productId is invalid', async () => {
receipt = `{"token": "${token}", "productId": "invalid"}`; receipt = `{"token": "${token}", "productId": "invalid"}`;
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -66,7 +67,7 @@ describe('Google Payments', () => {
it('should throw an error if user cannot purchase gems', async () => { it('should throw an error if user cannot purchase gems', async () => {
sinon.stub(user, 'canGetGems').resolves(false); sinon.stub(user, 'canGetGems').resolves(false);
await expect(googlePayments.verifyGemPurchase(user, receipt, signature, headers)) await expect(googlePayments.verifyGemPurchase({user, receipt, signature, headers}))
.to.eventually.be.rejected.and.to.eql({ .to.eventually.be.rejected.and.to.eql({
httpCode: 401, httpCode: 401,
name: 'NotAuthorized', name: 'NotAuthorized',
@@ -78,7 +79,7 @@ describe('Google Payments', () => {
it('purchases gems', async () => { it('purchases gems', async () => {
sinon.stub(user, 'canGetGems').resolves(true); sinon.stub(user, 'canGetGems').resolves(true);
await googlePayments.verifyGemPurchase(user, receipt, signature, headers); await googlePayments.verifyGemPurchase({user, receipt, signature, headers});
expect(iapSetupStub).to.be.calledOnce; expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce; expect(iapValidateStub).to.be.calledOnce;
@@ -99,6 +100,34 @@ describe('Google Payments', () => {
expect(user.canGetGems).to.be.calledOnce; expect(user.canGetGems).to.be.calledOnce;
user.canGetGems.restore(); user.canGetGems.restore();
}); });
it('gifts gems', async () => {
const receivingUser = new User();
await receivingUser.save();
mockFindById(receivingUser);
const gift = {uuid: receivingUser._id};
await googlePayments.verifyGemPurchase({user, gift, receipt, signature, headers});
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.GOOGLE, {
data: receipt,
signature,
});
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(paymentBuyGemsStub).to.be.calledOnce;
expect(paymentBuyGemsStub).to.be.calledWith({
user: receivingUser,
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
amount: 5.25,
headers,
});
restoreFindById();
});
}); });
describe('subscribe', () => { describe('subscribe', () => {

View File

@@ -15,6 +15,7 @@ describe('checkout', () => {
function getPaypalCreateOptions (description, amount) { function getPaypalCreateOptions (description, amount) {
return { return {
experience_profile_id: 'xp_profile_id',
intent: 'sale', intent: 'sale',
payer: { payment_method: 'Paypal' }, payer: { payment_method: 'Paypal' },
redirect_urls: { redirect_urls: {

View File

@@ -110,7 +110,7 @@ describe('slack', () => {
}); });
it('noops if no flagging url is provided', () => { it('noops if no flagging url is provided', () => {
sandbox.stub(nconf, 'get').withArgs('SLACK:FLAGGING_URL').returns(''); sandbox.stub(nconf, 'get').withArgs('SLACK_FLAGGING_URL').returns('');
sandbox.stub(logger, 'error'); sandbox.stub(logger, 'error');
let reRequiredSlack = requireAgain('../../../../website/server/libs/slack'); let reRequiredSlack = requireAgain('../../../../website/server/libs/slack');

View File

@@ -73,6 +73,56 @@ describe('redirects middleware', () => {
expect(res.redirect).to.have.not.been.called; expect(res.redirect).to.have.not.been.called;
}); });
it('does not redirect if passed skip ssl request param is passed with corrrect key', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key');
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
req.query.skipSSLCheck = 'test-key';
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.have.not.been.called;
});
it('does redirect if skip ssl request param is passed with incorrrect key', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns('test-key');
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front?skipSSLCheck=INVALID';
req.query.skipSSLCheck = 'INVALID';
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith('https://habitica.com/static/front?skipSSLCheck=INVALID');
});
it('does redirect if skip ssl check key is not set', () => {
let nconfStub = sandbox.stub(nconf, 'get');
nconfStub.withArgs('BASE_URL').returns('https://habitica.com');
nconfStub.withArgs('IS_PROD').returns(true);
nconfStub.withArgs('SKIP_SSL_CHECK_KEY').returns(null);
req.header = sandbox.stub().withArgs('x-forwarded-proto').returns('http');
req.originalUrl = '/static/front';
req.query.skipSSLCheck = 'INVALID';
const attachRedirects = requireAgain(pathToRedirectsMiddleware);
attachRedirects.forceSSL(req, res, next);
expect(res.redirect).to.be.calledOnce;
expect(res.redirect).to.be.calledWith('https://habitica.com/static/front');
});
}); });
context('forceHabitica', () => { context('forceHabitica', () => {

View File

@@ -32,8 +32,19 @@ describe('Group Model', () => {
privacy: 'private', privacy: 'private',
}); });
let _progress = {
up: 10,
down: 8,
collectedItems: 5,
};
questLeader = new User({ questLeader = new User({
party: { _id: party._id }, party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Quest Leader' }, profile: { name: 'Quest Leader' },
items: { items: {
quests: { quests: {
@@ -45,20 +56,40 @@ describe('Group Model', () => {
party.leader = questLeader._id; party.leader = questLeader._id;
participatingMember = new User({ participatingMember = new User({
party: { _id: party._id }, party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Participating Member' }, profile: { name: 'Participating Member' },
}); });
sleepingParticipatingMember = new User({ sleepingParticipatingMember = new User({
party: { _id: party._id }, party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Sleeping Participating Member' }, profile: { name: 'Sleeping Participating Member' },
preferences: { sleep: true }, preferences: { sleep: true },
}); });
nonParticipatingMember = new User({ nonParticipatingMember = new User({
party: { _id: party._id }, party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Non-Participating Member' }, profile: { name: 'Non-Participating Member' },
}); });
undecidedMember = new User({ undecidedMember = new User({
party: { _id: party._id }, party: {
_id: party._id,
quest: {
progress: _progress,
},
},
profile: { name: 'Undecided Member' }, profile: { name: 'Undecided Member' },
}); });
@@ -1163,16 +1194,17 @@ describe('Group Model', () => {
expect(party.quest.members).to.eql(expectedQuestMembers); expect(party.quest.members).to.eql(expectedQuestMembers);
}); });
it('applies updates to user object directly if user is participating', async () => { it('applies updates to user object directly if user is participating (without resetting progress, except progress.down)', async () => {
await party.startQuest(participatingMember); await party.startQuest(participatingMember);
expect(participatingMember.party.quest.key).to.eql('whale'); expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(0); expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0); expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(participatingMember.party.quest.completed).to.eql(null); expect(participatingMember.party.quest.completed).to.eql(null);
}); });
it('applies updates to other participating members', async () => { it('applies updates to other participating members (without resetting progress, except progress.down)', async () => {
await party.startQuest(nonParticipatingMember); await party.startQuest(nonParticipatingMember);
questLeader = await User.findById(questLeader._id); questLeader = await User.findById(questLeader._id);
@@ -1180,18 +1212,21 @@ describe('Group Model', () => {
sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id); sleepingParticipatingMember = await User.findById(sleepingParticipatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale'); expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(0); expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(0); expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(participatingMember.party.quest.completed).to.eql(null); expect(participatingMember.party.quest.completed).to.eql(null);
expect(sleepingParticipatingMember.party.quest.key).to.eql('whale'); expect(sleepingParticipatingMember.party.quest.key).to.eql('whale');
expect(sleepingParticipatingMember.party.quest.progress.up).to.eql(10);
expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0); expect(sleepingParticipatingMember.party.quest.progress.down).to.eql(0);
expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(0); expect(sleepingParticipatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(sleepingParticipatingMember.party.quest.completed).to.eql(null); expect(sleepingParticipatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale'); expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.up).to.eql(10);
expect(questLeader.party.quest.progress.down).to.eql(0); expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collectedItems).to.eql(0); expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
expect(questLeader.party.quest.completed).to.eql(null); expect(questLeader.party.quest.completed).to.eql(null);
}); });
@@ -1202,6 +1237,9 @@ describe('Group Model', () => {
undecidedMember = await User.findById(undecidedMember._id); undecidedMember = await User.findById(undecidedMember._id);
expect(nonParticipatingMember.party.quest.key).to.not.eql('whale'); expect(nonParticipatingMember.party.quest.key).to.not.eql('whale');
expect(nonParticipatingMember.party.quest.progress.up).to.eql(10);
expect(nonParticipatingMember.party.quest.progress.down).to.eql(8);
expect(nonParticipatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(undecidedMember.party.quest.key).to.not.eql('whale'); expect(undecidedMember.party.quest.key).to.not.eql('whale');
}); });
@@ -1369,8 +1407,9 @@ describe('Group Model', () => {
let userQuest = participatingMember.party.quest; let userQuest = participatingMember.party.quest;
expect(userQuest.key).to.eql('whale'); expect(userQuest.key).to.eql('whale');
expect(userQuest.progress.up).to.eql(10);
expect(userQuest.progress.down).to.eql(0); expect(userQuest.progress.down).to.eql(0);
expect(userQuest.progress.collectedItems).to.eql(0); expect(userQuest.progress.collectedItems).to.eql(5);
expect(userQuest.completed).to.eql(null); expect(userQuest.completed).to.eql(null);
}); });
@@ -1670,16 +1709,23 @@ describe('Group Model', () => {
}); });
}); });
it('sets user quest object to a clean state', async () => { it('updates participating members quest object to a clean state (except for progress)', async () => {
await party.finishQuest(quest); await party.finishQuest(quest);
let updatedLeader = await User.findById(questLeader._id); questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
expect(updatedLeader.party.quest.completed).to.eql('whale'); expect(questLeader.party.quest.completed).to.eql('whale');
expect(updatedLeader.party.quest.progress.up).to.eql(0); expect(questLeader.party.quest.progress.up).to.eql(10);
expect(updatedLeader.party.quest.progress.down).to.eql(0); expect(questLeader.party.quest.progress.down).to.eql(8);
expect(updatedLeader.party.quest.progress.collectedItems).to.eql(0); expect(questLeader.party.quest.progress.collectedItems).to.eql(5);
expect(updatedLeader.party.quest.RSVPNeeded).to.eql(false); expect(questLeader.party.quest.RSVPNeeded).to.eql(false);
expect(participatingMember.party.quest.completed).to.eql('whale');
expect(participatingMember.party.quest.progress.up).to.eql(10);
expect(participatingMember.party.quest.progress.down).to.eql(8);
expect(participatingMember.party.quest.progress.collectedItems).to.eql(5);
expect(participatingMember.party.quest.RSVPNeeded).to.eql(false);
}); });
}); });

View File

@@ -65,11 +65,11 @@ describe('GET /challenges/:challengeId/export/csv', () => {
const sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id'); const sortedMembers = _.sortBy([members[0], members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n'); const splitRes = res.split('\n');
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak'); expect(splitRes[0]).to.equal('UUID,Display Name,Username,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},${sortedMembers[0].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},${sortedMembers[1].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},${sortedMembers[2].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[4]).to.equal(`${sortedMembers[3]._id},${sortedMembers[3].profile.name},${sortedMembers[3].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[5]).to.equal(''); expect(splitRes[5]).to.equal('');
}); });
@@ -78,10 +78,10 @@ describe('GET /challenges/:challengeId/export/csv', () => {
const res = await members[1].get(`/challenges/${challenge._id}/export/csv`); const res = await members[1].get(`/challenges/${challenge._id}/export/csv`);
const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id'); const sortedMembers = _.sortBy([members[1], members[2], groupLeader], '_id');
const splitRes = res.split('\n'); const splitRes = res.split('\n');
expect(splitRes[0]).to.equal('UUID,name,Task,Value,Notes,Streak,Task,Value,Notes,Streak'); expect(splitRes[0]).to.equal('UUID,Display Name,Username,Task,Value,Notes,Streak,Task,Value,Notes,Streak');
expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[1]).to.equal(`${sortedMembers[0]._id},${sortedMembers[0].profile.name},${sortedMembers[0].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[2]).to.equal(`${sortedMembers[1]._id},${sortedMembers[1].profile.name},${sortedMembers[1].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},habit:Task 1,0,,0,todo:Task 2,0,,0`); expect(splitRes[3]).to.equal(`${sortedMembers[2]._id},${sortedMembers[2].profile.name},${sortedMembers[2].auth.local.username},habit:Task 1,0,,0,todo:Task 2,0,,0`);
expect(splitRes[4]).to.equal(''); expect(splitRes[4]).to.equal('');
}); });
}); });

View File

@@ -106,7 +106,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 400, code: 400,
error: 'BadRequest', error: 'BadRequest',
message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS.COMMUNITY_MANAGER_EMAIL}), message: t('messageCannotFlagSystemMessages', {communityManagerEmail: config.EMAILS_COMMUNITY_MANAGER_EMAIL}),
}); });
// let messages = await members[0].get(`/groups/${group._id}/chat`); // let messages = await members[0].get(`/groups/${group._id}/chat`);
// expect(messages[0].id).to.eql(skillMsg.id); // expect(messages[0].id).to.eql(skillMsg.id);

View File

@@ -333,7 +333,7 @@ describe('Post /groups/:groupId/invite', () => {
.to.eventually.be.rejected.and.eql({ .to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}), message: t('inviteLimitReached', {techAssistanceEmail: nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL')}),
}); });
}); });

View File

@@ -16,7 +16,7 @@ describe('POST /notifications/:notificationId/read', () => {
await expect(user.post(`/notifications/${dummyId}/read`)).to.eventually.be.rejected.and.eql({ await expect(user.post(`/notifications/${dummyId}/read`)).to.eventually.be.rejected.and.eql({
code: 404, code: 404,
error: 'NotFound', error: 'NotificationNotFound',
message: t('messageNotificationNotFound'), message: t('messageNotificationNotFound'),
}); });
}); });

View File

@@ -16,7 +16,7 @@ describe('POST /notifications/:notificationId/see', () => {
await expect(user.post(`/notifications/${dummyId}/see`)).to.eventually.be.rejected.and.eql({ await expect(user.post(`/notifications/${dummyId}/see`)).to.eventually.be.rejected.and.eql({
code: 404, code: 404,
error: 'NotFound', error: 'NotificationNotFound',
message: t('messageNotificationNotFound'), message: t('messageNotificationNotFound'),
}); });
}); });

View File

@@ -18,7 +18,7 @@ describe('POST /notifications/read', () => {
notificationIds: [dummyId], notificationIds: [dummyId],
})).to.eventually.be.rejected.and.eql({ })).to.eventually.be.rejected.and.eql({
code: 404, code: 404,
error: 'NotFound', error: 'NotificationNotFound',
message: t('messageNotificationNotFound'), message: t('messageNotificationNotFound'),
}); });
}); });

View File

@@ -18,7 +18,7 @@ describe('POST /notifications/see', () => {
notificationIds: [dummyId], notificationIds: [dummyId],
})).to.eventually.be.rejected.and.eql({ })).to.eventually.be.rejected.and.eql({
code: 404, code: 404,
error: 'NotFound', error: 'NotificationNotFound',
message: t('messageNotificationNotFound'), message: t('messageNotificationNotFound'),
}); });
}); });

View File

@@ -0,0 +1,67 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #norenewsubscribe', () => {
let endpoint = '/iap/ios/norenew-subscribe';
let sku = 'com.habitrpg.ios.habitica.subscription.3month';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies sub key', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscriptionCode'),
});
});
it('verifies receipt existence', async () => {
await expect(user.post(endpoint, {
sku,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(applePayments, 'noRenewSubscribe').resolves({});
});
afterEach(() => {
applePayments.noRenewSubscribe.restore();
});
it('makes a purchase', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint, {
sku,
transaction: {receipt: 'receipt'},
gift: {
uuid: '1',
},
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeStub.args[0][0].sku).to.eql(sku);
expect(subscribeStub.args[0][0].receipt).to.eql('receipt');
expect(subscribeStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -1,4 +1,4 @@
import {generateUser} from '../../../../../helpers/api-integration/v3'; import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import applePayments from '../../../../../../website/server/libs/payments/apple'; import applePayments from '../../../../../../website/server/libs/payments/apple';
describe('payments : apple #verify', () => { describe('payments : apple #verify', () => {
@@ -9,6 +9,14 @@ describe('payments : apple #verify', () => {
user = await generateUser(); user = await generateUser();
}); });
it('verifies receipt existence', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => { describe('success', () => {
let verifyStub; let verifyStub;
@@ -31,10 +39,31 @@ describe('payments : apple #verify', () => {
}}); }});
expect(verifyStub).to.be.calledOnce; expect(verifyStub).to.be.calledOnce;
expect(verifyStub.args[0][0]._id).to.eql(user._id); expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][1]).to.eql('receipt'); expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][2]['x-api-key']).to.eql(user.apiToken); expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][2]['x-api-user']).to.eql(user._id); expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('gifts a purchase', async () => {
user = await generateUser({
balance: 2,
});
await user.post(endpoint, {
transaction: {
receipt: 'receipt',
},
gift: {
uuid: '1',
}});
expect(verifyStub).to.be.calledOnce;
expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][0].gift.uuid).to.eql('1');
expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
}); });
}); });
}); });

View File

@@ -0,0 +1,97 @@
import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #norenewsubscribe', () => {
let endpoint = '/iap/android/norenew-subscribe';
let sku = 'com.habitrpg.android.habitica.subscription.3month';
let user;
beforeEach(async () => {
user = await generateUser();
});
it('verifies sub key', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingSubscriptionCode'),
});
});
it('verifies receipt existence', async () => {
await expect(user.post(endpoint, {
sku,
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => {
let subscribeStub;
beforeEach(async () => {
subscribeStub = sinon.stub(googlePayments, 'noRenewSubscribe').resolves({});
});
afterEach(() => {
googlePayments.noRenewSubscribe.restore();
});
it('makes a purchase', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint, {
sku,
transaction: {
receipt: 'receipt',
signature: 'signature',
},
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeStub.args[0][0].sku).to.eql(sku);
expect(subscribeStub.args[0][0].receipt).to.eql('receipt');
expect(subscribeStub.args[0][0].signature).to.eql('signature');
expect(subscribeStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('gifts a purchase', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
await user.post(endpoint, {
sku,
transaction: {
receipt: 'receipt',
signature: 'signature',
},
gift: {
uuid: '1',
},
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0].user._id).to.eql(user._id);
expect(subscribeStub.args[0][0].sku).to.eql(sku);
expect(subscribeStub.args[0][0].receipt).to.eql('receipt');
expect(subscribeStub.args[0][0].signature).to.eql('signature');
expect(subscribeStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
});
});

View File

@@ -1,4 +1,4 @@
import {generateUser} from '../../../../../helpers/api-integration/v3'; import {generateUser, translate as t} from '../../../../../helpers/api-integration/v3';
import googlePayments from '../../../../../../website/server/libs/payments/google'; import googlePayments from '../../../../../../website/server/libs/payments/google';
describe('payments : google #verify', () => { describe('payments : google #verify', () => {
@@ -9,6 +9,14 @@ describe('payments : google #verify', () => {
user = await generateUser(); user = await generateUser();
}); });
it('verifies receipt existence', async () => {
await expect(user.post(endpoint)).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingReceipt'),
});
});
describe('success', () => { describe('success', () => {
let verifyStub; let verifyStub;
@@ -30,11 +38,30 @@ describe('payments : google #verify', () => {
}); });
expect(verifyStub).to.be.calledOnce; expect(verifyStub).to.be.calledOnce;
expect(verifyStub.args[0][0]._id).to.eql(user._id); expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][1]).to.eql('receipt'); expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][2]).to.eql('signature'); expect(verifyStub.args[0][0].signature).to.eql('signature');
expect(verifyStub.args[0][3]['x-api-key']).to.eql(user.apiToken); expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][3]['x-api-user']).to.eql(user._id); expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
});
it('gifts a purchase', async () => {
user = await generateUser({
balance: 2,
});
await user.post(endpoint, {
transaction: {receipt: 'receipt', signature: 'signature'},
gift: {uuid: '1'},
});
expect(verifyStub).to.be.calledOnce;
expect(verifyStub.args[0][0].user._id).to.eql(user._id);
expect(verifyStub.args[0][0].receipt).to.eql('receipt');
expect(verifyStub.args[0][0].signature).to.eql('signature');
expect(verifyStub.args[0][0].gift.uuid).to.eql('1');
expect(verifyStub.args[0][0].headers['x-api-key']).to.eql(user.apiToken);
expect(verifyStub.args[0][0].headers['x-api-user']).to.eql(user._id);
}); });
}); });
}); });

View File

@@ -45,7 +45,7 @@ describe('POST /user/auth/local/login', () => {
})).to.eventually.be.rejected.and.eql({ })).to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL'), userId: user._id }), message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
}); });
}); });

View File

@@ -71,7 +71,7 @@ describe('PUT /user/auth/update-email', () => {
})).to.eventually.be.rejected.and.eql({ })).to.eventually.be.rejected.and.eql({
code: 401, code: 401,
error: 'NotAuthorized', error: 'NotAuthorized',
message: t('cannotFulfillReq', { techAssistanceEmail: nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL') }), message: t('cannotFulfillReq', { techAssistanceEmail: nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL') }),
}); });
}); });

View File

@@ -0,0 +1,300 @@
import Avatar from 'client/components/avatar';
import Vue from 'vue';
import generateStore from 'client/store';
context('avatar.vue', () => {
let Constructr;
let vm;
beforeEach(() => {
Constructr = Vue.extend(Avatar);
vm = new Constructr({
propsData: {
member: {
stats: {
buffs: {},
},
preferences: {
hair: {},
},
items: {
gear: {
equipped: {},
},
},
},
},
}).$mount();
vm.$store = generateStore();
});
afterEach(() => {
vm.$destroy();
});
describe('hasClass', () => {
beforeEach(() => {
vm.member = {
stats: { lvl: 17 },
preferences: { disableClasses: true },
flags: { classSelected: false },
};
});
it('accurately reports class status', () => {
expect(vm.hasClass).to.equal(false);
vm.member.preferences.disableClasses = false;
vm.member.flags.classSelected = true;
expect(vm.hasClass).to.equal(true);
});
});
describe('isBuffed', () => {
beforeEach(() => {
vm.member = {
stats: {
buffs: {},
},
};
});
it('accurately reports if buffed', () => {
expect(vm.isBuffed).to.equal(undefined);
vm.member.stats.buffs = { str: 1 };
expect(vm.isBuffed).to.equal(1);
});
});
describe('paddingTop', () => {
beforeEach(() => {
vm.member = {
items: {},
};
});
it('defaults to 28px', () => {
vm.avatarOnly = true;
expect(vm.paddingTop).to.equal('28px');
});
it('is 24.5px if user has a pet', () => {
vm.member.items = {
currentPet: { name: 'Foo' },
};
expect(vm.paddingTop).to.equal('24.5px');
});
it('is 0px if user has a mount', () => {
vm.member.items = {
currentMount: { name: 'Bar' },
};
expect(vm.paddingTop).to.equal('0px');
});
it('can be overriden', () => {
vm.overrideTopPadding = '27px';
expect(vm.paddingTop).to.equal('27px');
});
});
describe('costumeClass', () => {
beforeEach(() => {
vm.member = {
preferences: {},
};
});
it('returns if showing equiped gear', () => {
expect(vm.costumeClass).to.equal('equipped');
});
it('returns if wearing a costume', () => {
vm.member.preferences = { costume: true };
expect(vm.costumeClass).to.equal('costume');
});
});
describe('visualBuffs', () => {
it('returns an array of buffs', () => {
vm.member = {
stats: {
class: 'Warrior',
},
};
expect(vm.visualBuffs).to.include({snowball: 'snowman'});
expect(vm.visualBuffs).to.include({spookySparkles: 'ghost'});
expect(vm.visualBuffs).to.include({shinySeed: 'avatar_floral_Warrior'});
expect(vm.visualBuffs).to.include({seafoam: 'seafoam_star'});
});
});
describe('backgroundClass', () => {
beforeEach(() => {
vm.member.preferences = { background: 'pony' };
});
it('shows the background', () => {
expect(vm.backgroundClass).to.equal('background_pony');
});
it('can be overridden', () => {
vm.overrideAvatarGear = { background: 'character' };
expect(vm.backgroundClass).to.equal('background_character');
});
it('returns to a blank string if not showing background', () => {
vm.withBackground = false;
vm.avatarOnly = true;
expect(vm.backgroundClass).to.equal('');
});
});
describe('specialMountClass', () => {
it('checks if riding a Kangaroo', () => {
vm.member = {
stats: {
class: 'None',
},
items: {},
};
expect(vm.specialMountClass).to.equal(undefined);
vm.member.items = {
currentMount: ['Kangaroo'],
};
expect(vm.specialMountClass).to.equal('offset-kangaroo');
});
});
describe('skinClass', () => {
it('returns current skin color', () => {
vm.member = {
stats: {},
preferences: {
skin: 'blue',
},
};
expect(vm.skinClass).to.equal('skin_blue');
});
it('returns if sleep or not', () => {
vm.member = {
stats: {},
preferences: {
skin: 'blue',
sleep: false,
},
};
expect(vm.skinClass).to.equal('skin_blue');
vm.member.preferences.sleep = true;
expect(vm.skinClass).to.equal('skin_blue_sleep');
});
});
context('methods', () => {
describe('getGearClass', () => {
beforeEach(() => {
vm.member = {
items: {
gear: {
equipped: { Hat: 'Fancy Tophat' },
},
},
preferences: { costume: false },
};
});
it('returns undefined if no match', () => {
expect(vm.getGearClass('foo')).to.equal(undefined);
});
it('returns the matching gear', () => {
expect(vm.getGearClass('Hat')).to.equal('Fancy Tophat');
});
it('can be overridden', () => {
vm.overrideAvatarGear = { Hat: 'Dapper Bowler' };
expect(vm.getGearClass('Hat')).to.equal('Dapper Bowler');
});
});
describe('hideGear', () => {
it('returns no weapon equipped', () => {
vm.member.items.gear.equipped = {};
expect(vm.hideGear('weapon')).to.equal(false);
});
beforeEach(() => {
vm.member = {
items: {
gear: {
equipped: {
weapon: {
baseWeapon: 'Spoon',
twoHanded: false,
},
},
},
},
preferences: { costume: false },
};
});
});
describe('show avatar', () => {
beforeEach(() => {
vm.member = {
stats: {
buffs: {
snowball: false,
seafoam: false,
spookySparkles: false,
shinySeed: false,
},
},
};
});
it('does if not showing visual buffs', () => {
expect(vm.showAvatar()).to.equal(true);
let buffs = vm.member.stats.buffs;
buffs.snowball = true;
expect(vm.showAvatar()).to.equal(false);
buffs.snowball = false;
buffs.spookySparkles = true;
expect(vm.showAvatar()).to.equal(false);
buffs.spookySparkles = false;
buffs.shinySeed = true;
expect(vm.showAvatar()).to.equal(false);
buffs.shinySeed = false;
buffs.seafoam = true;
expect(vm.showAvatar()).to.equal(false);
buffs.seafoam = false;
vm.showVisualBuffs = false;
expect(vm.showAvatar()).to.equal(true);
});
});
});
});

View File

@@ -0,0 +1,76 @@
import { data, gems, buffs, preferences, tasksOrder } from 'client/store/getters/user';
context('user getters', () => {
describe('data', () => {
it('returns the user\'s data', () => {
expect(data({
state: {
user: {
data: {
lvl: 1,
},
},
},
}).lvl).to.equal(1);
});
});
describe('gems', () => {
it('returns the user\'s gems', () => {
expect(gems({
state: {
user: {
data: { balance: 4.5 },
},
},
})).to.equal(18);
});
});
describe('buffs', () => {
it('returns the user\'s buffs', () => {
expect(buffs({
state: {
user: {
data: {
stats: {
buffs: [1],
},
},
},
},
})(0)).to.equal(1);
});
});
describe('preferences', () => {
it('returns the user\'s preferences', () => {
expect(preferences({
state: {
user: {
data: {
preferences: 1,
},
},
},
})).to.equal(1);
});
});
describe('tasksOrder', () => {
it('returns the user\'s tasksOrder', () => {
expect(tasksOrder({
state: {
user: {
tasksOrder: {
masters: 1,
},
},
},
})('master')).to.equal(1);
expect(tasksOrder()).to.not.equal('null');
expect(tasksOrder()).to.not.equal('undefined');
});
});
});

View File

@@ -1,13 +0,0 @@
import { gems as userGems } from 'client/store/getters/user';
describe('userGems getter', () => {
it('returns the user\'s gems', () => {
expect(userGems({
state: {
user: {
data: {balance: 4.5},
},
},
})).to.equal(18);
});
});

View File

@@ -0,0 +1,20 @@
import mongoose from 'mongoose';
export async function mockFindById (response) {
const mockFind = {
select () {
return this;
},
lean () {
return this;
},
exec () {
return Promise.resolve(response);
},
};
sinon.stub(mongoose.Model, 'findById').returns(mockFind);
}
export function restoreFindById () {
return mongoose.Model.findById.restore();
}

View File

@@ -15,11 +15,11 @@ setupNconf(configFile);
// @TODO: Do we need? const CLIENT_VARS = ['language', 'isStaticPage', 'availableLanguages', 'translations', // @TODO: Do we need? const CLIENT_VARS = ['language', 'isStaticPage', 'availableLanguages', 'translations',
// 'FACEBOOK_KEY', 'GOOGLE_CLIENT_ID', 'NODE_ENV', 'BASE_URL', 'GA_ID', // 'FACEBOOK_KEY', 'GOOGLE_CLIENT_ID', 'NODE_ENV', 'BASE_URL', 'GA_ID',
// 'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY', // 'AMAZON_PAYMENTS', 'STRIPE_PUB_KEY', 'AMPLITUDE_KEY',
// 'worldDmg', 'mods', 'IS_MOBILE', 'PUSHER:KEY', 'PUSHER:ENABLED']; // 'worldDmg', 'mods', 'IS_MOBILE'];
const AMAZON_SELLER_ID = nconf.get('AMAZON_PAYMENTS:SELLER_ID') || nconf.get('AMAZON_PAYMENTS_SELLER_ID'); const AMAZON_SELLER_ID = nconf.get('AMAZON_PAYMENTS_SELLER_ID');
const AMAZON_CLIENT_ID = nconf.get('AMAZON_PAYMENTS:CLIENT_ID') || nconf.get('AMAZON_PAYMENTS_CLIENT_ID'); const AMAZON_CLIENT_ID = nconf.get('AMAZON_PAYMENTS_CLIENT_ID');
const AMAZON_MODE = nconf.get('AMAZON_PAYMENTS:MODE') || nconf.get('AMAZON_PAYMENTS_MODE'); const AMAZON_MODE = nconf.get('AMAZON_PAYMENTS_MODE');
let env = { let env = {
NODE_ENV: '"production"', NODE_ENV: '"production"',
@@ -30,13 +30,13 @@ let env = {
MODE: `"${AMAZON_MODE}"`, MODE: `"${AMAZON_MODE}"`,
}, },
EMAILS: { EMAILS: {
COMMUNITY_MANAGER_EMAIL: `"${nconf.get('EMAILS:COMMUNITY_MANAGER_EMAIL')}"`, COMMUNITY_MANAGER_EMAIL: `"${nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL')}"`,
TECH_ASSISTANCE_EMAIL: `"${nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL')}"`, TECH_ASSISTANCE_EMAIL: `"${nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL')}"`,
PRESS_ENQUIRY_EMAIL: `"${nconf.get('EMAILS:PRESS_ENQUIRY_EMAIL')}"`, PRESS_ENQUIRY_EMAIL: `"${nconf.get('EMAILS_PRESS_ENQUIRY_EMAIL')}"`,
}, },
}; };
'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY GOOGLE_CLIENT_ID AMPLITUDE_KEY PUSHER:KEY PUSHER:ENABLED LOGGLY_CLIENT_TOKEN' 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY GOOGLE_CLIENT_ID AMPLITUDE_KEY LOGGLY_CLIENT_TOKEN'
.split(' ') .split(' ')
.forEach(key => { .forEach(key => {
env[key] = `"${nconf.get(key)}"`; env[key] = `"${nconf.get(key)}"`;

View File

@@ -11,22 +11,23 @@ div
#app(:class='{"casting-spell": castingSpell}') #app(:class='{"casting-spell": castingSpell}')
banned-account-modal banned-account-modal
amazon-payments-modal(v-if='!isStaticPage') amazon-payments-modal(v-if='!isStaticPage')
payments-success-modal
snackbars snackbars
router-view(v-if="!isUserLoggedIn || isStaticPage") router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else) template(v-else)
template(v-if="isUserLoaded") template(v-if="isUserLoaded")
div.resting-banner(v-show="showRestingBanner", ref="restingBanner") .resting-banner(v-show="showRestingBanner", ref="restingBanner")
span.content span.content
span.label.d-inline.d-sm-none {{ $t('innCheckOutBannerShort') }} span.label.d-inline.d-sm-none {{ $t('innCheckOutBannerShort') }}
span.label.d-none.d-sm-inline {{ $t('innCheckOutBanner') }} span.label.d-none.d-sm-inline {{ $t('innCheckOutBanner') }}
span.separator | span.separator |
span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }} span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }}
div.closepadding(@click="hideBanner()") .closepadding(@click="hideBanner()")
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close") span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close")
notifications-display notifications-display
app-menu(:class='{"restingInn": showRestingBanner}' :style="{ marginTop: bannerHeight + 'px' }") app-menu
.container-fluid .container-fluid
app-header(:class='{"restingInn": showRestingBanner}') app-header
buyModal( buyModal(
:item="selectedItemToBuy || {}", :item="selectedItemToBuy || {}",
:withPin="true", :withPin="true",
@@ -49,6 +50,13 @@ div
<style lang='scss' scoped> <style lang='scss' scoped>
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
#app {
height: calc(100% - 56px); /* 56px is the menu */
display: flex;
flex-direction: column;
min-height: 100vh;
}
#loading-screen-inapp { #loading-screen-inapp {
#melior { #melior {
margin: 0 auto; margin: 0 auto;
@@ -78,6 +86,11 @@ div
cursor: crosshair; cursor: crosshair;
} }
.container-fluid {
overflow-x: hidden;
flex: 1 0 auto;
}
.notification { .notification {
border-radius: 1000px; border-radius: 1000px;
background-color: $green-10; background-color: $green-10;
@@ -88,42 +101,10 @@ div
margin-bottom: .5em; margin-bottom: .5em;
} }
.container-fluid {
overflow-x: hidden;
flex: 1 0 auto;
}
#app {
height: calc(100% - 56px); /* 56px is the menu */
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal {
overflow-y: scroll !important;
}
.modal-backdrop.show {
opacity: .9 !important;
background-color: $purple-100 !important;
}
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1600 !important; /* Must stay above nav bar */
}
.resting-banner { .resting-banner {
width: 100%; width: 100%;
min-height: 40px; min-height: 40px;
background-color: $blue-10; background-color: $blue-10;
position: fixed;
top: 0; top: 0;
z-index: 1300; z-index: 1300;
display: flex; display: flex;
@@ -139,14 +120,10 @@ div
.closepadding { .closepadding {
margin: 11px 24px; margin: 11px 24px;
display: inline-block; display: inline-block;
position: absolute; position: relative;
right: 0; right: 0;
top: 0; top: 0;
cursor: pointer; cursor: pointer;
span svg path {
stroke: $blue-500;
}
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
@@ -169,6 +146,30 @@ div
} }
</style> </style>
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
.closepadding span svg path {
stroke: #FFF;
opacity: 0.48;
}
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal {
overflow-y: scroll !important;
}
.modal-backdrop.show {
opacity: .9 !important;
background-color: $purple-100 !important;
}
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1600 !important; /* Must stay above nav bar */
}
</style>
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar'; import { loadProgressBar } from 'axios-progress-bar';
@@ -185,7 +186,10 @@ import SelectMembersModal from 'client/components/selectMembersModal.vue';
import notifications from 'client/mixins/notifications'; import notifications from 'client/mixins/notifications';
import { setup as setupPayments } from 'client/libs/payments'; import { setup as setupPayments } from 'client/libs/payments';
import amazonPaymentsModal from 'client/components/payments/amazonModal'; import amazonPaymentsModal from 'client/components/payments/amazonModal';
import paymentsSuccessModal from 'client/components/payments/successModal';
import spellsMixin from 'client/mixins/spells'; import spellsMixin from 'client/mixins/spells';
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager';
import svgClose from 'assets/svg/close.svg'; import svgClose from 'assets/svg/close.svg';
import bannedAccountModal from 'client/components/bannedAccountModal'; import bannedAccountModal from 'client/components/bannedAccountModal';
@@ -205,6 +209,7 @@ export default {
SelectMembersModal, SelectMembersModal,
amazonPaymentsModal, amazonPaymentsModal,
bannedAccountModal, bannedAccountModal,
paymentsSuccessModal,
}, },
data () { data () {
return { return {
@@ -220,7 +225,6 @@ export default {
loading: true, loading: true,
currentTipNumber: 0, currentTipNumber: 0,
bannerHidden: false, bannerHidden: false,
bannerHeight: 0,
}; };
}, },
computed: { computed: {
@@ -313,6 +317,7 @@ export default {
const errorMessage = errorData.message || errorData; const errorMessage = errorData.message || errorData;
// Check for conditions to reset the user auth // Check for conditions to reset the user auth
// TODO use a specific error like NotificationNotFound instead of checking for the string
const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.']; const invalidUserMessage = [this.$t('invalidCredentials'), 'Missing authentication headers.'];
if (invalidUserMessage.indexOf(errorMessage) !== -1) { if (invalidUserMessage.indexOf(errorMessage) !== -1) {
this.$store.dispatch('auth:logout'); this.$store.dispatch('auth:logout');
@@ -322,12 +327,6 @@ export default {
let snackbarTimeout = false; let snackbarTimeout = false;
if (error.response.status === 502) snackbarTimeout = true; if (error.response.status === 502) snackbarTimeout = true;
const notificationNotFoundMessage = [
this.$t('messageNotificationNotFound'),
this.$t('messageNotificationNotFound', 'en'),
];
if (notificationNotFoundMessage.indexOf(errorMessage) !== -1) snackbarTimeout = true;
let errorsToShow = []; let errorsToShow = [];
// show only the first error for each param // show only the first error for each param
let paramErrorsFound = {}; let paramErrorsFound = {};
@@ -341,13 +340,17 @@ export default {
} else { } else {
errorsToShow.push(errorMessage); errorsToShow.push(errorMessage);
} }
// dispatch as one snackbar notification
this.$store.dispatch('snackbars:add', { // Ignore NotificationNotFound errors, see https://github.com/HabitRPG/habitica/issues/10391
title: 'Habitica', if (errorData.error !== 'NotificationNotFound') {
text: errorsToShow.join(' '), // dispatch as one snackbar notification
type: 'error', this.$store.dispatch('snackbars:add', {
timeout: snackbarTimeout, title: 'Habitica',
}); text: errorsToShow.join(' '),
type: 'error',
timeout: snackbarTimeout,
});
}
} }
return Promise.reject(error); return Promise.reject(error);
@@ -418,14 +421,6 @@ export default {
this.hideLoadingScreen(); this.hideLoadingScreen();
window.addEventListener('resize', this.setBannerOffset);
// Adjust the positioning of the header banners
this.$watch('showRestingBanner', () => {
this.$nextTick(() => {
this.setBannerOffset();
});
}, {immediate: true});
// Adjust the timezone offset // Adjust the timezone offset
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) { if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
this.$store.dispatch('user:set', { this.$store.dispatch('user:set', {
@@ -433,6 +428,14 @@ export default {
}); });
} }
let appState = getLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
if (appState) {
appState = JSON.parse(appState);
if (appState.paymentCompleted) {
removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE);
this.$root.$emit('habitica:payment-success', appState);
}
}
this.$nextTick(() => { this.$nextTick(() => {
// Load external scripts after the app has been rendered // Load external scripts after the app has been rendered
setupPayments(); setupPayments();
@@ -452,7 +455,6 @@ export default {
this.$root.$off('bv::show::modal'); this.$root.$off('bv::show::modal');
this.$root.$off('buyModal::showItem'); this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem'); this.$root.$off('selectMembersModal::showItem');
window.removeEventListener('resize', this.setBannerOffset);
}, },
mounted () { mounted () {
// Remove the index.html loading screen and now show the inapp loading // Remove the index.html loading screen and now show the inapp loading
@@ -611,22 +613,10 @@ export default {
}, },
hideBanner () { hideBanner () {
this.bannerHidden = true; this.bannerHidden = true;
this.setBannerOffset();
}, },
resumeDamage () { resumeDamage () {
this.$store.dispatch('user:sleep'); this.$store.dispatch('user:sleep');
}, },
setBannerOffset () {
let contentPlacement = 0;
if (this.showRestingBanner && this.$refs.restingBanner !== undefined) {
contentPlacement = this.$refs.restingBanner.clientHeight;
}
this.bannerHeight = contentPlacement;
let smartBanner = document.getElementsByClassName('smartbanner')[0];
if (smartBanner !== undefined) {
smartBanner.style.top = `${contentPlacement}px`;
}
},
}, },
}; };
</script> </script>

View File

@@ -1,90 +1,78 @@
.achievement-costumeContest6x { .promo_armoire_backgrounds_201901 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -148px; background-position: -445px 0px;
width: 144px;
height: 156px;
}
.promo_alligator {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 480px;
height: 360px;
}
.promo_animal_tails {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -994px 0px;
width: 141px;
height: 441px;
}
.promo_armoire_backgrounds_201811 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -481px -420px;
width: 423px; width: 423px;
height: 147px; height: 147px;
} }
.promo_frost_potions { .promo_bird_buddies_bundle {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -421px -723px; background-position: -421px -337px;
width: 417px; width: 420px;
height: 147px; height: 147px;
} }
.promo_ios { .promo_g1g1 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -361px; background-position: -241px -633px;
width: 375px; width: 237px;
height: 361px; height: 150px;
} }
.promo_mystery_201811 { .promo_npc_alex {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -839px -723px; background-position: -566px -485px;
width: 282px; width: 162px;
height: 147px; height: 138px;
} }
.promo_oddballs_bundle { .promo_seasonal_shop {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -481px -568px; background-position: -403px -485px;
width: 162px;
height: 138px;
}
.promo_snow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -445px -148px;
width: 423px; width: 423px;
height: 147px; height: 147px;
} }
.promo_piyo {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px 0px;
width: 279px;
height: 147px;
}
.promo_take_this { .promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1281px -148px; background-position: -729px -485px;
width: 96px; width: 96px;
height: 69px; height: 69px;
} }
.promo_turkey_day_2018 { .promo_winter_wonderland_2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -723px; background-position: 0px -485px;
width: 402px;
height: 147px;
}
.promo_wintery_skins {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -337px;
width: 420px; width: 420px;
height: 147px; height: 147px;
} }
.promo_veteran_pets { .customize-option.promo_wintery_skins {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -871px; background-position: -25px -352px;
width: 363px; width: 60px;
height: 141px; height: 60px;
} }
.scene_nametag { .scene_hat_guild {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -481px 0px; background-position: 0px 0px;
width: 512px; width: 444px;
height: 208px; height: 336px;
} }
.scene_sleep { .scene_starting_over {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -481px -209px; background-position: -479px -633px;
width: 390px; width: 150px;
height: 210px; height: 150px;
} }
.scene_veteran_pets { .scene_todo_list {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -305px; background-position: 0px -633px;
width: 242px; width: 240px;
height: 62px; height: 195px;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,402 +1,402 @@
.quest_TEMPLATE_FOR_MISSING_IMAGE { .npc_matt {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -251px -1519px; background-position: -597px -1535px;
width: 221px; width: 195px;
height: 39px; height: 138px;
}
.background_dysheartener {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px 0px;
width: 306px;
height: 202px;
}
.banner_flair_dysheartener {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1963px -836px;
width: 69px;
height: 18px;
}
.phobia_dysheartener {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1187px -880px;
width: 201px;
height: 195px;
}
.quest_alligator {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1627px -1079px;
width: 201px;
height: 213px;
}
.quest_armadillo {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -435px;
width: 219px;
height: 219px;
}
.quest_atom1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1105px -1315px;
width: 250px;
height: 150px;
}
.quest_atom2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -389px -1535px;
width: 207px;
height: 138px;
}
.quest_atom3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -747px -440px;
width: 216px;
height: 180px;
}
.quest_axolotl {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -747px -220px;
width: 219px;
height: 219px;
}
.quest_badger {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -655px;
width: 219px;
height: 219px;
}
.quest_basilist {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1844px -392px;
width: 189px;
height: 141px;
}
.quest_beetle {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1627px -1293px;
width: 204px;
height: 201px;
}
.quest_bunny {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -967px -660px;
width: 210px;
height: 186px;
}
.quest_butterfly {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -967px 0px;
width: 219px;
height: 219px;
}
.quest_cheetah {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -967px -220px;
width: 219px;
height: 219px;
} }
.quest_cow { .quest_cow {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -175px; background-position: 0px -1535px;
width: 174px; width: 174px;
height: 213px; height: 213px;
} }
.quest_dilatory { .quest_dilatory {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -440px 0px; background-position: -220px -875px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_dilatoryDistress1 { .quest_dilatoryDistress1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1540px -1085px; background-position: -1627px -868px;
width: 210px; width: 210px;
height: 210px; height: 210px;
} }
.quest_dilatoryDistress2 { .quest_dilatoryDistress2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -959px; background-position: -1844px -534px;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.quest_dilatoryDistress3 { .quest_dilatoryDistress3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -440px -232px; background-position: -880px -875px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_dilatory_derby { .quest_dilatory_derby {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1320px -660px; background-position: 0px -875px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_dustbunnies { .quest_dustbunnies {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px 0px; background-position: -1187px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_egg { .quest_egg {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -751px; background-position: -1844px -184px;
width: 165px; width: 165px;
height: 207px; height: 207px;
} }
.quest_evilsanta { .quest_evilsanta {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1562px -1332px; background-position: -1844px -836px;
width: 118px; width: 118px;
height: 131px; height: 131px;
} }
.quest_evilsanta2 { .quest_evilsanta2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px -452px; background-position: -1187px -660px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_falcon { .quest_falcon {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -440px -452px; background-position: 0px -1095px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_ferret { .quest_ferret {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px -452px; background-position: -307px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_frog { .quest_frog {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -880px -1112px; background-position: -660px -1315px;
width: 221px; width: 221px;
height: 213px; height: 213px;
} }
.quest_ghost_stag { .quest_ghost_stag {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -880px -220px; background-position: -660px -1095px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_goldenknight1 { .quest_goldenknight1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -880px -440px; background-position: -880px -1095px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_goldenknight2 { .quest_goldenknight2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1311px -1332px; background-position: -1356px -1315px;
width: 250px; width: 250px;
height: 150px; height: 150px;
} }
.quest_goldenknight3 { .quest_goldenknight3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px 0px; background-position: 0px -203px;
width: 219px; width: 219px;
height: 231px; height: 231px;
} }
.quest_gryphon { .quest_gryphon {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1322px -1112px; background-position: -307px -220px;
width: 216px; width: 216px;
height: 177px; height: 177px;
} }
.quest_guineapig { .quest_guineapig {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px -672px; background-position: -1407px -440px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_harpy { .quest_harpy {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -880px -672px; background-position: -1407px -660px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_hedgehog { .quest_hedgehog {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -1332px; background-position: -1407px -1100px;
width: 219px; width: 219px;
height: 186px; height: 186px;
} }
.quest_hippo { .quest_hippo {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1100px -220px; background-position: 0px -1315px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_horse { .quest_horse {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1100px -440px; background-position: -220px -1315px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_kangaroo { .quest_kangaroo {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1100px -660px; background-position: -1407px -880px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_kraken { .quest_kraken {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -877px -1332px; background-position: -527px -220px;
width: 216px; width: 216px;
height: 177px; height: 177px;
} }
.quest_lostMasterclasser1 { .quest_lostMasterclasser1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px -892px; background-position: -1100px -1095px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_lostMasterclasser2 { .quest_lostMasterclasser2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -440px -892px; background-position: -440px -1095px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_lostMasterclasser3 { .quest_lostMasterclasser3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px -892px; background-position: -1187px -220px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_mayhemMistiflying1 { .quest_mayhemMistiflying1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -1261px; background-position: -1844px -685px;
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
.quest_mayhemMistiflying2 { .quest_mayhemMistiflying2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1100px -892px; background-position: -440px -875px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_mayhemMistiflying3 { .quest_mayhemMistiflying3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1320px 0px; background-position: -660px -655px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_monkey { .quest_monkey {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1320px -220px; background-position: -440px -655px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moon1 { .quest_moon1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1540px -217px; background-position: -1627px -651px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_moon2 { .quest_moon2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px 0px; background-position: -220px -655px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moon3 { .quest_moon3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1320px -880px; background-position: -747px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moonstone1 { .quest_moonstone1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -1112px; background-position: -527px 0px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moonstone2 { .quest_moonstone2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px -1112px; background-position: -1407px -220px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_moonstone3 { .quest_moonstone3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -440px -1112px; background-position: -440px -1315px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_nudibranch { .quest_nudibranch {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1540px -868px; background-position: -1627px -217px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_octopus { .quest_octopus {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px -1332px; background-position: -882px -1315px;
width: 222px; width: 222px;
height: 177px; height: 177px;
} }
.quest_owl { .quest_owl {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px -1112px; background-position: -1187px -440px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_peacock { .quest_peacock {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1540px 0px; background-position: -1627px 0px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_penguin { .quest_penguin {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -567px; background-position: -1844px 0px;
width: 190px; width: 190px;
height: 183px; height: 183px;
} }
.quest_pterodactyl { .quest_pterodactyl {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1320px -440px; background-position: -660px -875px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_rat { .quest_rat {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -880px -892px; background-position: -967px -440px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_rock { .quest_rock {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1540px -434px; background-position: -1627px -434px;
width: 216px; width: 216px;
height: 216px; height: 216px;
} }
.quest_rooster { .quest_rooster {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px 0px; background-position: -175px -1535px;
width: 213px; width: 213px;
height: 174px; height: 174px;
} }
.quest_sabretooth { .quest_sabretooth {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -892px; background-position: -440px -435px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_seaserpent { .quest_seaserpent {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1100px 0px; background-position: -220px -435px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_sheep { .quest_sheep {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -440px -672px; background-position: -220px -1095px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }
.quest_slime { .quest_slime {
background-image: url('~assets/images/sprites/spritesmith-main-11.png'); background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -672px; background-position: -1407px 0px;
width: 219px;
height: 219px;
}
.quest_sloth {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -880px 0px;
width: 219px;
height: 219px;
}
.quest_snail {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1102px -1112px;
width: 219px;
height: 213px;
}
.quest_snake {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px -1332px;
width: 216px;
height: 177px;
}
.quest_spider {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -1519px;
width: 250px;
height: 150px;
}
.quest_squirrel {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -452px;
width: 219px;
height: 219px;
}
.quest_stoikalmCalamity1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -1412px;
width: 150px;
height: 150px;
}
.quest_stoikalmCalamity2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -660px -220px;
width: 219px;
height: 219px;
}
.quest_stoikalmCalamity3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px -232px;
width: 219px;
height: 219px;
}
.quest_taskwoodsTerror1 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -1110px;
width: 150px;
height: 150px;
}
.quest_taskwoodsTerror2 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1540px -651px;
width: 216px;
height: 216px;
}
.quest_taskwoodsTerror3 {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: 0px -232px;
width: 219px;
height: 219px;
}
.quest_treeling {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1094px -1332px;
width: 216px;
height: 177px;
}
.quest_trex {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -1757px -389px;
width: 204px;
height: 177px;
}
.quest_trex_undead {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -443px -1332px;
width: 216px;
height: 177px;
}
.quest_triceratops {
background-image: url('~assets/images/sprites/spritesmith-main-11.png');
background-position: -220px -672px;
width: 219px; width: 219px;
height: 219px; height: 219px;
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 KiB

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 500 KiB

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 KiB

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 66 KiB

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