mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-05-19 12:18:51 +00:00
Drop Cap Notification, Modal and A/B Test (#12651)
* add drop cap notification * add drop cap notification * add dismissible notification * fix(notification): correct remove icon positioning * track events * add modal * add back files * fix links and add missing analytics * fix rounded borders and hide sub info for subscribers * a/b test * fix comparison * Translated using Weblate (Spanish) Currently translated at 98.2% (55 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/es/ Translated using Weblate (Spanish) Currently translated at 99.4% (179 of 180 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/ Merge branch 'origin/develop' into Weblate. Translated using Weblate (Spanish) Currently translated at 99.4% (175 of 176 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/ Translated using Weblate (Spanish (Latin America)) Currently translated at 98.6% (359 of 364 strings) Translation: Habitica/Groups Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/ Translated using Weblate (Spanish) Currently translated at 85.7% (151 of 176 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/ Translated using Weblate (Spanish) Currently translated at 95.3% (538 of 564 strings) Translation: Habitica/Backgrounds Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/ Translated using Weblate (Spanish (Latin America)) Currently translated at 98.6% (359 of 364 strings) Translation: Habitica/Groups Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/ Translated using Weblate (French) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/fr/ Translated using Weblate (German) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/de/ Translated using Weblate (French) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/ Translated using Weblate (German) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/ Translated using Weblate (Czech) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Spells Translate-URL: https://translate.habitica.com/projects/habitica/spells/cs/ Translated using Weblate (Japanese) Currently translated at 100.0% (175 of 175 strings) Translation: Habitica/Subscriber Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/ Translated using Weblate (Italian) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/it/ Translated using Weblate (Italian) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/ Translated using Weblate (Czech) Currently translated at 100.0% (180 of 180 strings) Translation: Habitica/Settings Translate-URL: https://translate.habitica.com/projects/habitica/settings/cs/ Translated using Weblate (Basque) Currently translated at 100.0% (2 of 2 strings) Translation: Habitica/Noscript Translate-URL: https://translate.habitica.com/projects/habitica/noscript/eu/ Translated using Weblate (Basque) Currently translated at 6.5% (8 of 123 strings) Translation: Habitica/Communityguidelines Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/eu/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hans/ Translated using Weblate (Japanese) Currently translated at 100.0% (56 of 56 strings) Translation: Habitica/Messages Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/ Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (718 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/ Translated using Weblate (Portuguese (Brazil)) Currently translated at 99.8% (717 of 718 strings) Translation: Habitica/Questscontent Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/ * clarify a/b test values * add tests * refactor user dropdown * fix hover state * fix user dropdown * fix user menu hierarchy * restore i18n files to release version Co-authored-by: Melior <admin@habitica.com>
This commit is contained in:
parent
2832226539
commit
e04d4e8bea
24 changed files with 689 additions and 87 deletions
|
|
@ -293,4 +293,107 @@ describe('cron middleware', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Drop Cap A/B Test', async () => {
|
||||
it('enrolls web users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.a.string;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not enroll 80% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.5);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-not-enrolled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('enables the new notification for 10% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.1);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-enabled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the new notification for 10% of users', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.2);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.be.equal('drop-cap-notif-disabled');
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect subscribers', async () => {
|
||||
sandbox.stub(Math, 'random').returns(0.2);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-web';
|
||||
sandbox.stub(User.prototype, 'isSubscribed').returns(true);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not affect mobile users', async () => {
|
||||
user.lastCron = moment(new Date()).subtract({ days: 2 });
|
||||
await user.save();
|
||||
req.headers['x-client'] = 'habitica-ios';
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
cronMiddleware(req, res, async err => {
|
||||
if (err) return reject(err);
|
||||
user = await User.findById(user._id).exec();
|
||||
expect(user._ABtests.dropCapNotif).to.not.exist;
|
||||
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import randomDrop from '../../../website/common/script/fns/randomDrop';
|
||||
import i18n from '../../../website/common/script/i18n';
|
||||
import {
|
||||
generateUser,
|
||||
generateTodo,
|
||||
|
|
@ -144,5 +145,148 @@ describe('common.fns.randomDrop', () => {
|
|||
expect(acceptableDrops).to.contain(user._tmp.drop.key); // always Desert
|
||||
});
|
||||
});
|
||||
|
||||
context('drop cap notification', () => {
|
||||
let analytics;
|
||||
const req = {};
|
||||
let isSubscribedStub;
|
||||
|
||||
beforeEach(() => {
|
||||
user.addNotification = () => {};
|
||||
sandbox.stub(user, 'addNotification');
|
||||
user.isSubscribed = () => {};
|
||||
isSubscribedStub = sandbox.stub(user, 'isSubscribed');
|
||||
isSubscribedStub.returns(false);
|
||||
analytics = { track () {} };
|
||||
sandbox.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
it('sends a notification if A/B test is enabled when drop cap is reached', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.be.calledOnce;
|
||||
expect(user.addNotification).to.be.calledWith('DROP_CAP_REACHED', {
|
||||
message: i18n.t('dropCapReached'),
|
||||
items: 5,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send a notification if user is enrolled in disabled A/B test group', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if user is enrolled in disabled A/B test group', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if drop cap is not reached', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(4);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('does not send a notification if user is subscribed', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(user.addNotification).to.not.be.called;
|
||||
});
|
||||
|
||||
it('tracks drop cap reached event for enrolled users (notification enabled)', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.be.calledWith('drop cap reached');
|
||||
});
|
||||
|
||||
it('tracks drop cap reached event for enrolled users (notification disabled)', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.be.calledWith('drop cap reached');
|
||||
});
|
||||
|
||||
it('does not track drop cap reached event for users not enrolled in A/B test', () => {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
predictableRandom.returns(0.1);
|
||||
isSubscribedStub.returns(true);
|
||||
|
||||
// Max Drop Count is 5
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
randomDrop(user, { task, predictableRandom }, req, analytics);
|
||||
expect(user.items.lastDrop.count).to.equal(5);
|
||||
expect(analytics.track).to.not.be.calledWith('drop cap reached');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
BIN
website/client/src/assets/images/swords.png
Normal file
BIN
website/client/src/assets/images/swords.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
website/client/src/assets/images/swords@2x.png
Normal file
BIN
website/client/src/assets/images/swords@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.3 KiB |
BIN
website/client/src/assets/images/swords@3x.png
Normal file
BIN
website/client/src/assets/images/swords@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
font-size: 14px;
|
||||
line-height: 1.71;
|
||||
color: $gray-50;
|
||||
color: $gray-50 !important;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
|
|
@ -54,16 +54,16 @@
|
|||
|
||||
|
||||
&:active, &:hover, &:focus, &.active {
|
||||
background-color: rgba($purple-600, 0.32);
|
||||
color: $purple-200;
|
||||
background-color: rgba($purple-600, 0.32) !important;
|
||||
color: $purple-200 !important;
|
||||
}
|
||||
|
||||
&.dropdown-inactive {
|
||||
cursor: default;
|
||||
|
||||
&:active, &:hover, &.active {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
background-color: inherit !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@
|
|||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-bottom-right-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 3rem auto 3rem;
|
||||
width: auto;
|
||||
|
|
|
|||
250
website/client/src/components/achievements/dropCapReached.vue
Normal file
250
website/client/src/components/achievements/dropCapReached.vue
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<template>
|
||||
<b-modal
|
||||
id="drop-cap-reached"
|
||||
size="md"
|
||||
:hide-header="true"
|
||||
:hide-footer="!hasSubscription"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="modal-close"
|
||||
@click="close()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
<h1
|
||||
v-once
|
||||
class="header purple"
|
||||
>
|
||||
{{ $t('dropCapReached') }}
|
||||
</h1>
|
||||
<div class="max-items-wrapper d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
class="svg-icon sparkles sparkles-rotate"
|
||||
v-html="icons.sparkles"
|
||||
></div>
|
||||
<div class="max-items-module d-flex align-items-center justify-content-center flex-column">
|
||||
<h1 class="max-items">
|
||||
{{ maxItems }}
|
||||
</h1>
|
||||
<span
|
||||
v-once
|
||||
class="items-text"
|
||||
>{{ $t('items') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="svg-icon sparkles"
|
||||
v-html="icons.sparkles"
|
||||
></div>
|
||||
</div>
|
||||
<p
|
||||
v-once
|
||||
class="mb-4"
|
||||
>
|
||||
{{ $t('dropCapExplanation') }}
|
||||
</p>
|
||||
<a
|
||||
v-once
|
||||
class="standard-link d-block mb-3"
|
||||
@click="toWiki()"
|
||||
>
|
||||
{{ $t('dropCapLearnMore') }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
slot="modal-footer"
|
||||
class="footer"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
class="purple d-block font-weight-bold mb-3 mt-3"
|
||||
>
|
||||
{{ $t('lookingForMoreItems') }}
|
||||
</span>
|
||||
<img
|
||||
class="swords mb-3"
|
||||
srcset="
|
||||
~@/assets/images/swords.png,
|
||||
~@/assets/images/swords@2x.png 2x,
|
||||
~@/assets/images/swords@3x.png 3x"
|
||||
src="~@/assets/images/swords.png"
|
||||
>
|
||||
<p
|
||||
v-once
|
||||
class="subs-benefits mb-3"
|
||||
>
|
||||
{{ $t('dropCapSubs') }}
|
||||
</p>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary"
|
||||
@click="toLearnMore()"
|
||||
>
|
||||
{{ $t('learnMore') }}
|
||||
</button>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#drop-cap-reached {
|
||||
.modal-body {
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
background: $gray-700;
|
||||
border-top: none;
|
||||
padding: 0 1.5rem 2rem 1.5rem;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
width: 20.625rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
.svg-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.subs-benefits {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
width: 2.5rem;
|
||||
height: 4rem;
|
||||
|
||||
&.sparkles-rotate {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.max-items-wrapper {
|
||||
margin: 17px auto;
|
||||
}
|
||||
|
||||
.max-items-module {
|
||||
background: white;
|
||||
border-radius: 92px;
|
||||
border: 8px solid $purple-400;
|
||||
width: 92px;
|
||||
height: 92px;
|
||||
margin-left: 17px;
|
||||
margin-right: 17px;
|
||||
|
||||
.items-text {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
.max-items {
|
||||
font-size: 2rem;
|
||||
line-height: 1.25;
|
||||
color: $purple-300;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.swords {
|
||||
width: 7rem;
|
||||
height: 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
import sparkles from '@/assets/svg/star-group.svg';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
sparkles,
|
||||
}),
|
||||
maxItems: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
hasSubscription () {
|
||||
return Boolean(this.user.purchased.plan.customerId);
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:drop-cap-reached', notification => {
|
||||
this.maxItems = notification.data.items;
|
||||
this.$root.$emit('bv::show::modal', 'drop-cap-reached');
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:drop-cap-reached');
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'drop-cap-reached');
|
||||
},
|
||||
toWiki () {
|
||||
window.open('https://habitica.fandom.com/wiki/Drops', '_blank');
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'drop-cap-reached',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Drop Cap Reached > Modal > Wiki',
|
||||
});
|
||||
},
|
||||
toLearnMore () {
|
||||
this.close();
|
||||
this.$router.push('/user/settings/subscription');
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'drop-cap-reached',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Drop Cap Reached > Modal > Subscriptions',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -22,12 +22,15 @@
|
|||
export default {
|
||||
props: {
|
||||
categories: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
owner: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
member: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.message-count {
|
||||
background-color: $blue-50;
|
||||
background-color: $red-50;
|
||||
border-radius: 50%;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
|
@ -31,7 +31,6 @@
|
|||
right: 0.3em;
|
||||
top: -0.8em;
|
||||
padding: 0.2em;
|
||||
background-color: $red-50;
|
||||
}
|
||||
|
||||
.message-count.top-count-gray {
|
||||
|
|
|
|||
|
|
@ -118,11 +118,11 @@
|
|||
}
|
||||
|
||||
.notification-remove {
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
padding: 4px;
|
||||
right: 24px;
|
||||
position: relative;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
right: 0px;
|
||||
top: 10.5px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:has-icon="false"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="notification-bold-blue"
|
||||
>
|
||||
{{ $t('dropCapReached') }}
|
||||
</div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
props: {
|
||||
notification: { type: Object, required: true },
|
||||
canRemove: { type: Boolean, required: true },
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
this.$root.$emit('habitica:drop-cap-reached', this.notification);
|
||||
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'drop-cap-reached',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Drop Cap Reached > Notification Click',
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -149,6 +149,7 @@ import ACHIEVEMENT_MIND_OVER_MATTER from './notifications/mindOverMatter';
|
|||
import ONBOARDING_COMPLETE from './notifications/onboardingComplete';
|
||||
import GIFT_ONE_GET_ONE from './notifications/g1g1';
|
||||
import OnboardingGuide from './onboardingGuide';
|
||||
import DROP_CAP_REACHED from './notifications/dropCapReached';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
@ -178,6 +179,7 @@ export default {
|
|||
OnboardingGuide,
|
||||
ONBOARDING_COMPLETE,
|
||||
GIFT_ONE_GET_ONE,
|
||||
DROP_CAP_REACHED,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
|
|
@ -203,7 +205,7 @@ export default {
|
|||
'GROUP_TASK_CLAIMED', 'NEW_MYSTERY_ITEMS', 'CARD_RECEIVED',
|
||||
'NEW_INBOX_MESSAGE', 'NEW_CHAT_MESSAGE', 'UNALLOCATED_STATS_POINTS',
|
||||
'ACHIEVEMENT_JUST_ADD_WATER', 'ACHIEVEMENT_LOST_MASTERCLASSER', 'ACHIEVEMENT_MIND_OVER_MATTER',
|
||||
'VERIFY_USERNAME', 'ONBOARDING_COMPLETE',
|
||||
'VERIFY_USERNAME', 'ONBOARDING_COMPLETE', 'DROP_CAP_REACHED',
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,13 +23,6 @@
|
|||
slot="dropdown-content"
|
||||
class="user-dropdown"
|
||||
>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item edit-avatar dropdown-separated"
|
||||
@click="showAvatar('body', 'size')"
|
||||
>
|
||||
<h3>{{ user.profile.name }}</h3>
|
||||
<span class="small-text">{{ $t('editAvatar') }}</span>
|
||||
</a>
|
||||
<a
|
||||
class="topbar-dropdown-item nav-link dropdown-item
|
||||
dropdown-separated d-flex justify-content-between align-items-center"
|
||||
|
|
@ -43,20 +36,20 @@
|
|||
</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showAvatar('backgrounds', '2020')"
|
||||
>{{ $t('backgrounds') }}</a>
|
||||
@click="showAvatar('body', 'size')"
|
||||
>{{ $t('editAvatar') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showProfile('profile')"
|
||||
>{{ $t('profile') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
@click="showProfile('stats')"
|
||||
>{{ $t('stats') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
@click="showProfile('achievements')"
|
||||
>{{ $t('achievements') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
@click="showProfile('profile')"
|
||||
>{{ $t('profile') }}</a>
|
||||
<router-link
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
:to="{name: 'site'}"
|
||||
|
|
@ -75,19 +68,36 @@
|
|||
>{{ $t('logout') }}</a>
|
||||
<li
|
||||
v-if="!user.purchased.plan.customerId"
|
||||
@click="showBuyGemsModal()"
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated
|
||||
d-flex flex-column justify-content-center align-items-center dropdown-inactive subs-info"
|
||||
>
|
||||
<div class="topbar-dropdown-item dropdown-item text-center">
|
||||
<h3 class="purple">
|
||||
{{ $t('needMoreGems') }}
|
||||
</h3>
|
||||
<span class="small-text">{{ $t('needMoreGemsInfo') }}</span>
|
||||
</div>
|
||||
<div class="learn-background py-2 text-center">
|
||||
<button class="btn btn-primary btn-lg learn-button">
|
||||
{{ $t('learnMore') }}
|
||||
</button>
|
||||
</div>
|
||||
<span
|
||||
v-once
|
||||
class="purple d-block font-weight-bold mb-3"
|
||||
>
|
||||
{{ $t('lookingForMoreItems') }}
|
||||
</span>
|
||||
<img
|
||||
class="swords mb-3"
|
||||
srcset="
|
||||
~@/assets/images/swords.png,
|
||||
~@/assets/images/swords@2x.png 2x,
|
||||
~@/assets/images/swords@3x.png 3x"
|
||||
src="~@/assets/images/swords.png"
|
||||
>
|
||||
<p
|
||||
v-once
|
||||
class="subs-benefits mb-3"
|
||||
>
|
||||
{{ $t('dropCapSubs') }}
|
||||
</p>
|
||||
<button
|
||||
v-once
|
||||
class="btn btn-primary mb-4"
|
||||
@click="$router.push({name: 'subscription'})"
|
||||
>
|
||||
{{ $t('learnMore') }}
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
</menu-dropdown>
|
||||
|
|
@ -96,39 +106,30 @@
|
|||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.edit-avatar {
|
||||
h3 {
|
||||
color: $gray-10;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
width: 14.75em;
|
||||
}
|
||||
|
||||
.learn-background {
|
||||
background: url('~@/assets/images/gem-rain.png') bottom left no-repeat,
|
||||
url('~@/assets/images/gold-rain.png') bottom right no-repeat;
|
||||
}
|
||||
|
||||
.learn-button {
|
||||
margin: 0.75em 0.75em 0.75em 1em;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple-200;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
color: $gray-200;
|
||||
.subs-info {
|
||||
padding-top: 1.438rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.subs-benefits {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
font-style: normal;
|
||||
display: block;
|
||||
white-space: normal;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.swords {
|
||||
width: 7rem;
|
||||
height: 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
<div class="class-badge d-flex justify-content-center">
|
||||
<div
|
||||
class="align-self-center svg-icon"
|
||||
v-html="icons[memberClass]"
|
||||
:aria-label="$t(memberClass)"
|
||||
v-html="icons[memberClass]"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
<mind-over-matter />
|
||||
<onboarding-complete />
|
||||
<first-drops />
|
||||
<drop-cap-reached-modal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -145,6 +146,7 @@ import loginIncentives from './achievements/login-incentives';
|
|||
import onboardingComplete from './achievements/onboardingComplete';
|
||||
import verifyUsername from './settings/verifyUsername';
|
||||
import firstDrops from './achievements/firstDrops';
|
||||
import DropCapReachedModal from '@/components/achievements/dropCapReached';
|
||||
|
||||
const NOTIFICATIONS = {
|
||||
CHALLENGE_JOINED_ACHIEVEMENT: {
|
||||
|
|
@ -384,6 +386,7 @@ export default {
|
|||
justAddWater,
|
||||
onboardingComplete,
|
||||
firstDrops,
|
||||
DropCapReachedModal,
|
||||
},
|
||||
mixins: [notifications, guide],
|
||||
data () {
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@
|
|||
<span
|
||||
v-if="withPin"
|
||||
class="badge-dialog"
|
||||
tabindex="0"
|
||||
@click.prevent.stop="togglePinned()"
|
||||
@keypress.enter.prevent.stop="togglePinned()"
|
||||
tabindex="0"
|
||||
>
|
||||
<pin-badge
|
||||
:pinned="isPinned"
|
||||
|
|
@ -19,9 +19,9 @@
|
|||
<span
|
||||
class="svg-icon icon-12 close-icon"
|
||||
aria-hidden="true"
|
||||
tabindex="0"
|
||||
@click="hideDialog()"
|
||||
@keypress.enter="hideDialog()"
|
||||
tabindex="0"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
</div>
|
||||
|
|
@ -154,8 +154,8 @@
|
|||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)"
|
||||
:class="{'notEnough': !preventHealthPotion ||
|
||||
!enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
||||
@click="buyItem()"
|
||||
tabindex="0"
|
||||
@click="buyItem()"
|
||||
>
|
||||
{{ $t('buyNow') }}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
<div
|
||||
:id="itemId"
|
||||
class="item-wrapper"
|
||||
tabindex="0"
|
||||
@click="click()"
|
||||
@keypress.enter="click()"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="item"
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@
|
|||
:key="filter"
|
||||
class="filter small-text"
|
||||
:class="{active: activeFilter.label === filter}"
|
||||
tabindex="0"
|
||||
@click="activateFilter(type, filter)"
|
||||
@keypress.enter="activateFilter(type, filter)"
|
||||
tabindex="0"
|
||||
>
|
||||
{{ $t(filter) }}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -36,11 +36,11 @@
|
|||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
}, controlClass.up.inner]"
|
||||
tabindex="0"
|
||||
@click="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
@keypress.enter="(isUser && task.up && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('up') : null"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
|
|
@ -66,11 +66,11 @@
|
|||
<div
|
||||
class="task-control daily-todo-control"
|
||||
:class="controlClass.inner"
|
||||
tabindex="0"
|
||||
@click="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
@keypress.enter="isUser && !task.group.approval.requested
|
||||
? score(task.completed ? 'down' : 'up' ) : null"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
|
|
@ -97,9 +97,9 @@
|
|||
<div
|
||||
class="task-clickable-area"
|
||||
:class="{'task-clickable-area-user': isUser}"
|
||||
tabindex="0"
|
||||
@click="edit($event, task)"
|
||||
@keypress.enter="edit($event, task)"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3
|
||||
|
|
@ -140,8 +140,8 @@
|
|||
<div
|
||||
v-if="isUser"
|
||||
class="dropdown-item"
|
||||
@click="moveToTop"
|
||||
tabindex="0"
|
||||
@click="moveToTop"
|
||||
@keypress.enter="moveToTop"
|
||||
>
|
||||
<span class="dropdown-icon-item">
|
||||
|
|
@ -155,8 +155,8 @@
|
|||
<div
|
||||
v-if="isUser"
|
||||
class="dropdown-item"
|
||||
@click="moveToBottom"
|
||||
tabindex="0"
|
||||
@click="moveToBottom"
|
||||
@keypress.enter="moveToBottom"
|
||||
>
|
||||
<span class="dropdown-icon-item">
|
||||
|
|
@ -170,8 +170,8 @@
|
|||
<div
|
||||
v-if="showDelete"
|
||||
class="dropdown-item"
|
||||
@click="destroy"
|
||||
tabindex="0"
|
||||
@click="destroy"
|
||||
@keypress.enter="destroy"
|
||||
>
|
||||
<span class="dropdown-icon-item delete-task-item">
|
||||
|
|
@ -349,11 +349,11 @@
|
|||
'task-not-scoreable': isUser !== true
|
||||
|| (task.group.approval.requested && !task.group.approval.approved),
|
||||
}, controlClass.down.inner]"
|
||||
tabindex="0"
|
||||
@click="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
@keypress.enter="(isUser && task.down && (!task.group.approval.requested
|
||||
|| task.group.approval.approved)) ? score('down') : null"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
v-if="!isUser"
|
||||
|
|
@ -373,9 +373,9 @@
|
|||
v-if="task.type === 'reward'"
|
||||
class="right-control d-flex align-items-center justify-content-center reward-control"
|
||||
:class="controlClass.bg"
|
||||
tabindex="0"
|
||||
@click="isUser ? score('down') : null"
|
||||
@keypress.enter="isUser ? score('down') : null"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="svg-icon"
|
||||
|
|
|
|||
|
|
@ -156,6 +156,38 @@ export default function randomDrop (user, options, req = {}, analytics) {
|
|||
user.items.lastDrop.date = Number(new Date());
|
||||
user.items.lastDrop.count += 1;
|
||||
|
||||
const dropN = user.items.lastDrop.count;
|
||||
const dropCapReached = dropN === maxDropCount;
|
||||
const isEnrolledInDropCapTest = user._ABtests.dropCapNotif
|
||||
&& user._ABtests.dropCapNotif !== 'drop-cap-notif-not-enrolled';
|
||||
const hasActiveDropCapNotif = isEnrolledInDropCapTest
|
||||
&& user._ABtests.dropCapNotif === 'drop-cap-notif-enabled';
|
||||
|
||||
// Unsubscribed users get a notification when they reach the drop cap
|
||||
// One per day
|
||||
if (
|
||||
hasActiveDropCapNotif && dropCapReached
|
||||
&& user.addNotification
|
||||
&& user.isSubscribed && !user.isSubscribed()
|
||||
) {
|
||||
const prevNotifIndex = user.notifications.findIndex(n => n.type === 'DROP_CAP_REACHED');
|
||||
if (prevNotifIndex !== -1) user.notifications.splice(prevNotifIndex, 1);
|
||||
|
||||
user.addNotification('DROP_CAP_REACHED', {
|
||||
message: i18n.t('dropCapReached', req.language),
|
||||
items: dropN,
|
||||
});
|
||||
}
|
||||
|
||||
if (isEnrolledInDropCapTest) {
|
||||
analytics.track('drop cap reached', {
|
||||
uuid: user._id,
|
||||
dropCap: maxDropCount,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
|
||||
analytics.track('dropped item', {
|
||||
uuid: user._id,
|
||||
|
|
@ -164,15 +196,6 @@ export default function randomDrop (user, options, req = {}, analytics) {
|
|||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
if (user.items.lastDrop.count === maxDropCount) {
|
||||
analytics.track('drop cap reached', {
|
||||
uuid: user._id,
|
||||
dropCap: maxDropCount,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -464,8 +464,12 @@ async function scoreTask (user, task, direction, req, res) {
|
|||
user,
|
||||
});
|
||||
|
||||
const isEnrolledInDropCapTest = user._ABtests.dropCapNotif
|
||||
&& user._ABtests.dropCapNotif !== 'drop-cap-notif-not-enrolled';
|
||||
|
||||
// Track when new users (first 7 days) score tasks
|
||||
if (moment().diff(user.auth.timestamps.created, 'days') < 7) {
|
||||
// or if they're enrolled in the Drop Cap A/B Test
|
||||
if (moment().diff(user.auth.timestamps.created, 'days') < 7 || isEnrolledInDropCapTest) {
|
||||
res.analytics.track('task score', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
|
|
|
|||
|
|
@ -52,6 +52,26 @@ async function unlockUser (user) {
|
|||
}).exec();
|
||||
}
|
||||
|
||||
// Enroll users in the Drop Cap A/B Test
|
||||
function dropCapABTest (user, req) {
|
||||
// Only target users that use web for cron and aren't subscribed.
|
||||
// Those using mobile aren't excluded as they may use it later
|
||||
const isWeb = req.headers['x-client'] === 'habitica-web';
|
||||
|
||||
if (isWeb && !user._ABtests.dropCapNotif && !user.isSubscribed()) {
|
||||
const testGroup = Math.random();
|
||||
// Enroll 20% of users, splitting them 50/50
|
||||
if (testGroup <= 0.1) {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-enabled';
|
||||
} else if (testGroup <= 0.2) {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-disabled';
|
||||
} else {
|
||||
user._ABtests.dropCapNotif = 'drop-cap-notif-not-enrolled';
|
||||
}
|
||||
user.markModified('_ABtests');
|
||||
}
|
||||
}
|
||||
|
||||
async function cronAsync (req, res) {
|
||||
let { user } = res.locals;
|
||||
if (!user) return null; // User might not be available when authentication is not mandatory
|
||||
|
|
@ -66,6 +86,7 @@ async function cronAsync (req, res) {
|
|||
res.locals.user = user;
|
||||
const { daysMissed, timezoneUtcOffsetFromUserPrefs } = user.daysUserHasMissed(now, req);
|
||||
|
||||
dropCapABTest(user, req);
|
||||
await updateLastCron(user, now);
|
||||
|
||||
if (daysMissed <= 0) {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ const NOTIFICATION_TYPES = [
|
|||
'ACHIEVEMENT_GOOD_AS_GOLD',
|
||||
'ACHIEVEMENT_ALL_THAT_GLITTERS',
|
||||
'ACHIEVEMENT', // generic achievement notification, details inside `notification.data`
|
||||
'DROP_CAP_REACHED',
|
||||
];
|
||||
|
||||
const { Schema } = mongoose;
|
||||
|
|
|
|||
Loading…
Reference in a new issue