feat(event): turn on G1G1 2019

This commit is contained in:
Sabe Jones
2019-12-17 14:04:55 -06:00
parent e223311aac
commit 34983f1221
22 changed files with 273 additions and 107 deletions

View File

@@ -252,6 +252,77 @@ describe('payments/index', () => {
},
});
});
context('Winter 2018-19 Gift-1-Get-1 Promotion', async () => {
it('creates a gift subscription for purchaser and recipient if none exist', async () => {
await api.createSubscription(data);
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(user.purchased.plan.customerId).to.eql('Gift');
expect(user.purchased.plan.dateTerminated).to.exist;
expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.dateCreated).to.exist;
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(recipient.purchased.plan.customerId).to.eql('Gift');
expect(recipient.purchased.plan.dateTerminated).to.exist;
expect(recipient.purchased.plan.dateUpdated).to.exist;
expect(recipient.purchased.plan.dateCreated).to.exist;
});
it('adds extraMonths to existing subscription for purchaser and creates a gift subscription for recipient without sub', async () => {
user.purchased.plan = plan;
expect(user.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(3);
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(recipient.purchased.plan.customerId).to.eql('Gift');
expect(recipient.purchased.plan.dateTerminated).to.exist;
expect(recipient.purchased.plan.dateUpdated).to.exist;
expect(recipient.purchased.plan.dateCreated).to.exist;
});
it('adds extraMonths to existing subscription for recipient and creates a gift subscription for purchaser without sub', async () => {
recipient.purchased.plan = plan;
expect(recipient.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
expect(user.items.pets['Jackalope-RoyalPurple']).to.eql(5);
expect(user.purchased.plan.customerId).to.eql('Gift');
expect(user.purchased.plan.dateTerminated).to.exist;
expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.dateCreated).to.exist;
});
it('adds extraMonths to existing subscriptions for purchaser and recipient', async () => {
user.purchased.plan = plan;
recipient.purchased.plan = plan;
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(recipient.purchased.plan.extraMonths).to.eql(0);
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(3);
expect(recipient.purchased.plan.extraMonths).to.eql(3);
});
it('sends a private message about the promotion', async () => {
await api.createSubscription(data);
const msg = '`Hello sender, you received 3 months of subscription as part of our holiday gift-giving promotion!`';
expect(user.sendMessage).to.be.calledTwice;
expect(user.sendMessage).to.be.calledWith(user, { senderMsg: msg });
});
});
});
context('Purchasing a subscription for self', () => {

View File

@@ -64,6 +64,30 @@
></span>
</div>
</div>
<div
class="g1g1-banner d-flex justify-content-center align-items-center"
v-if="!giftingHidden">
<div
class="svg-icon svg-gifts left-gift"
v-html="icons.gifts">
</div>
<router-link class="g1g1-link" to="/user/settings/subscription">
{{ $t('g1g1Announcement') }}
</router-link>
<div
class="svg-icon svg-gifts right-gift"
v-html="icons.gifts">
</div>
<div
class="closepadding"
@click="hideGiftingBanner()">
<span
class="svg-icon inline-icon icon-10"
aria-hidden="true"
v-html="icons.close">
</span>
</div>
</div>
<notifications-display />
<app-menu />
<div class="container-fluid">
@@ -135,10 +159,43 @@
cursor: crosshair;
}
.closepadding {
margin: 11px 24px;
display: inline-block;
position: relative;
right: 0;
top: 0;
cursor: pointer;
}
.container-fluid {
flex: 1 0 auto;
}
.g1g1-banner {
width: 100%;
min-height: 2.5rem;
background-color: $teal-50;
}
.g1g1-link {
color: $white;
}
.left-gift {
margin: auto 1rem auto auto;
}
.right-gift {
margin: auto auto auto 1rem;
filter: flipH;
transform: scaleX(-1);
}
.svg-gifts {
width: 4.6rem;
}
.notification {
border-radius: 1000px;
background-color: $green-10;
@@ -165,15 +222,6 @@
margin: auto;
}
.closepadding {
margin: 11px 24px;
display: inline-block;
position: relative;
right: 0;
top: 0;
cursor: pointer;
}
@media only screen and (max-width: 768px) {
.content {
font-size: 12px;
@@ -239,8 +287,14 @@ import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import spellsMixin from '@/mixins/spells';
import { CONSTANTS, getLocalSetting, removeLocalSetting } from '@/libs/userlocalManager';
import {
CONSTANTS,
getLocalSetting,
removeLocalSetting,
setLocalSetting,
} from '@/libs/userlocalManager';
import gifts from '@/assets/svg/gifts.svg';
import svgClose from '@/assets/svg/close.svg';
import bannedAccountModal from '@/components/bannedAccountModal';
@@ -267,6 +321,7 @@ export default {
return {
icons: Object.freeze({
close: svgClose,
gifts,
}),
selectedItemToBuy: null,
selectedSpellToBuy: null,
@@ -277,6 +332,7 @@ export default {
loading: true,
currentTipNumber: 0,
bannerHidden: false,
giftingHidden: getLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY) === 'dismissed',
};
},
computed: {
@@ -670,6 +726,10 @@ export default {
hideBanner () {
this.bannerHidden = true;
},
hideGiftingBanner () {
setLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY, 'dismissed');
this.giftingHidden = true;
},
resumeDamage () {
this.$store.dispatch('user:sleep');
},

View File

@@ -1,72 +1,36 @@
.promo_achievement_white {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -928px -465px;
background-position: -283px -293px;
width: 204px;
height: 102px;
}
.promo_armoire_backgrounds_201912 {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -928px 0px;
background-position: 0px 0px;
width: 423px;
height: 147px;
}
.promo_costume_achievement {
.promo_g1g1_2019 {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1175px -296px;
width: 144px;
height: 156px;
}
.promo_delightful_dinos {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -752px;
width: 423px;
height: 147px;
}
.promo_ember_thunderstorm_potions {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -424px -752px;
width: 423px;
height: 147px;
}
.promo_harvest_feast {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -928px -296px;
width: 246px;
height: 168px;
background-position: 0px -148px;
width: 357px;
height: 144px;
}
.promo_mystery_201912 {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -928px -148px;
background-position: 0px -293px;
width: 282px;
height: 147px;
}
.promo_take_this {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1211px -148px;
background-position: -424px -196px;
width: 96px;
height: 69px;
}
.scene_habitica_map {
.scene_todos {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px 0px;
width: 450px;
height: 450px;
}
.scene_office {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -421px -451px;
width: 360px;
height: 240px;
}
.scene_seaserpent {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -451px 0px;
width: 476px;
height: 364px;
}
.scene_yarn_boss {
background-image: url('~@/assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -451px;
width: 420px;
height: 300px;
background-position: -424px 0px;
width: 240px;
height: 195px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -76,7 +76,7 @@
</h3>
<div class="panel-body">
<div class="row">
<div class="col-md-12">
<div class="col-md-4">
<div class="form-group">
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
<div
@@ -98,6 +98,11 @@
</div>
</div>
</div>
<div class="col-md-8">
<h4 v-once> {{ $t('winterPromoGiftHeader') }} </h4>
<p v-once> {{ $t('winterPromoGiftDetails1') }} </p>
<p v-once> {{ $t('winterPromoGiftDetails2') }} </p>
</div>
</div>
</div>
</div>

View File

@@ -237,6 +237,11 @@
{{ $t('giftSubscriptionText4') }}
</h4>
</div>
<div class="col-6">
<h2 v-once> {{ $t('winterPromoGiftHeader') }} </h2>
<p v-once> {{ $t('winterPromoGiftDetails1') }} </p>
<p v-once> {{ $t('winterPromoGiftDetails2') }} </p>
</div>
</div>
</div>
</template>

View File

@@ -20,6 +20,10 @@
z-index: 1400; // 1400 is above modal backgrounds
&-top-pos {
&-double {
top: 145px;
}
&-normal {
top: 65px;
}
@@ -34,6 +38,7 @@
<script>
import { mapState } from '@/libs/store';
import notification from './notification';
import { CONSTANTS, getLocalSetting } from '@/libs/userlocalManager';
export default {
components: {
@@ -47,7 +52,9 @@ export default {
notificationsTopPos () {
const base = 'notifications-top-pos-';
let modifier = '';
if (this.userSleeping) {
if (this.userSleeping && this.giftingShown) {
modifier = 'double';
} else if (this.userSleeping || this.giftingShown) {
modifier = 'sleeping';
} else {
modifier = 'normal';
@@ -55,5 +62,10 @@ export default {
return `${base}${modifier}`;
},
},
data () {
return {
giftingShown: getLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY) !== 'dismissed',
};
},
};
</script>

View File

@@ -5,6 +5,7 @@ const CONSTANTS = {
EQUIPMENT_DRAWER_STATE: 'equipment-drawer-state',
CURRENT_EQUIPMENT_DRAWER_TAB: 'current-equipment-drawer-tab',
STABLE_SORT_STATE: 'stable-sort-state',
GIFTING_BANNER_DISPLAY: 'gifting-banner-display',
},
drawerStateValues: {
DRAWER_CLOSED: 'drawer-closed',

View File

@@ -163,7 +163,7 @@
"dateEndJanuary": "January 31",
"dateEndFebruary": "February 28",
"winterPromoGiftHeader": "GIFT A SUBSCRIPTION AND GET ONE FREE!",
"winterPromoGiftDetails1": "Until January 15th only, when you gift somebody a subscription, you get the same subscription for yourself for free!",
"winterPromoGiftDetails1": "Until January 6th only, when you gift somebody a subscription, you get the same subscription for yourself for free!",
"winterPromoGiftDetails2": "Please note that if you or your gift recipient already have a recurring subscription, the gifted subscription will only start after that subscription is cancelled or has expired. Thanks so much for your support! <3",
"discountBundle": "bundle",
"g1g1Announcement": "Gift a Subscription, Get a Subscription Free event going on now!",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -4,7 +4,7 @@ const api = {};
// @TODO export this const, cannot export it from here because only routes are exported from
// controllers
const LAST_ANNOUNCEMENT_TITLE = 'NEW PET COLLECTION BADGES!';
const LAST_ANNOUNCEMENT_TITLE = 'GIFT ONE, GET ONE PROMOTION! AND BLOG POST ON RUNNING CHALLENGES';
const worldDmg = { // @TODO
bailey: false,
};
@@ -31,23 +31,46 @@ api.getNews = {
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>12/10/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<h2>12/17/2019 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div>
</div>
<hr/>
<div class="promo_achievement_white center-block"></div>
<div class="promo_g1g1_2019 center-block"></div>
<h3>Gift a Subscription and Get One Free!</h3>
<p>
We're releasing a new achievement so you can celebrate your successes in the world of
Habitican pet collecting! Earn the Primed for Painting and Pearly Pro achievements by
collecting White pets and mounts and you'll earn a nifty badge for your profile.
In honor of the season of giving--and due to popular demand!--we're bringing back a very
special promotion from now until January 6. Now when you gift somebody a subscription,
you get the same subscription for yourself for free!
</p>
<p>
If you already have all the White pets and/or mounts in your stable, you'll receive the
badge automatically! Check your profile and celebrate your new achievement with pride.
Subscribers get tons of perks every month, including exclusive equipment, the ability to
buy Gems with Gold, a special Jackalope Pet, and increased data history. Plus, it helps
keep Habitica running :)
</p>
<div class="small mb-3">
by Piyowo and SabreCat
</div>
<p>
To gift a subscription to someone on our mobile apps, just go to Menu and tap the Gift
One Get One banner. On web, just open their profile and click the present icon in the
upper right. You can open their profile by clicking their avatar in your party header or
their name in chat.
</p>
<p>
Please note that this promotion only applies when you gift to another Habitican. If you
or your gift recipient already have a recurring subscription, the gifted subscription
will only start after that subscription is cancelled or has expired.
</p>
<p>Thanks so much for your support! <3</p>
<div class="scene_todos center-block"></div>
<h3>Blog Post: Running a Challenge</h3>
<p>
This month's <a href="https://habitica.wordpress.com/2019/12/11/running-a-challenge/"
target="_blank">featured Wiki article</a> is about Running a Challenge! We hope that it
will help you as look for exciting ways to motivate yourself and others. Be sure to check
it out, and let us know what you think by reaching out on <a
href="https://twitter.com/habitica" target="_blank">Twitter</a>, <a
href="http://blog.habitrpg.com" target="_blank">Tumblr</a>, and <a
href='https://facebook.com/habitica' target="_blank">Facebook</a>.
</p>
<div class="small mb-3">by shanaqui and the Wiki Wizards</div>
</div>
`,
});

View File

@@ -165,56 +165,81 @@ async function createSubscription (data) {
txnEmail(data.user, emailType);
}
analytics.trackPurchase({
uuid: data.user._id,
groupId,
itemPurchased,
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
purchaseType,
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: block.price,
headers: data.headers,
});
if (!data.promo) {
analytics.trackPurchase({
uuid: data.user._id,
groupId,
itemPurchased,
sku: `${data.paymentMethod.toLowerCase()}-subscription`,
purchaseType,
paymentMethod: data.paymentMethod,
quantity: 1,
gift: Boolean(data.gift),
purchaseValue: block.price,
headers: data.headers,
});
}
if (!group) data.user.purchased.txnCount += 1;
if (!group && !data.promo) data.user.purchased.txnCount += 1;
if (data.gift) {
const byUserName = getUserInfo(data.user, ['name']).name;
// generate the message in both languages, so both users can understand it
const languages = [data.user.preferences.language, data.gift.member.preferences.language];
let senderMsg = shared.i18n.t('giftedSubscriptionFull', {
username: data.gift.member.profile.name,
sender: byUserName,
monthCount: shared.content.subscriptionBlocks[data.gift.subscription.key].months,
}, languages[0]);
senderMsg = `\`${senderMsg}\``;
if (!data.promo) {
let senderMsg = shared.i18n.t('giftedSubscriptionFull', {
username: data.gift.member.profile.name,
sender: byUserName,
monthCount: shared.content.subscriptionBlocks[data.gift.subscription.key].months,
}, languages[0]);
senderMsg = `\`${senderMsg}\``;
let receiverMsg = shared.i18n.t('giftedSubscriptionFull', {
username: data.gift.member.profile.name,
sender: byUserName,
monthCount: shared.content.subscriptionBlocks[data.gift.subscription.key].months,
}, languages[1]);
receiverMsg = `\`${receiverMsg}\``;
let receiverMsg = shared.i18n.t('giftedSubscriptionFull', {
username: data.gift.member.profile.name,
sender: byUserName,
monthCount: shared.content.subscriptionBlocks[data.gift.subscription.key].months,
}, languages[1]);
receiverMsg = `\`${receiverMsg}\``;
if (data.gift.message) {
receiverMsg += ` ${data.gift.message}`;
senderMsg += ` ${data.gift.message}`;
if (data.gift.message) {
receiverMsg += ` ${data.gift.message}`;
senderMsg += ` ${data.gift.message}`;
}
data.user.sendMessage(data.gift.member, { receiverMsg, senderMsg, save: false });
}
data.user.sendMessage(data.gift.member, { receiverMsg, senderMsg, save: false });
if (data.gift.member.preferences.emailNotifications.giftedSubscription !== false) {
txnEmail(data.gift.member, 'gifted-subscription', [
{ name: 'GIFTER', content: byUserName },
{ name: 'X_MONTHS_SUBSCRIPTION', content: months },
]);
if (data.promo) {
txnEmail(data.gift.member, 'gift-one-get-one', [
{ name: 'GIFTEE_USERNAME', content: data.promoUsername },
{ name: 'X_MONTHS_SUBSCRIPTION', content: months },
]);
} else {
txnEmail(data.gift.member, 'gifted-subscription', [
{ name: 'GIFTER', content: byUserName },
{ name: 'X_MONTHS_SUBSCRIPTION', content: months },
]);
}
}
// Only send push notifications if sending to a user other than yourself
if (data.gift.member._id !== data.user._id) {
const promoData = {
user: data.user,
gift: {
member: data.user,
subscription: {
key: data.gift.subscription.key,
},
},
paymentMethod: data.paymentMethod,
promo: 'Winter',
promoUsername: data.gift.member.auth.local.username,
};
await this.createSubscription(promoData);
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
sendPushNotification(data.gift.member,
{

View File

@@ -182,7 +182,7 @@ function sendSubscriptionNotification ({
let text;
const timestamp = new Date();
if (recipient.id) {
text = `${buyer.name} ${buyer.id} ${buyer.email} bought a ${months}-month gift subscription for ${recipient.name} ${recipient.id} ${recipient.email} using ${paymentMethod} on ${timestamp}`;
text = `${buyer.name} ${buyer.id} ${buyer.email} bought a ${months}-month gift subscription for ${recipient.name} ${recipient.id} ${recipient.email} and got a promo using ${paymentMethod} on ${timestamp}`;
} else if (groupId) {
text = `${buyer.name} ${buyer.id} ${buyer.email} bought a 1-month recurring group-plan for ${groupId} using ${paymentMethod} on ${timestamp}`;
} else {