Merge branch 'release' into develop

This commit is contained in:
Sabe Jones
2019-04-02 13:09:46 -05:00
90 changed files with 23412 additions and 23307 deletions

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-console */
import { sendTxn } from '../../../website/server/libs/email';
import { sendTxn } from '../../website/server/libs/email';
import { model as User } from '../../website/server/models/user';
import moment from 'moment';
import nconf from 'nconf';

View File

@@ -1,67 +1,24 @@
/* eslint-disable no-console */
const MIGRATION_NAME = 'full-stable';
import each from 'lodash/each';
import keys from 'lodash/keys';
import content from '../../website/common/script/content/index';
const migrationName = 'full-stable.js';
const authorName = 'Sabe'; // in case script author needs to know when their ...
const authorUuid = '7f14ed62-5408-4e1b-be83-ada62d504931'; // ... own data is done
import { model as User } from '../../website/server/models/user';
const progressCount = 1000;
let count = 0;
/*
* Award users every extant pet and mount
*/
const connectionString = 'mongodb://localhost:27017/habitrpg?auto_reconnect=true'; // FOR TEST DATABASE
let monk = require('monk');
let dbUsers = monk(connectionString).get('users', { castIds: false });
function processUsers (lastId) {
// specify a query to limit the affected users (empty for all users):
let query = {
'profile.name': 'SabreCat',
};
if (lastId) {
query._id = {
$gt: lastId,
};
}
dbUsers.find(query, {
sort: {_id: 1},
limit: 250,
fields: [
], // specify fields we are interested in to limit retrieved data (empty if we're not reading data):
})
.then(updateUsers)
.catch((err) => {
console.log(err);
return exiting(1, `ERROR! ${ err}`);
});
}
let progressCount = 1000;
let count = 0;
function updateUsers (users) {
if (!users || users.length === 0) {
console.warn('All appropriate users found and modified.');
displayData();
return;
}
let userPromises = users.map(updateUser);
let lastUser = users[users.length - 1];
return Promise.all(userPromises)
.then(() => {
processUsers(lastUser._id);
});
}
function updateUser (user) {
async function updateUser (user) {
count++;
let set = {
migration: migrationName,
};
const set = {};
set.migration = MIGRATION_NAME;
each(keys(content.pets), (pet) => {
set[`items.pets.${pet}`] = 5;
@@ -88,30 +45,40 @@ function updateUser (user) {
set[`items.mounts.${mount}`] = true;
});
dbUsers.update({_id: user._id}, {$set: set});
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (user._id === authorUuid) console.warn(`${authorName } processed`);
return await User.update({_id: user._id}, {$set: set}).exec();
}
function displayData () {
module.exports = async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.local.username': 'olson22',
};
const fields = {
_id: 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`);
return exiting(0);
}
function exiting (code, msg) {
code = code || 0; // 0 = success
if (code && !msg) {
msg = 'ERROR!';
}
if (msg) {
if (code) {
console.error(msg);
break;
} else {
console.log(msg);
}
}
process.exit(code);
query._id = {
$gt: users[users.length - 1],
};
}
module.exports = processUsers;
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "habitica",
"version": "4.90.4",
"version": "4.91.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.90.4",
"version": "4.91.0",
"main": "./website/server/index.js",
"dependencies": {
"@google-cloud/trace-agent": "^3.6.0",

View File

@@ -53,7 +53,7 @@ async function _deleteHabiticaData (user, email) {
if (response) {
console.log(`${response.status} ${response.statusText}`);
if (response.status === 200) console.log(`${user._id} removed. Last login: ${user.auth.timestamps.loggedin}`);
if (response.status === 200) console.log(`${user._id} (${email}) removed. Last login: ${user.auth.timestamps.loggedin}`);
}
}

View File

@@ -335,14 +335,13 @@ describe('analyticsService', () => {
let data, itemSpy;
beforeEach(() => {
Visitor.prototype.event.yields();
itemSpy = sandbox.stub().returnsThis();
Visitor.prototype.event.returns({
send: sandbox.stub(),
});
Visitor.prototype.transaction.returns({
item: itemSpy,
send: sandbox.stub().returnsThis(),
send: sandbox.stub().yields(),
});
data = {

View File

@@ -1,72 +1,60 @@
.promo_april_fools_2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -719px -767px;
background-position: -445px -163px;
width: 423px;
height: 147px;
}
.promo_armoire_backgrounds_201903 {
.promo_armoire_backgrounds_201904 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -424px -933px;
background-position: 0px -337px;
width: 423px;
height: 147px;
}
.promo_beffymaroo_wondercon {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 718px;
height: 932px;
}
.promo_celestial_rainbow_potions {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -933px;
background-position: -424px -337px;
width: 423px;
height: 147px;
}
.promo_classes_spring2019 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -719px -604px;
background-position: -445px 0px;
width: 432px;
height: 162px;
}
.promo_egg_hunt {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1160px 0px;
background-position: 0px -485px;
width: 354px;
height: 147px;
}
.promo_mystery_201903 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1160px -148px;
background-position: -355px -485px;
width: 351px;
height: 147px;
}
.promo_seasonalshop_spring {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1160px -492px;
background-position: -707px -485px;
width: 162px;
height: 138px;
}
.promo_shiny_seeds {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -633px;
width: 351px;
height: 147px;
}
.promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1401px -296px;
background-position: -352px -633px;
width: 96px;
height: 69px;
}
.scene_dailies {
.scene_hat_guild {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -719px -327px;
width: 327px;
height: 276px;
}
.scene_tavern {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -719px 0px;
width: 440px;
height: 326px;
}
.scene_todos {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1160px -296px;
width: 240px;
height: 195px;
background-position: 0px 0px;
width: 444px;
height: 336px;
}

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

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.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 KiB

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 KiB

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 KiB

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -481,5 +481,13 @@
"backgroundFieldWithColoredEggsText": "Field with Colored Eggs",
"backgroundFieldWithColoredEggsNotes": "Hunt for springtime treasure in a Field with Colored Eggs.",
"backgroundFlowerMarketText": "Flower Market",
"backgroundFlowerMarketNotes": "Find the perfect colors for bouquet or garden in a Flower Market."
"backgroundFlowerMarketNotes": "Find the perfect colors for bouquet or garden in a Flower Market.",
"backgrounds042019": "SET 59: Released April 2019",
"backgroundBirchForestText": "Birch Forest",
"backgroundBirchForestNotes": "Dally in a peaceful Birch Forest.",
"backgroundHalflingsHouseText": "Halfling's House",
"backgroundHalflingsHouseNotes": "Visit a charming Halfling's House.",
"backgroundBlossomingDesertText": "Blossoming Desert",
"backgroundBlossomingDesertNotes": "Witness a rare superbloom in the Blossoming Desert."
}

View File

@@ -418,6 +418,10 @@
"weaponArmoireChefsSpoonNotes": "Raise it as you release your battle cry: “SPOOOON!!” Increases Intelligence by <%= int %>. Enchanted Armoire: Chef Set (Item 3 of 4).",
"weaponArmoireVernalTaperText": "Vernal Taper",
"weaponArmoireVernalTaperNotes": "The days are getting longer, but this candle will help you find your way before sunrise. Increases Constitution by <%= con %>. Enchanted Armoire: Vernal Vestments Set (Item 3 of 3).",
"weaponArmoireJugglingBallsText": "Juggling Balls",
"weaponArmoireJugglingBallsNotes": "Habiticans are master multi-taskers, so you should have no trouble keeping all these balls in the air! Increases Intelligence by <%= int %>. Enchanted Armoire: Independent Item.",
"weaponArmoireSlingshotText": "Slingshot",
"weaponArmoireSlingshotNotes": "Take aim at your red Dailies! Increases Strength by <%= str %>. Enchanted Armoire: Independent Item.",
"armor": "armor",
"armorCapitalized": "Armor",
@@ -1406,6 +1410,8 @@
"headArmoireToqueBlancheNotes": "According to legend, the number of folds in this hat indicate the number of ways you know how to cook an egg! Is it accurate? Increases Perception by <%= per %>. Enchanted Armoire: Chef Set (Item 1 of 4).",
"headArmoireVernalHenninText": "Vernal Hennin",
"headArmoireVernalHenninNotes": "More than just a pretty hat, this conical chapeau can also hold a rolled-up To-Do list inside. Increases Perception by <%= per %>. Enchanted Armoire: Vernal Vestments Set (Item 1 of 3).",
"headArmoireTricornHatText": "Tricorn Hat",
"headArmoireTricornHatNotes": "Become a revolutionary jokester! Increases Perception by <%= per %>. Enchanted Armoire: Independent Item.",
"offhand": "off-hand item",
"offhandCapitalized": "Off-Hand Item",

View File

@@ -815,6 +815,20 @@ let backgrounds = {
notes: t('backgroundFlowerMarketNotes'),
},
},
backgrounds042019: {
halflings_house: {
text: t('backgroundHalflingsHouseText'),
notes: t('backgroundHalflingsHouseNotes'),
},
blossoming_desert: {
text: t('backgroundBlossomingDesertText'),
notes: t('backgroundBlossomingDesertNotes'),
},
birch_forest: {
text: t('backgroundBirchForestText'),
notes: t('backgroundBirchForestNotes'),
},
},
incentiveBackgrounds: {
violet: {
text: t('backgroundVioletText'),

View File

@@ -924,6 +924,13 @@ let head = {
set: 'vernalVestments',
canOwn: ownsItem('head_armoire_vernalHennin'),
},
tricornHat: {
text: t('headArmoireTricornHatText'),
notes: t('headArmoireTricornHatNotes', { per: 10 }),
value: 100,
per: 10,
canOwn: ownsItem('head_armoire_tricornHat'),
},
};
let shield = {
@@ -1553,6 +1560,20 @@ let weapon = {
set: 'vernalVestments',
canOwn: ownsItem('weapon_armoire_vernalTaper'),
},
jugglingBalls: {
text: t('weaponArmoireJugglingBallsText'),
notes: t('weaponArmoireJugglingBallsNotes', { int: 10 }),
value: 100,
int: 10,
canOwn: ownsItem('weapon_armoire_jugglingBalls'),
},
slingshot: {
text: t('weaponArmoireSlingshotText'),
notes: t('weaponArmoireSlingshotNotes', { str: 10 }),
value: 100,
str: 10,
canOwn: ownsItem('weapon_armoire_slingshot'),
},
};
let armoireSet = {

View File

@@ -8,12 +8,12 @@ const featuredItems = {
path: 'armoire',
},
{
type: 'hatchingPotions',
path: 'hatchingPotions.Golden',
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Celestial',
},
{
type: 'eggs',
path: 'eggs.PandaCub',
type: 'premiumHatchingPotion',
path: 'premiumHatchingPotions.Rainbow',
},
{
type: 'card',

View File

@@ -19,6 +19,7 @@ module.exports = {
},
availableSpells: [
'shinySeed',
],
availableQuests: [

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 711 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
let api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'THE APRIL FOOL STRIKES AGAIN!';
const LAST_ANNOUNCEMENT_TITLE = 'NEW BACKGROUNDS AND ARMOIRE ITEMS, MONTHLY CHALLENGES, AND SHINY SEEDS';
const worldDmg = { // @TODO
bailey: false,
};
@@ -30,21 +30,30 @@ api.getNews = {
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>4/1/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<h2>4/2/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div>
</div>
<hr/>
<div class="promo_april_fools_2019 center-block"></div>
<h3>Fruit and Veggie Pets and NPCs</h3>
<p>The April Fool has appeared, and he's got a farmer's market's worth of produce in tow.</p>
<p>"HAHA!" he cries, as a dragonfruit bounces along beside him. "I've always thought good humor should be healthful and nourishing, so I've gone back to my roots, if you will, to bring some plant-powered goodness into Habitica once again!"</p>
<p>"He's replaced all our equipped pets with fruits and vegetables!" says QuartzFox, gently patting a contented-looking tomato. "Although to be fair, they are very cute fruits and vegetables!"</p>
<p>Equipping different pets will show different fruits and veggies. Have fun discovering them all!</p>
<p>The NPCs have also been turned into their fruit and vegetable forms as a tribute to Habitica's <a href="https://habitica.fandom.com/wiki/April_Fools'_Day_2014" target='_blank'>very first April Fool's prank back in 2014</a>! Go check them out.</p>
<h3>Special April Fool's Social Media Challenge!</h3>
<p>For even more fun, check out the <a href='/challenges/b0337534-ec69-4269-8cc6-f74c91881451'>official Challenge</a> posted especially for today! Share your avatar featuring your new fruit and veggie pet on social media between now and April 3, and you'll have a chance to win gems and have your avatar featured on the Habitica Blog!</p>
<div class="small mb-3">by Beffymaroo, SabreCat, Piyo, Viirus, and Lemoness</div>
<div class="npc_aprilFool center-block"></div>
<div class="promo_armoire_backgrounds_201904 center-block"></div>
<h3>April Backgrounds and Armoire Items</h3>
<p>Weve added three new backgrounds to the Background Shop! Now your avatar can visit a Halfling's House, dally through a peaceful Birch Forest, and take in the Superbloom in the Blossoming Desert. Check them out under User Icon > Backgrounds!</p>
<p>Plus, theres new Gold-purchasable equipment in the Enchanted Armoire, including some fun joke props in honor of April Fool's Day! Better work hard on your real-life tasks to earn all the pieces! Enjoy :)</p>
<div class="small mb-3">by Vikte, QuartzFox, Katy133, GeraldThePixel, and Gully</div>
<div class="scene_hat_guild center-block"></div>
<h3>April 2019 Resolution Success Challenge and New Take This Challenge</h3>
<p>The Habitica team has launched a special official Challenge series hosted in the <a href='/groups/guild/6e6a8bd3-9f5f-4351-9188-9f11fcd80a99' target='_blank'>Official New Year's Resolution Guild</a>. These Challenges are designed to help you build and maintain goals that are destined for success and then stick with them as the year progresses. For this month's Challenge, <a href='/challenges/ae4a6ab8-e4c7-46fb-ba48-a5f05610a55d'>Gather Your Party</a>, we're focusing on finding encouraging allies to help you gain accountability for your goals! It has a 15 Gem prize, which will be awarded to five lucky winners on May 1st.</p>
<p>Congratulations to the winners of March's Challenge, DcryptMart, LONEW0LF, Elcaracol, DungeonMasterful, and 7NationTpr!</p>
<div class="promo_take_this center-block"></div>
<p>The next Take This Challenge has also launched, "<a href='/challenges/5712376e-89f1-4f8b-89eb-8f94026d0da9'>Harder, Faster, Stronger!</a>", with a focus on setting and meeting physical activity goals. Be sure to check it out to earn additional pieces of the Take This armor set!</p>
<p><a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.</p>
<p>Congratulations to the winners of the last Take This Challenge, "Do One Thing Well!": grand prize winner Денис Кадников, and runners-up addone, alihenri, Hemogoblin3991, Kalu_Ienvru, and gabriellamara! Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a> if they hadn't completed it already. It is located in your Rewards column. Enjoy!</p>
<div class="small mb-3">by Doctor B, the Take This team, Lemoness, Beffymaroo, and SabreCat</div>
<div class="promo_shiny_seeds center-block"></div>
<h3>Shiny Seeds</h3>
<p>Throw a Shiny Seed at your friends and they will turn into a cheerful flower until their next cron! You can buy the Seeds in the <a href='/shops/seasonal'>Seasonal Shop</a> for Gold. Plus, if you get transformed by a Shiny Seed, you'll receive the Agricultural Friends badge!</p>
<p>Don't want to be a flower? Just buy some Petal-Free Potion from your Rewards column to reverse it.</p>
<p>Shiny Seeds will be available in the <a href='/shops/seasonal'>Seasonal Shop</a> until April 30th!</p>
<div class="small mb-3">by Lemoness</div>
</div>
`,
});

View File

@@ -9,6 +9,7 @@ import {
toArray,
} from 'lodash';
import { content as Content } from '../../common';
import logger from './logger';
const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
const GA_TOKEN = nconf.get('GA_ID');
@@ -27,7 +28,7 @@ if (AMPLITUDE_TOKEN) amplitude = new Amplitude(AMPLITUDE_TOKEN);
let ga = googleAnalytics(GA_TOKEN);
let _lookUpItemName = (itemKey) => {
function _lookUpItemName (itemKey) {
if (!itemKey) return;
let gear = Content.gear.flat[itemKey];
@@ -54,9 +55,9 @@ let _lookUpItemName = (itemKey) => {
}
return itemName;
};
}
let _formatUserData = (user) => {
function _formatUserData (user) {
let properties = {};
if (user.stats) {
@@ -102,9 +103,9 @@ let _formatUserData = (user) => {
}
return properties;
};
}
let _formatPlatformForAmplitude = (platform) => {
function _formatPlatformForAmplitude (platform) {
if (!platform) {
return 'Unknown';
}
@@ -114,9 +115,9 @@ let _formatPlatformForAmplitude = (platform) => {
}
return '3rd Party';
};
}
let _formatUserAgentForAmplitude = (platform, agentString) => {
function _formatUserAgentForAmplitude (platform, agentString) {
if (!agentString) {
return 'Unknown';
}
@@ -135,14 +136,18 @@ let _formatUserAgentForAmplitude = (platform, agentString) => {
}
return formattedAgent;
};
}
let _formatDataForAmplitude = (data) => {
function _formatUUIDForAmplitude (uuid) {
return uuid || 'no-user-id-was-provided';
}
function _formatDataForAmplitude (data) {
let event_properties = omit(data, AMPLITUDE_PROPERTIES_TO_SCRUB);
let platform = _formatPlatformForAmplitude(data.headers && data.headers['x-client']);
let agent = _formatUserAgentForAmplitude(platform, data.headers && data.headers['user-agent']);
let ampData = {
user_id: data.uuid || 'no-user-id-was-provided',
user_id: _formatUUIDForAmplitude(data.uuid),
platform,
os_name: agent.name,
os_version: agent.version,
@@ -159,21 +164,19 @@ let _formatDataForAmplitude = (data) => {
ampData.event_properties.itemName = itemName;
}
return ampData;
};
}
let _sendDataToAmplitude = (eventType, data) => {
function _sendDataToAmplitude (eventType, data) {
let amplitudeData = _formatDataForAmplitude(data);
amplitudeData.event_type = eventType;
return new Promise((resolve, reject) => {
amplitude.track(amplitudeData)
.then(resolve)
.catch(() => reject('Error while sending data to Amplitude.'));
});
};
return amplitude
.track(amplitudeData)
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
let _generateLabelForGoogleAnalytics = (data) => {
function _generateLabelForGoogleAnalytics (data) {
let label;
each(GA_POSSIBLE_LABELS, (key) => {
@@ -184,9 +187,9 @@ let _generateLabelForGoogleAnalytics = (data) => {
});
return label;
};
}
let _generateValueForGoogleAnalytics = (data) => {
function _generateValueForGoogleAnalytics (data) {
let value;
each(GA_POSSIBLE_VALUES, (key) => {
@@ -197,9 +200,9 @@ let _generateValueForGoogleAnalytics = (data) => {
});
return value;
};
}
let _sendDataToGoogle = (eventType, data) => {
function _sendDataToGoogle (eventType, data) {
let eventData = {
ec: data.gaCategory || data.category || 'behavior',
ea: eventType,
@@ -217,28 +220,28 @@ let _sendDataToGoogle = (eventType, data) => {
eventData.ev = value;
}
return new Promise((resolve, reject) => {
const promise = new Promise((resolve, reject) => {
ga.event(eventData, (err) => {
if (err) return reject(err);
resolve();
});
});
};
let _sendPurchaseDataToAmplitude = (data) => {
return promise.catch(err => logger.error(err, 'Error while sending data to Google Analytics.'));
}
function _sendPurchaseDataToAmplitude (data) {
let amplitudeData = _formatDataForAmplitude(data);
amplitudeData.event_type = 'purchase';
amplitudeData.revenue = data.purchaseValue;
return new Promise((resolve, reject) => {
amplitude.track(amplitudeData)
.then(resolve)
.catch(reject);
});
};
return amplitude
.track(amplitudeData)
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
let _sendPurchaseDataToGoogle = (data) => {
function _sendPurchaseDataToGoogle (data) {
let label = data.paymentMethod;
let type = data.purchaseType;
let price = data.purchaseValue;
@@ -256,38 +259,55 @@ let _sendPurchaseDataToGoogle = (data) => {
ev: price,
};
return new Promise((resolve) => {
ga.event(eventData).send();
ga.transaction(data.uuid, price)
.item(price, qty, sku, itemKey, variation)
.send();
const eventPromise = new Promise((resolve, reject) => {
ga.event(eventData, (err) => {
if (err) return reject(err);
resolve();
});
};
let _setOnce = (data) => {
return amplitude.identify({
user_properties: {
$setOnce: data,
},
});
};
function track (eventType, data) {
const transactionPromise = new Promise((resolve, reject) => {
ga.transaction(data.uuid, price)
.item(price, qty, sku, itemKey, variation)
.send(err => {
if (err) return reject(err);
resolve();
});
});
return Promise
.all([eventPromise, transactionPromise])
.catch(err => logger.error(err, 'Error while sending data to Google Analytics.'));
}
function _setOnce (dataToSetOnce, uuid) {
return amplitude
.identify({
user_id: _formatUUIDForAmplitude(uuid),
user_properties: {
$setOnce: dataToSetOnce,
},
})
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
// There's no error handling directly here because it's handled inside _sendDataTo{Amplitude|Google}
async function track (eventType, data) {
let promises = [
_sendDataToAmplitude(eventType, data),
_sendDataToGoogle(eventType, data),
];
if (data.user && data.user.registeredThrough) {
promises.push(_setOnce({registeredPlatform: data.user.registeredThrough}));
promises.push(_setOnce({
registeredPlatform: data.user.registeredThrough,
}, data.uuid || data.user._id));
}
return Promise.all(promises);
}
function trackPurchase (data) {
// There's no error handling directly here because it's handled inside _sendPurchaseDataTo{Amplitude|Google}
async function trackPurchase (data) {
return Promise.all([
_sendPurchaseDataToAmplitude(data),
_sendPurchaseDataToGoogle(data),
@@ -295,7 +315,7 @@ function trackPurchase (data) {
}
// Stub for non-prod environments
let mockAnalyticsService = {
const mockAnalyticsService = {
track: () => { },
trackPurchase: () => { },
};