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 {