diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js
index fc87f9dfe2..ca6f4b2c4f 100644
--- a/test/api/unit/libs/payments/payments.test.js
+++ b/test/api/unit/libs/payments/payments.test.js
@@ -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', () => {
diff --git a/website/client/src/app.vue b/website/client/src/app.vue
index 3b2cfc8817..49b49c5b67 100644
--- a/website/client/src/app.vue
+++ b/website/client/src/app.vue
@@ -64,6 +64,30 @@
>
+
+
+
+
+ {{ $t('g1g1Announcement') }}
+
+
+
+
+
+
+
+
@@ -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');
},
diff --git a/website/client/src/assets/css/sprites/spritesmith-largeSprites-0.css b/website/client/src/assets/css/sprites/spritesmith-largeSprites-0.css
index 257b1e2213..1ab4480434 100644
--- a/website/client/src/assets/css/sprites/spritesmith-largeSprites-0.css
+++ b/website/client/src/assets/css/sprites/spritesmith-largeSprites-0.css
@@ -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;
}
diff --git a/website/client/src/assets/images/sprites/spritesmith-largeSprites-0.png b/website/client/src/assets/images/sprites/spritesmith-largeSprites-0.png
index bcbd71e964..0024904d71 100644
Binary files a/website/client/src/assets/images/sprites/spritesmith-largeSprites-0.png and b/website/client/src/assets/images/sprites/spritesmith-largeSprites-0.png differ
diff --git a/website/client/src/components/payments/sendGemsModal.vue b/website/client/src/components/payments/sendGemsModal.vue
index cf16303504..8c1f303c25 100644
--- a/website/client/src/components/payments/sendGemsModal.vue
+++ b/website/client/src/components/payments/sendGemsModal.vue
@@ -76,7 +76,7 @@
-
+
+
+
{{ $t('winterPromoGiftHeader') }}
+
{{ $t('winterPromoGiftDetails1') }}
+
{{ $t('winterPromoGiftDetails2') }}
+
diff --git a/website/client/src/components/settings/subscription.vue b/website/client/src/components/settings/subscription.vue
index 0938bff3b8..4ffad1bcbc 100644
--- a/website/client/src/components/settings/subscription.vue
+++ b/website/client/src/components/settings/subscription.vue
@@ -237,6 +237,11 @@
{{ $t('giftSubscriptionText4') }}
+
+
{{ $t('winterPromoGiftHeader') }}
+
{{ $t('winterPromoGiftDetails1') }}
+
{{ $t('winterPromoGiftDetails2') }}
+
diff --git a/website/client/src/components/snackbars/notifications.vue b/website/client/src/components/snackbars/notifications.vue
index 3d44089a8a..d549b251f1 100644
--- a/website/client/src/components/snackbars/notifications.vue
+++ b/website/client/src/components/snackbars/notifications.vue
@@ -20,6 +20,10 @@
z-index: 1400; // 1400 is above modal backgrounds
&-top-pos {
+ &-double {
+ top: 145px;
+ }
+
&-normal {
top: 65px;
}
@@ -34,6 +38,7 @@
diff --git a/website/client/src/libs/userlocalManager.js b/website/client/src/libs/userlocalManager.js
index da3e30489a..9d7b55e0ba 100644
--- a/website/client/src/libs/userlocalManager.js
+++ b/website/client/src/libs/userlocalManager.js
@@ -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',
diff --git a/website/common/locales/en/limited.json b/website/common/locales/en/limited.json
index e8c312ffb3..2a4719e772 100644
--- a/website/common/locales/en/limited.json
+++ b/website/common/locales/en/limited.json
@@ -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!",
diff --git a/website/raw_sprites/spritesmith_large/promo_costume_achievement.png b/website/raw_sprites/spritesmith_large/promo_costume_achievement.png
deleted file mode 100644
index f3794e8c84..0000000000
Binary files a/website/raw_sprites/spritesmith_large/promo_costume_achievement.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/promo_delightful_dinos.png b/website/raw_sprites/spritesmith_large/promo_delightful_dinos.png
deleted file mode 100644
index 42a8d5472e..0000000000
Binary files a/website/raw_sprites/spritesmith_large/promo_delightful_dinos.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/promo_ember_thunderstorm_potions.png b/website/raw_sprites/spritesmith_large/promo_ember_thunderstorm_potions.png
deleted file mode 100644
index e277878243..0000000000
Binary files a/website/raw_sprites/spritesmith_large/promo_ember_thunderstorm_potions.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/promo_g1g1_2019.png b/website/raw_sprites/spritesmith_large/promo_g1g1_2019.png
new file mode 100644
index 0000000000..9c404c01b0
Binary files /dev/null and b/website/raw_sprites/spritesmith_large/promo_g1g1_2019.png differ
diff --git a/website/raw_sprites/spritesmith_large/promo_harvest_feast.png b/website/raw_sprites/spritesmith_large/promo_harvest_feast.png
deleted file mode 100644
index 2d5c520be2..0000000000
Binary files a/website/raw_sprites/spritesmith_large/promo_harvest_feast.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/scene_habitica_map.png b/website/raw_sprites/spritesmith_large/scene_habitica_map.png
deleted file mode 100644
index 6924c934a6..0000000000
Binary files a/website/raw_sprites/spritesmith_large/scene_habitica_map.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/scene_office.png b/website/raw_sprites/spritesmith_large/scene_office.png
deleted file mode 100644
index 6095bd22ab..0000000000
Binary files a/website/raw_sprites/spritesmith_large/scene_office.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/scene_seaserpent.png b/website/raw_sprites/spritesmith_large/scene_seaserpent.png
deleted file mode 100644
index c18d9634fc..0000000000
Binary files a/website/raw_sprites/spritesmith_large/scene_seaserpent.png and /dev/null differ
diff --git a/website/raw_sprites/spritesmith_large/scene_todos.png b/website/raw_sprites/spritesmith_large/scene_todos.png
new file mode 100644
index 0000000000..b8c52d399d
Binary files /dev/null and b/website/raw_sprites/spritesmith_large/scene_todos.png differ
diff --git a/website/raw_sprites/spritesmith_large/scene_yarn_boss.png b/website/raw_sprites/spritesmith_large/scene_yarn_boss.png
deleted file mode 100644
index c338cb7061..0000000000
Binary files a/website/raw_sprites/spritesmith_large/scene_yarn_boss.png and /dev/null differ
diff --git a/website/server/controllers/api-v3/news.js b/website/server/controllers/api-v3/news.js
index d2ac2b39e2..0f6fb5b56c 100644
--- a/website/server/controllers/api-v3/news.js
+++ b/website/server/controllers/api-v3/news.js
@@ -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 = {
${res.t('newStuff')}
- 12/10/2019 - ${LAST_ANNOUNCEMENT_TITLE}
+ 12/17/2019 - ${LAST_ANNOUNCEMENT_TITLE}
-
+
+ Gift a Subscription and Get One Free!
- 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!
- 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 :)
-
- by Piyowo and SabreCat
-
+
+ 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.
+
+
+ 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.
+
+ Thanks so much for your support! <3
+
+ Blog Post: Running a Challenge
+
+ This month's featured Wiki article 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 Twitter, Tumblr, and Facebook.
+
+ by shanaqui and the Wiki Wizards
`,
});
diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js
index b795ad4ecf..e3bf124541 100644
--- a/website/server/libs/payments/subscriptions.js
+++ b/website/server/libs/payments/subscriptions.js
@@ -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,
{
diff --git a/website/server/libs/slack.js b/website/server/libs/slack.js
index f4b4f49eaa..7a68ca71aa 100644
--- a/website/server/libs/slack.js
+++ b/website/server/libs/slack.js
@@ -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 {