Gift 1 Get 1 Promo 2018-19! (#10915)

* feat(subscription): promo banner in modal

* feat(subscription): promo banner on main page

* fix(banners): remove extraneous margin adjustment

* fix(banners): various

* feat(promotion): gift 1, get 1

* fix(promo): various

* chore(promo): add Bailey

* fix(promo): use different email template for promo beneficiary

* fix(promo): turns out Winter is meaningful
This commit is contained in:
Sabe Jones
2018-12-18 15:28:53 -06:00
committed by GitHub
parent bd1f6918ba
commit 8220199e49
18 changed files with 533 additions and 282 deletions

View File

@@ -209,7 +209,7 @@ describe('payments/index', () => {
await api.createSubscription(data); await api.createSubscription(data);
let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`'; let msg = '\`Hello recipient, sender has sent you 3 months of subscription!\`';
expect(user.sendMessage).to.be.calledOnce; expect(user.sendMessage).to.be.calledTwice;
expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false }); expect(user.sendMessage).to.be.calledWith(recipient, { receiverMsg: msg, senderMsg: msg, save: false });
}); });
@@ -247,6 +247,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);
let 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', () => { context('Purchasing a subscription for self', () => {

View File

@@ -16,18 +16,24 @@ div
router-view(v-if="!isUserLoggedIn || isStaticPage") router-view(v-if="!isUserLoggedIn || isStaticPage")
template(v-else) template(v-else)
template(v-if="isUserLoaded") template(v-if="isUserLoaded")
div.resting-banner(v-show="showRestingBanner", ref="restingBanner") .resting-banner(v-show="showRestingBanner", ref="restingBanner")
span.content span.content
span.label.d-inline.d-sm-none {{ $t('innCheckOutBannerShort') }} span.label.d-inline.d-sm-none {{ $t('innCheckOutBannerShort') }}
span.label.d-none.d-sm-inline {{ $t('innCheckOutBanner') }} span.label.d-none.d-sm-inline {{ $t('innCheckOutBanner') }}
span.separator | span.separator |
span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }} span.resume(@click="resumeDamage()") {{ $t('resumeDamage') }}
div.closepadding(@click="hideBanner()") .closepadding(@click="hideBanner()")
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close")
.g1g1-banner.d-flex.justify-content-center.align-items-center(v-if="!giftingHidden")
.svg-icon.svg-gifts.left-gift(v-html="icons.gifts")
router-link(:to="{name: 'subscription'}") {{ $t('g1g1Announcement') }}
.svg-icon.svg-gifts.right-gift(v-html="icons.gifts")
.closepadding(@click="hideGiftingBanner()")
span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close") span.svg-icon.inline.icon-10(aria-hidden="true", v-html="icons.close")
notifications-display notifications-display
app-menu(:class='{"restingInn": showRestingBanner}' :style="{ marginTop: bannerHeight + 'px' }") app-menu
.container-fluid .container-fluid
app-header(:class='{"restingInn": showRestingBanner}') app-header
buyModal( buyModal(
:item="selectedItemToBuy || {}", :item="selectedItemToBuy || {}",
:withPin="true", :withPin="true",
@@ -50,6 +56,13 @@ div
<style lang='scss' scoped> <style lang='scss' scoped>
@import '~client/assets/scss/colors.scss'; @import '~client/assets/scss/colors.scss';
#app {
height: calc(100% - 56px); /* 56px is the menu */
display: flex;
flex-direction: column;
min-height: 100vh;
}
#loading-screen-inapp { #loading-screen-inapp {
#melior { #melior {
margin: 0 auto; margin: 0 auto;
@@ -79,6 +92,46 @@ div
cursor: crosshair; cursor: crosshair;
} }
.container-fluid {
overflow-x: hidden;
flex: 1 0 auto;
}
.g1g1-banner {
width: 100%;
min-height: 2.5rem;
background-color: #34b5c1;
a {
color: $white;
text-decoration: none;
font-weight: bold;
}
.closepadding {
margin: 11px 24px;
display: inline-block;
position: relative;
right: 0;
top: 0;
cursor: pointer;
}
.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 { .notification {
border-radius: 1000px; border-radius: 1000px;
background-color: $green-10; background-color: $green-10;
@@ -89,42 +142,10 @@ div
margin-bottom: .5em; margin-bottom: .5em;
} }
.container-fluid {
overflow-x: hidden;
flex: 1 0 auto;
}
#app {
height: calc(100% - 56px); /* 56px is the menu */
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal {
overflow-y: scroll !important;
}
.modal-backdrop.show {
opacity: .9 !important;
background-color: $purple-100 !important;
}
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1600 !important; /* Must stay above nav bar */
}
.resting-banner { .resting-banner {
width: 100%; width: 100%;
min-height: 40px; min-height: 40px;
background-color: $blue-10; background-color: $blue-10;
position: fixed;
top: 0; top: 0;
z-index: 1300; z-index: 1300;
display: flex; display: flex;
@@ -140,14 +161,10 @@ div
.closepadding { .closepadding {
margin: 11px 24px; margin: 11px 24px;
display: inline-block; display: inline-block;
position: absolute; position: relative;
right: 0; right: 0;
top: 0; top: 0;
cursor: pointer; cursor: pointer;
span svg path {
stroke: $blue-500;
}
} }
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
@@ -170,6 +187,25 @@ div
} }
</style> </style>
<style lang='scss'>
@import '~client/assets/scss/colors.scss';
/* @TODO: The modal-open class is not being removed. Let's try this for now */
.modal {
overflow-y: scroll !important;
}
.modal-backdrop.show {
opacity: .9 !important;
background-color: $purple-100 !important;
}
/* Push progress bar above modals */
#nprogress .bar {
z-index: 1600 !important; /* Must stay above nav bar */
}
</style>
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { loadProgressBar } from 'axios-progress-bar'; import { loadProgressBar } from 'axios-progress-bar';
@@ -189,8 +225,9 @@ import amazonPaymentsModal from 'client/components/payments/amazonModal';
import paymentsSuccessModal from 'client/components/payments/successModal'; import paymentsSuccessModal from 'client/components/payments/successModal';
import spellsMixin from 'client/mixins/spells'; import spellsMixin from 'client/mixins/spells';
import { CONSTANTS, getLocalSetting, removeLocalSetting } from 'client/libs/userlocalManager'; import { CONSTANTS, getLocalSetting, removeLocalSetting, setLocalSetting } from 'client/libs/userlocalManager';
import gifts from 'assets/svg/gifts.svg';
import svgClose from 'assets/svg/close.svg'; import svgClose from 'assets/svg/close.svg';
import bannedAccountModal from 'client/components/bannedAccountModal'; import bannedAccountModal from 'client/components/bannedAccountModal';
@@ -215,6 +252,7 @@ export default {
return { return {
icons: Object.freeze({ icons: Object.freeze({
close: svgClose, close: svgClose,
gifts,
}), }),
selectedItemToBuy: null, selectedItemToBuy: null,
selectedSpellToBuy: null, selectedSpellToBuy: null,
@@ -226,6 +264,7 @@ export default {
currentTipNumber: 0, currentTipNumber: 0,
bannerHidden: false, bannerHidden: false,
bannerHeight: 0, bannerHeight: 0,
giftingHidden: getLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY) === 'dismissed',
}; };
}, },
computed: { computed: {
@@ -423,14 +462,6 @@ export default {
this.hideLoadingScreen(); this.hideLoadingScreen();
window.addEventListener('resize', this.setBannerOffset);
// Adjust the positioning of the header banners
this.$watch('showRestingBanner', () => {
this.$nextTick(() => {
this.setBannerOffset();
});
}, {immediate: true});
// Adjust the timezone offset // Adjust the timezone offset
if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) { if (this.user.preferences.timezoneOffset !== this.browserTimezoneOffset) {
this.$store.dispatch('user:set', { this.$store.dispatch('user:set', {
@@ -465,7 +496,6 @@ export default {
this.$root.$off('bv::show::modal'); this.$root.$off('bv::show::modal');
this.$root.$off('buyModal::showItem'); this.$root.$off('buyModal::showItem');
this.$root.$off('selectMembersModal::showItem'); this.$root.$off('selectMembersModal::showItem');
window.removeEventListener('resize', this.setBannerOffset);
}, },
mounted () { mounted () {
// Remove the index.html loading screen and now show the inapp loading // Remove the index.html loading screen and now show the inapp loading
@@ -624,22 +654,14 @@ export default {
}, },
hideBanner () { hideBanner () {
this.bannerHidden = true; this.bannerHidden = true;
this.setBannerOffset(); },
hideGiftingBanner () {
setLocalSetting(CONSTANTS.keyConstants.GIFTING_BANNER_DISPLAY, 'dismissed');
this.giftingHidden = true;
}, },
resumeDamage () { resumeDamage () {
this.$store.dispatch('user:sleep'); this.$store.dispatch('user:sleep');
}, },
setBannerOffset () {
let contentPlacement = 0;
if (this.showRestingBanner && this.$refs.restingBanner !== undefined) {
contentPlacement = this.$refs.restingBanner.clientHeight;
}
this.bannerHeight = contentPlacement;
let smartBanner = document.getElementsByClassName('smartbanner')[0];
if (smartBanner !== undefined) {
smartBanner.style.top = `${contentPlacement}px`;
}
},
}, },
}; };
</script> </script>

View File

@@ -1,6 +1,6 @@
.achievement-costumeContest6x { .achievement-costumeContest6x {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -438px; background-position: -740px -420px;
width: 144px; width: 144px;
height: 156px; height: 156px;
} }
@@ -34,6 +34,12 @@
width: 417px; width: 417px;
height: 147px; height: 147px;
} }
.promo_g1g1 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -867px;
width: 237px;
height: 150px;
}
.promo_ios { .promo_ios {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -361px; background-position: 0px -361px;
@@ -42,22 +48,34 @@
} }
.promo_mystery_201811 { .promo_mystery_201811 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -142px; background-position: -1136px -571px;
width: 282px; width: 282px;
height: 147px; height: 147px;
} }
.promo_piyo { .promo_piyo {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -290px; background-position: -1136px -719px;
width: 279px; width: 279px;
height: 147px; height: 147px;
} }
.promo_studying {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -142px;
width: 220px;
height: 232px;
}
.promo_take_this { .promo_take_this {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1281px -438px; background-position: -1357px -142px;
width: 96px; width: 96px;
height: 69px; height: 69px;
} }
.promo_todos {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -375px;
width: 240px;
height: 195px;
}
.promo_turkey_day_2018 { .promo_turkey_day_2018 {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: 0px -871px; background-position: 0px -871px;
@@ -90,7 +108,7 @@
} }
.scene_veteran_pets { .scene_veteran_pets {
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png'); background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
background-position: -1136px -595px; background-position: -740px -577px;
width: 242px; width: 242px;
height: 62px; height: 62px;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12"> <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12">
<g fill="none" fill-rule="evenodd" stroke="#A5A1AC" stroke-width="2"> <g fill="none" fill-rule="evenodd" stroke="#FFF" stroke-width="2" opacity=".48">
<path d="M1 11L11 1M11 11L1 1"/> <path d="M1 11L11 1M11 11L1 1"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 215 B

After

Width:  |  Height:  |  Size: 226 B

View File

@@ -0,0 +1,44 @@
<svg xmlns="http://www.w3.org/2000/svg" width="73" height="27" viewBox="0 0 73 27">
<g fill="none" fill-rule="evenodd">
<path fill="#8EEDF6" d="M4.333 9.757l2.166-.554-1.742-1.401-.554-2.166-1.401 1.742-2.166.554 1.742 1.4.554 2.167zM70.333 5.757l2.166-.554-1.742-1.401-.554-2.166-1.401 1.742-2.166.554 1.742 1.4.554 2.167zM37.147 5.518l2.573.428-1.202-2.315.428-2.574-2.315 1.202-2.574-.427 1.202 2.315-.427 2.573zM68.735 22.132l2.367 1.812.03-2.981 1.812-2.368-2.981-.03-2.368-1.812-.03 2.982-1.812 2.367zM33.676 23.656l1.839.305-.859-1.653.305-1.839-1.653.859-1.839-.305.859 1.653-.305 1.839zM10.695 25.362l.805 1.68.862-1.651 1.68-.804-1.651-.862-.804-1.681-.862 1.652-1.681.804z"/>
<path fill="#F8F9F9" d="M21.862 9.542l5.914-1.585 2.219 8.28-5.914 1.584z"/>
<path fill="#DDF3F3" d="M15.949 11.126l5.913-1.584 2.219 8.28-5.914 1.584z"/>
<path fill="#FFA624" d="M20.68 9.859l1.182-.317 2.219 8.28-1.183.316z"/>
<path fill="#FFBE5D" d="M21.862 9.542l1.183-.317 2.219 8.28-1.183.316z"/>
<path fill="#EE9109" d="M22.581 16.955l1.183-.317.317 1.183-1.183.317zM20.68 9.859l1.182-.317.317 1.183-1.182.317z"/>
<path fill="#C1E9E9" d="M15.949 11.126l4.73-1.267.318 1.183-4.732 1.267zM17.85 18.223l4.731-1.268.317 1.183-4.731 1.268z"/>
<path fill="#DDF3F3" d="M23.045 9.225l4.731-1.268.317 1.183-4.73 1.268zM24.947 16.322l4.73-1.268.318 1.183-4.731 1.267z"/>
<path fill="#FFA624" d="M23.764 16.638l1.183-.316.317 1.182-1.183.317zM21.862 9.542l1.183-.317.317 1.183-1.183.317z"/>
<g>
<path stroke="#FFA624" stroke-width="1.5" d="M21.737 3.348c-.295-1.038-1.009-2.02-2.008-2.204-1-.184-1.69.542-1.495 1.297.195.755.88.984 3.075 1.916.623.264.723.03.428-1.009z"/>
<path stroke="#FFBE5D" stroke-width="1.5" d="M24.307 3.71c.57-.918 1.527-1.664 2.538-1.565 1.012.098 1.475.986 1.08 1.658-.396.671-1.117.703-3.484.994-.672.083-.703-.17-.134-1.088z"/>
<path fill="#EE9109" d="M23.129 2.769c-1.968-.277-1.106 2.141-.317 2.252.79.111 2.285-1.976.317-2.252z"/>
<path fill="#F8F9F9" d="M22.862 4.665l7.276 1.022-.511 3.638-7.276-1.022z"/>
<path fill="#DDF3F3" d="M15.587 3.642l7.275 1.023-.51 3.638-7.276-1.023z"/>
<path fill="#FFBE5D" d="M20.437 4.324l4.85.682-.51 3.638-4.85-.682z"/>
<path fill="#FFA624" d="M20.437 4.324l2.425.341-.51 3.638-2.426-.341z"/>
<path fill="#FFA624" d="M22.862 4.665l2.426.34-.17 1.213-2.426-.34z"/>
<path fill="#EE9109" d="M20.437 4.324l2.425.341-.17 1.213-2.425-.341z"/>
<path fill="#C1E9E9" d="M15.246 6.068l4.85.681-.17 1.213-4.85-.682z"/>
<path fill="#DDF3F3" d="M24.947 7.431l4.85.682-.17 1.212-4.85-.681z"/>
</g>
<g>
<path stroke="#FFA624" stroke-width="1.5" d="M49.696 7.04c-.549-1.24-1.609-2.337-2.885-2.391-1.276-.054-2.007.969-1.632 1.874.374.905 1.266 1.071 4.16 1.847.822.22.905-.09.357-1.33z"/>
<path stroke="#FFBE5D" stroke-width="1.5" d="M52.956 7.04c.548-1.24 1.609-2.337 2.885-2.391 1.276-.054 2.007.969 1.632 1.874-.374.905-1.266 1.071-4.16 1.847-.822.22-.905-.09-.357-1.33z"/>
<path fill="#EE9109" d="M51.326 6.075c-2.497 0-1.002 2.858 0 2.858s2.497-2.858 0-2.858z"/>
<path fill="#F8F9F9" d="M51.326 8.481h9.23v4.616h-9.23z"/>
<path fill="#DDF3F3" d="M42.095 8.481h9.23v4.616h-9.23z"/>
<path fill="#FFBE5D" d="M48.249 8.481h6.154v4.616h-6.154z"/>
<path fill="#FFA624" d="M48.249 8.481h3.077v4.616h-3.077zM51.326 8.481h3.077v1.539h-3.077z"/>
<path fill="#EE9109" d="M48.249 8.481h3.077v1.539h-3.077z"/>
<path fill="#F8F9F9" d="M51.326 13.097h7.692v10.769h-7.692z"/>
<path fill="#DDF3F3" d="M43.634 13.097h7.692v10.769h-7.692z"/>
<path fill="#FFA624" d="M49.787 13.097h1.538v10.769h-1.538z"/>
<path fill="#FFBE5D" d="M51.326 13.097h1.538v10.769h-1.538z"/>
<path fill="#EE9109" d="M49.787 22.327h1.539v1.539h-1.539zM49.787 13.097h1.539v1.538h-1.539z"/>
<path fill="#C1E9E9" d="M43.634 13.097h6.153v1.538h-6.153zM42.095 11.558h6.154v1.539h-6.154zM43.634 22.327h6.153v1.539h-6.153z"/>
<path fill="#DDF3F3" d="M52.864 13.097h6.154v1.538h-6.154zM54.403 11.558h6.154v1.539h-6.154zM52.864 22.327h6.154v1.539h-6.154z"/>
<path fill="#FFA624" d="M51.326 22.327h1.538v1.539h-1.538zM51.326 13.097h1.538v1.538h-1.538z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -2,23 +2,21 @@
mixin featureBullet (text) mixin featureBullet (text)
.row .row
.col-md-2.offset-1 .col-md-2.offset-1
.bubble.mx-auto .d-flex.bubble.justify-content-center.align-items-center
.svg-icon.check(v-html='icons.check') .svg-icon.check.mx-auto(v-html='icons.check')
.col-md-8.align-self-center .col-md-8.align-self-center
p=text p=text
div(v-if='user') div(v-if='user')
b-modal(:hide-footer='true', :hide-header='true', :id='"buy-gems"', size='lg') b-modal#buy-gems(:hide-footer='true', :id='"buy-gems"', size='lg')
.container-fluid.purple-gradient .header-wrap(slot='modal-header')
.gemfall .image-gemfall
.row .row
h2.text-invert.mx-auto {{ $t('support') }} h2.header-invert.mx-auto {{ $t('support') }}
.row .row
.logo.svg-icon.mx-auto(v-html="icons.logo") .logo.svg-icon.mx-auto(v-html="icons.logo")
.container-fluid .d-flex.nav.justify-content-center
.row .nav-item.text-center(@click='selectedPage = "subscribe"', :class="{active: selectedPage === 'subscribe'}") {{ $t('subscribe') }}
.col-6.offset-3.nav .nav-item.text-center(@click='selectedPage = "gems"', :class="{active: selectedPage === 'gems'}") {{ $t('buyGems') }}
.nav-item(@click='selectedPage = "subscribe"', :class="{active: selectedPage === 'subscribe'}") {{ $t('subscribe') }}
.nav-item(@click='selectedPage = "gems"', :class="{active: selectedPage === 'gems'}") {{ $t('buyGems') }}
div(v-show='selectedPage === "gems"') div(v-show='selectedPage === "gems"')
div(v-if='hasSubscription') div(v-if='hasSubscription')
.row.text-center .row.text-center
@@ -36,14 +34,6 @@
+featureBullet("{{ $t('gemBenefit3') }}") +featureBullet("{{ $t('gemBenefit3') }}")
+featureBullet("{{ $t('gemBenefit4') }}") +featureBullet("{{ $t('gemBenefit4') }}")
.card-deck.gem-deck .card-deck.gem-deck
//.card.text-center(:class="{active: gemAmount === 4}")
.card-img-top
.mx-auto(v-html='icons.fourGems', style='"height: 53px; width: 49.5px; margin-top: 2em;"')
.card-body
.gem-count 4
.gem-text {{ $t('gems') }}
.divider
button.btn.btn-primary(@click='gemAmount = 4') {{gemAmount === 4 ? $t('selected') : '$1.00'}}
.card.text-center.col-3(:class="{active: gemAmount === 20 }") .card.text-center.col-3(:class="{active: gemAmount === 20 }")
.card-img-top .card-img-top
.mx-auto(v-html='icons.twentyOneGems', style='"height: 55px; width: 47.5px; margin-top: 1.85em;"') .mx-auto(v-html='icons.twentyOneGems', style='"height: 55px; width: 47.5px; margin-top: 1.85em;"')
@@ -52,22 +42,6 @@
.gem-text {{ $t('gems') }} .gem-text {{ $t('gems') }}
.divider .divider
button.btn.btn-primary(@click='gemAmount === 20 ? gemAmount = 0 : gemAmount = 20') {{gemAmount === 20 ? $t('selected') : '$5.00'}} button.btn.btn-primary(@click='gemAmount === 20 ? gemAmount = 0 : gemAmount = 20') {{gemAmount === 20 ? $t('selected') : '$5.00'}}
//.card.text-center(:class="{active: gemAmount === 42}")
.card-img-top
.mx-auto(v-html='icons.fortyTwoGems', style='"height: 49.5px; width: 51px; margin-top: 1.9em;"')
.card-body
.gem-count 42
.gem-text {{ $t('gems') }}
.divider
button.btn.btn-primary(@click='gemAmount = 42') {{gemAmount === 42 ? $t('selected') : '$10.00'}}
//.card.text-center(:class="{active: gemAmount === 84}")
.card-img-top
.mx-auto(v-html='icons.eightyFourGems', style='"height: 65px; width: 67px; margin-top: 1em;"')
.card-body
.gem-count 84
.gem-text {{ $t('gems') }}
.divider
button.btn.btn-primary(@click='gemAmount = 84') {{gemAmount === 84 ? $t('selected') : '$20.00'}}
.row.text-center .row.text-center
h2.mx-auto.text-payment {{ $t('choosePaymentMethod') }} h2.mx-auto.text-payment {{ $t('choosePaymentMethod') }}
.card-deck .card-deck
@@ -86,11 +60,17 @@
.col-6.offset-3 {{ $t('buyGemsSupportsDevs') }} .col-6.offset-3 {{ $t('buyGemsSupportsDevs') }}
div(v-show='selectedPage === "subscribe"') div(v-show='selectedPage === "subscribe"')
.g1g1-promo.d-flex.justify-content-center.align-items-center
.svg-icon.svg-gifts.left-gift(v-html="icons.gifts")
.text-center
strong.gift-text {{ $t('g1g1Announcement') }}
.gift-text {{ $t('g1g1Details') }}
.svg-icon.svg-gifts.right-gift(v-html="icons.gifts")
div(v-if='hasSubscription') div(v-if='hasSubscription')
.row.text-center .row.text-center
h2.mx-auto.text-leadin {{ $t('subscriptionAlreadySubscribedLeadIn') }} h2.mx-auto.text-leadin {{ $t('subscriptionAlreadySubscribedLeadIn') }}
.row.text-center .row.text-center
.col .col-10.offset-1
p(v-html='$t("subscriptionAlreadySubscribed1")') p(v-html='$t("subscriptionAlreadySubscribed1")')
div(v-if='!hasSubscription') div(v-if='!hasSubscription')
.row.text-center .row.text-center
@@ -170,12 +150,24 @@
</template> </template>
<style lang="scss"> <style lang="scss">
#buy-gems__BV_body_ { #buy-gems .modal-body {
padding: 0; padding: 0;
} }
#buy-gems .modal-content {
border-radius: 8px;
width: 824px;
}
#buy-gems .modal-header {
padding: 0;
border-bottom: 0px;
}
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
a.mx-auto { a.mx-auto {
color: #2995cd; color: #2995cd;
} }
@@ -228,6 +220,11 @@
margin: 1em auto; margin: 1em auto;
} }
.g1g1-promo {
background-color: #34b5c1;
height: 3.75rem;
}
.gem-count { .gem-count {
font-family: Roboto; font-family: Roboto;
font-size: 40px; font-size: 40px;
@@ -242,11 +239,40 @@
margin-bottom: 1em; margin-bottom: 1em;
} }
.gemfall { .gift-text {
color: $white;
font-size: 14px;
}
.image-gemfall {
background: url(~assets/images/gemfall.png) center repeat-y; background: url(~assets/images/gemfall.png) center repeat-y;
height: 14em; height: 14em;
} }
.svg-gifts {
width: 4.6rem;
}
.left-gift {
margin: auto 1rem auto 4.8rem;
}
.right-gift {
margin: auto 4.8rem auto 1rem;
filter: FlipH;
transform: scaleX(-1);
}
.header-wrap {
background-image: linear-gradient(74deg, #4f2a93, #6133b4);
height: 14em;
width: 100%;
color: #4e4a57;
padding: 0;
border-top-left-radius: 7px;
border-top-right-radius: 7px;
}
.logo { .logo {
width: 256px; width: 256px;
height: 56px; height: 56px;
@@ -258,19 +284,21 @@
.nav { .nav {
font-weight: bold; font-weight: bold;
height: 40px; background-color: $gray-600;
} }
.nav-item { .nav-item {
box-sizing: border-box;
display: inline-block; display: inline-block;
font-size: 16px; font-size: 16px;
margin: 0 auto; margin: 0rem;
padding: 1.5em; padding: 1rem;
width: 7.5rem;
} }
.nav-item:hover, .nav-item.active { .nav-item:hover, .nav-item.active {
color: #4f2a93; color: #4f2a93;
border-bottom: 2px solid #4f2a93; border-bottom: 4px solid $purple-300;
cursor: pointer; cursor: pointer;
} }
@@ -286,11 +314,6 @@
padding-top: 1.3em; padding-top: 1.3em;
} }
.purple-gradient {
background-image: linear-gradient(74deg, #4f2a93, #6133b4);
height: 14em;
}
.spacer { .spacer {
height: 4em; height: 4em;
} }
@@ -309,10 +332,11 @@
.svg-icon.check { .svg-icon.check {
color: #bda8ff; color: #bda8ff;
width: 1rem;
} }
.text-invert { .header-invert {
margin: 1.6em; margin: 3rem auto 1.5rem;
color: #FFFFFF; color: #FFFFFF;
font-family: Roboto; font-family: Roboto;
font-weight: normal; font-weight: normal;
@@ -320,9 +344,9 @@
} }
.text-leadin { .text-leadin {
margin: 1.6em; margin: 1rem;
font-weight: normal; font-weight: bold;
color: #4f2a93; color: $purple-200;
} }
.text-outtro { .text-outtro {
@@ -346,6 +370,7 @@
import checkIcon from 'assets/svg/check.svg'; import checkIcon from 'assets/svg/check.svg';
import creditCard from 'assets/svg/credit-card.svg'; import creditCard from 'assets/svg/credit-card.svg';
import gifts from 'assets/svg/gifts.svg';
import heart from 'assets/svg/health.svg'; import heart from 'assets/svg/health.svg';
import logo from 'assets/svg/habitica-logo.svg'; import logo from 'assets/svg/habitica-logo.svg';
@@ -381,6 +406,7 @@
check: checkIcon, check: checkIcon,
creditCard, creditCard,
fourGems, fourGems,
gifts,
heart, heart,
twentyOneGems, twentyOneGems,
fortyTwoGems, fortyTwoGems,

View File

@@ -31,11 +31,17 @@ b-modal#send-gems(:title="title", :hide-footer="true", size='lg', @hide='onHide(
) )
h3.panel-heading {{ $t('subscription') }} h3.panel-heading {{ $t('subscription') }}
.panel-body .panel-body
.row
.col-md-4
.form-group .form-group
.radio(v-for='block in subscriptionBlocks', v-if="block.target !== 'group' && block.canSubscribe === true") .radio(v-for='block in subscriptionBlocks', v-if="block.target !== 'group' && block.canSubscribe === true")
label label
input(type="radio", name="subRadio", :value="block.key", v-model='gift.subscription.key') input(type="radio", name="subRadio", :value="block.key", v-model='gift.subscription.key')
| {{ $t('sendGiftSubscription', {price: block.price, months: block.months}) }} | {{ $t('sendGiftSubscription', {price: block.price, months: block.months}) }}
.col-md-8
h4 {{ $t('winterPromoGiftHeader') }}
p {{ $t('winterPromoGiftDetails1') }}
p {{ $t('winterPromoGiftDetails2') }}
textarea.form-control(rows='3', v-model='gift.message', :placeholder="$t('sendGiftMessagePlaceholder')") textarea.form-control(rows='3', v-model='gift.message', :placeholder="$t('sendGiftMessagePlaceholder')")
//include ../formatting-help //include ../formatting-help

View File

@@ -90,6 +90,10 @@
li(v-once) {{ $t('giftSubscriptionText2') }} li(v-once) {{ $t('giftSubscriptionText2') }}
li(v-once) {{ $t('giftSubscriptionText3') }} li(v-once) {{ $t('giftSubscriptionText3') }}
h4(v-once) {{ $t('giftSubscriptionText4') }} h4(v-once) {{ $t('giftSubscriptionText4') }}
.col-6
h2 {{ $t('winterPromoGiftHeader') }}
p {{ $t('winterPromoGiftDetails1') }}
p {{ $t('winterPromoGiftDetails2') }}
</template> </template>
<style scoped> <style scoped>

View File

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

View File

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

View File

@@ -143,7 +143,9 @@
"dateEndJanuary": "January 31", "dateEndJanuary": "January 31",
"dateEndFebruary": "February 28", "dateEndFebruary": "February 28",
"winterPromoGiftHeader": "GIFT A SUBSCRIPTION AND GET ONE FREE!", "winterPromoGiftHeader": "GIFT A SUBSCRIPTION AND GET ONE FREE!",
"winterPromoGiftDetails1": "Until January 12th only, when you gift somebody a subscription, you get the same subscription for yourself for free!", "winterPromoGiftDetails1": "Until January 15th 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", "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" "discountBundle": "bundle",
"g1g1Announcement": "Gift a Subscription, Get a Subscription Free event going on now!",
"g1g1Details": "Gift a sub to a friend from their profile and youll receive the same sub for free!"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
let api = {}; let api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers // @TODO export this const, cannot export it from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'NEW DISCOUNTED PET QUEST BUNDLE: BIRD BUDDIES!'; const LAST_ANNOUNCEMENT_TITLE = 'GIFT A SUBSCRIPTION, GET A SUBSCRIPTION! AND HABITICA BLOG POSTS';
const worldDmg = { // @TODO const worldDmg = { // @TODO
bailey: false, bailey: false,
}; };
@@ -30,14 +30,26 @@ api.getNews = {
<div class="mr-3 ${baileyClass}"></div> <div class="mr-3 ${baileyClass}"></div>
<div class="media-body"> <div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1> <h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>12/11/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2> <h2>12/18/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div> </div>
</div> </div>
<hr/> <hr/>
<p>If you're looking to add some pets to your Habitica stable, you're in luck! From now until December 31, you can purchase the Bird Buddies Pet Quest Bundle and receive the Rooster, Peacock, and Penguin quests, all for only 7 Gems! That's a discount of 5 Gems from the price of purchasing them separately. Check it out in the <a href='/shops/quests'>Quest Shop</a> today!</p> <div class="promo_g1g1 center-block"></div>
<div class="small">Art by UncommonCriminal, Eevachu, PainterProphet, Lilith of Alfheim, Pfeffernusse, Draayder, Podcod, Fire Fire Fire, Pandoro, RBrinks, EmeraldOx, extrajordanary, melynnrose, Rattify, McCoyly, Breadstrings, and Darkly</div> <h3>Gift a Subscription and Get One Free!</h3>
<div class="small mb-3">Writing by Elizabeth Queenan, Leephon, playgroundgiraffe, and Daniel the Bard</div> <p>In honor of the season of giving--and due to popular demand!--we're bringing back a very special promotion from today through January 15. Now when you gift somebody a subscription, you get the same subscription for yourself for free!</p>
<div class="promo_bird_buddies_bundle center-block"></div> <p>Subscribers get tons of perks every month, including exclusive items, the ability to buy Gems with Gold, a cute exclusive Jackalope Pet, and increased data history. Plus, it helps keep Habitica running :) To gift a subscription to someone, 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>This promotion is only available on <a href='https://habitica.com/'>the web</a> for now, but it will be coming to the Habitica mobile apps very soon.</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. Thanks so much for your support! <3</p>
<div class="small mb-3">by SabreCat and viirus</div>
<div class="promo_todos center-block"></div>
<h3>Wiki Feature: Habitican Shared Lists</h3>
<p>This month's <a href='https://habitica.wordpress.com/2018/12/12/habitican-shared-task-lists/' target='_blank'>featured Wiki article</a> is about Habitican Shared Lists! We hope that it will help you as you explore new possibilities for your task list. 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 class="promo_studying center-block"></div>
<h3>Use Case and Guild Spotlights on Professionalization and Adulting Skills</h3>
<p>We've got new posts on the blog all about ways to use Habitica to help with all those pesky "grown-up" tasks! First, there's a <a href='https://habitica.wordpress.com/2018/12/18/professionalization-and-adulting-skills-guild-spotlight/' target='_blank'>Guild Spotlight</a> that highlights the Guilds that can help you as you explore ways to use Habitica to boost your adulting level. Next, we have a <a href='https://habitica.wordpress.com/2018/12/18/use-case-spotlight-professionalization-and-adulting-skills/' target='_blank'>Use Case Spotlight</a> with adulting and professionalization tips! These suggestions were submitted by Habiticans in the <a href='/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6' target='_blank'>Use Case Spotlights Guild</a>.</p>
<p>Plus, we're collecting user submissions for the next Use Case Spotlight! How do you use Habitica to establish new habits? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!</p>
<div class="small mb-3">by shanaqui</div>
</div> </div>
`, `,
}); });

View File

@@ -147,6 +147,7 @@ async function createSubscription (data) {
txnEmail(data.user, emailType); txnEmail(data.user, emailType);
} }
if (!data.promo) {
analytics.trackPurchase({ analytics.trackPurchase({
uuid: data.user._id, uuid: data.user._id,
groupId, groupId,
@@ -159,14 +160,24 @@ async function createSubscription (data) {
purchaseValue: block.price, purchaseValue: block.price,
headers: data.headers, headers: data.headers,
}); });
}
if (!group) data.user.purchased.txnCount++; if (!group && !data.promo) data.user.purchased.txnCount++;
if (data.gift) { if (data.gift) {
let byUserName = getUserInfo(data.user, ['name']).name; let byUserName = getUserInfo(data.user, ['name']).name;
// generate the message in both languages, so both users can understand it // generate the message in both languages, so both users can understand it
let languages = [data.user.preferences.language, data.gift.member.preferences.language]; let languages = [data.user.preferences.language, data.gift.member.preferences.language];
if (data.promo) {
let senderMsg = shared.i18n.t(`giftedSubscription${data.promo}Promo`, {
username: data.gift.member.profile.name,
monthCount: shared.content.subscriptionBlocks[data.gift.subscription.key].months,
}, languages[0]);
senderMsg = `\`${senderMsg}\``;
data.user.sendMessage(data.gift.member, { senderMsg });
} else {
let senderMsg = shared.i18n.t('giftedSubscriptionFull', { let senderMsg = shared.i18n.t('giftedSubscriptionFull', {
username: data.gift.member.profile.name, username: data.gift.member.profile.name,
sender: byUserName, sender: byUserName,
@@ -187,15 +198,37 @@ async function createSubscription (data) {
} }
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) { if (data.gift.member.preferences.emailNotifications.giftedSubscription !== false) {
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', [ txnEmail(data.gift.member, 'gifted-subscription', [
{name: 'GIFTER', content: byUserName}, {name: 'GIFTER', content: byUserName},
{name: 'X_MONTHS_SUBSCRIPTION', content: months}, {name: 'X_MONTHS_SUBSCRIPTION', content: months},
]); ]);
} }
}
if (data.gift.member._id !== data.user._id) { // If sending to a user other than yourself, don't push notify, and get bonus sub for self per holiday promo
let 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._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) { if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
sendPushNotification(data.gift.member, sendPushNotification(data.gift.member,
{ {

View File

@@ -90,7 +90,7 @@ function sendSubscriptionNotification ({
let text; let text;
let timestamp = new Date(); let timestamp = new Date();
if (recipient.id) { 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) { } else if (groupId) {
text = `${buyer.name} ${buyer.id} ${buyer.email} bought a 1-month recurring group-plan for ${groupId} using ${paymentMethod} on ${timestamp}`; text = `${buyer.name} ${buyer.id} ${buyer.email} bought a 1-month recurring group-plan for ${groupId} using ${paymentMethod} on ${timestamp}`;
} else { } else {