From a0c51ee4ca5bb68ffacb3b35d3d1584030f37a8e Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 18 May 2018 13:42:44 +0200 Subject: [PATCH 1/6] fix(loading bar): always above other elements --- website/client/app.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/client/app.vue b/website/client/app.vue index 3f9031a071..5c87315d0e 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -118,7 +118,7 @@ div /* Push progress bar above modals */ #nprogress .bar { - z-index: 1043 !important; /* Must stay above nav bar */ + z-index: 1090 !important; /* Must stay above nav bar */ } .restingInn { From c39b9dc32048f4907383a948bd39daff936773e7 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 18 May 2018 17:33:38 +0200 Subject: [PATCH 2/6] fix(challenges): format summary with markdown and do not split words, fixes #10371 --- website/client/components/challenges/challengeItem.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/client/components/challenges/challengeItem.vue b/website/client/components/challenges/challengeItem.vue index 0e458fd144..8377561faf 100644 --- a/website/client/components/challenges/challengeItem.vue +++ b/website/client/components/challenges/challengeItem.vue @@ -32,7 +32,7 @@ :member="isMember", v-once ) - .challenge-description {{challenge.summary}} + .challenge-description(v-markdown='challenge.summary') .well-wrapper(v-if="fullLayout") .well div(v-for="task in tasksData", :class="{'muted': task.value === 0}", v-once) @@ -162,7 +162,7 @@ .challenge-description { color: $gray-200; margin: 0 1.5em; - word-break: break-all; + word-break: break-word; } .well { From 644391844056857197fb920b1470c109c070f952 Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 18 May 2018 17:14:39 +0000 Subject: [PATCH 3/6] 4.42.6 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 77cb7b4c00..9c4a039594 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.42.5", + "version": "4.42.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c8c0310352..f58602da38 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.42.5", + "version": "4.42.6", "main": "./website/server/index.js", "dependencies": { "@slack/client": "^3.8.1", From 25d07ac0cecb5849e77fd1e1e2832ed8525c4c9f Mon Sep 17 00:00:00 2001 From: Sabe Jones Date: Fri, 18 May 2018 14:41:15 -0500 Subject: [PATCH 4/6] fix(snackbars): don't timeout server error snacks (#10372) Fixes #10031 and #9249. --- website/client/app.vue | 2 +- website/client/components/snackbars/notification.vue | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/website/client/app.vue b/website/client/app.vue index 02807e356a..8471bb1adb 100644 --- a/website/client/app.vue +++ b/website/client/app.vue @@ -325,7 +325,7 @@ export default { title: 'Habitica', text: errorMessage, type: 'error', - timeout: true, + timeout: false, }); } diff --git a/website/client/components/snackbars/notification.vue b/website/client/components/snackbars/notification.vue index ec544e6c93..256806f6dc 100644 --- a/website/client/components/snackbars/notification.vue +++ b/website/client/components/snackbars/notification.vue @@ -130,7 +130,6 @@ export default { }; }, created () { - // @TODO the notifications always close even if timeout is false let timeout = this.notification.hasOwnProperty('timeout') ? this.notification.timeout : true; if (timeout) { let delay = this.notification.delay || 1500; From 04d7ff13def23a4265075a903f586eb18684a099 Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 19 May 2018 10:31:26 -0500 Subject: [PATCH 5/6] Refactored stripe checkout (#10345) * Refactored stripe checkout * Fixed dependency injection cache --- .../group-plans/group-payments-create.test.js | 3 +- .../libs/payments/stripe/checkout.test.js | 17 +- website/server/libs/payments/stripe.js | 143 ++--------------- website/server/libs/payments/stripe/api.js | 14 ++ .../server/libs/payments/stripe/checkout.js | 146 ++++++++++++++++++ .../server/libs/payments/stripe/constants.js | 15 ++ 6 files changed, 203 insertions(+), 135 deletions(-) create mode 100644 website/server/libs/payments/stripe/api.js create mode 100644 website/server/libs/payments/stripe/checkout.js create mode 100644 website/server/libs/payments/stripe/constants.js diff --git a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js index f25038002c..8ff7e5d7c3 100644 --- a/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js +++ b/test/api/v3/unit/libs/payments/group-plans/group-payments-create.test.js @@ -443,8 +443,7 @@ describe('Purchasing a group plan for group', () => { await api.createSubscription(data); - let updatedUser = await User.findById(recipient._id).exec(); - + const updatedUser = await User.findById(recipient._id).exec(); expect(updatedUser.purchased.plan.extraMonths).to.within(2, 3); }); diff --git a/test/api/v3/unit/libs/payments/stripe/checkout.test.js b/test/api/v3/unit/libs/payments/stripe/checkout.test.js index fca369540c..faba2dd5ef 100644 --- a/test/api/v3/unit/libs/payments/stripe/checkout.test.js +++ b/test/api/v3/unit/libs/payments/stripe/checkout.test.js @@ -37,6 +37,22 @@ describe('checkout', () => { payments.createSubscription.restore(); }); + it('should error if there is no token', async () => { + await expect(stripePayments.checkout({ + user, + gift, + groupId, + email, + headers, + coupon, + }, stripe)) + .to.eventually.be.rejected.and.to.eql({ + httpCode: 400, + message: 'Missing req.body.id', + name: 'BadRequest', + }); + }); + it('should error if gem amount is too low', async () => { let receivingUser = new User(); receivingUser.save(); @@ -64,7 +80,6 @@ describe('checkout', () => { }); }); - it('should error if user cannot get gems', async () => { gift = undefined; sinon.stub(user, 'canGetGems').returnsPromise().resolves(false); diff --git a/website/server/libs/payments/stripe.js b/website/server/libs/payments/stripe.js index c2c218682d..ff7483a6f5 100644 --- a/website/server/libs/payments/stripe.js +++ b/website/server/libs/payments/stripe.js @@ -1,7 +1,5 @@ -import stripeModule from 'stripe'; -import nconf from 'nconf'; -import cc from 'coupon-code'; import moment from 'moment'; + import logger from '../logger'; import { BadRequest, @@ -10,38 +8,22 @@ import { } from '../errors'; import payments from './payments'; import { model as User } from '../../models/user'; -import { model as Coupon } from '../../models/coupon'; import { model as Group, basicFields as basicGroupFields, } from '../../models/group'; import shared from '../../../common'; +import stripeConstants from './stripe/constants'; +import { checkout } from './stripe/checkout'; +import { getStripeApi, setStripeApi } from './stripe/api'; -let stripe = stripeModule(nconf.get('STRIPE_API_KEY')); const i18n = shared.i18n; let api = {}; -api.constants = { - // CURRENCY_CODE: 'USD', - // SELLER_NOTE: 'Habitica Payment', - // SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', - // SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', - // STORE_NAME: 'Habitica', - // - // GIFT_TYPE_GEMS: 'gems', - // GIFT_TYPE_SUBSCRIPTION: 'subscription', - // - // METHOD_BUY_GEMS: 'buyGems', - // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', - PAYMENT_METHOD: 'Stripe', - // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', -}; - -api.setStripeApi = function setStripeApi (stripeInc) { - stripe = stripeInc; -}; +api.constants = Object.assign({}, stripeConstants); +api.setStripeApi = setStripeApi; /** * Allows for purchasing a user subscription, group subscription or gems with Stripe @@ -56,110 +38,7 @@ api.setStripeApi = function setStripeApi (stripeInc) { * @param options.headers The request headers to store on analytics * @return undefined */ -api.checkout = async function checkout (options, stripeInc) { - let { - token, - user, - gift, - sub, - groupId, - email, - headers, - coupon, - } = options; - let response; - let subscriptionId; - - // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; - if (stripeInc) stripeApi = stripeInc; - - if (!token) throw new BadRequest('Missing req.body.id'); - - if (gift) { - const member = await User.findById(gift.uuid).exec(); - gift.member = member; - } - - if (sub) { - if (sub.discount) { - if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); - coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); - if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); - } - - let customerObject = { - email, - metadata: { uuid: user._id }, - card: token, - plan: sub.key, - }; - - if (groupId) { - customerObject.quantity = sub.quantity; - const groupFields = basicGroupFields.concat(' purchased'); - const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); - const membersCount = await group.getMemberCount(); - customerObject.quantity = membersCount + sub.quantity - 1; - } - - response = await stripeApi.customers.create(customerObject); - - if (groupId) subscriptionId = response.subscriptions.data[0].id; - } else { - let amount = 500; // $5 - - if (gift) { - if (gift.type === 'subscription') { - amount = `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; - } else { - if (gift.gems.amount <= 0) { - throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); - } - amount = `${gift.gems.amount / 4 * 100}`; - } - } - - if (!gift || gift.type === 'gems') { - const receiver = gift ? gift.member : user; - const receiverCanGetGems = await receiver.canGetGems(); - if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); - } - - response = await stripeApi.charges.create({ - amount, - currency: 'usd', - card: token, - }); - } - - if (sub) { - await payments.createSubscription({ - user, - customerId: response.id, - paymentMethod: this.constants.PAYMENT_METHOD, - sub, - headers, - groupId, - subscriptionId, - }); - } else { - let method = 'buyGems'; - let data = { - user, - customerId: response.id, - paymentMethod: this.constants.PAYMENT_METHOD, - gift, - }; - - if (gift) { - if (gift.type === 'subscription') method = 'createSubscription'; - data.paymentMethod = 'Gift'; - } - - await payments[method](data); - } -}; +api.checkout = checkout; /** * Edits a subscription created by Stripe @@ -176,7 +55,7 @@ api.editSubscription = async function editSubscription (options, stripeInc) { let customerId; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; if (groupId) { @@ -220,7 +99,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc) let customerId; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; if (groupId) { @@ -271,7 +150,7 @@ api.cancelSubscription = async function cancelSubscription (options, stripeInc) }; api.chargeForAdditionalGroupMember = async function chargeForAdditionalGroupMember (group) { - let stripeApi = stripe; + let stripeApi = getStripeApi(); let plan = shared.content.subscriptionBlocks.group_monthly; await stripeApi.subscriptions.update( @@ -298,7 +177,7 @@ api.handleWebhooks = async function handleWebhooks (options, stripeInc) { let {requestBody} = options; // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? - let stripeApi = stripe; + let stripeApi = getStripeApi(); if (stripeInc) stripeApi = stripeInc; // Verify the event by fetching it from Stripe diff --git a/website/server/libs/payments/stripe/api.js b/website/server/libs/payments/stripe/api.js new file mode 100644 index 0000000000..43a99402b1 --- /dev/null +++ b/website/server/libs/payments/stripe/api.js @@ -0,0 +1,14 @@ +import stripeModule from 'stripe'; +import nconf from 'nconf'; + +let stripe = stripeModule(nconf.get('STRIPE_API_KEY')); + +function setStripeApi (stripeInc) { + stripe = stripeInc; +} + +function getStripeApi () { + return stripe; +} + +module.exports = { getStripeApi, setStripeApi }; diff --git a/website/server/libs/payments/stripe/checkout.js b/website/server/libs/payments/stripe/checkout.js new file mode 100644 index 0000000000..8dafa3536c --- /dev/null +++ b/website/server/libs/payments/stripe/checkout.js @@ -0,0 +1,146 @@ +import cc from 'coupon-code'; + +import { getStripeApi } from './api'; +import { model as User } from '../../../models/user'; +import { model as Coupon } from '../../../models/coupon'; +import { + model as Group, + basicFields as basicGroupFields, +} from '../../../models/group'; +import shared from '../../../../common'; +import { + BadRequest, + NotAuthorized, +} from '../../errors'; +import payments from './../payments'; +import stripeConstants from './constants'; + +function getGiftAmount (gift) { + if (gift.type === 'subscription') { + return `${shared.content.subscriptionBlocks[gift.subscription.key].price * 100}`; + } + + if (gift.gems.amount <= 0) { + throw new BadRequest(shared.i18n.t('badAmountOfGemsToPurchase')); + } + + return `${gift.gems.amount / 4 * 100}`; +} + +async function buyGems (gift, user, token, stripeApi) { + let amount = 500; // $5 + + if (gift) amount = getGiftAmount(gift); + + if (!gift || gift.type === 'gems') { + const receiver = gift ? gift.member : user; + const receiverCanGetGems = await receiver.canGetGems(); + if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', receiver.preferences.language)); + } + + const response = await stripeApi.charges.create({ + amount, + currency: 'usd', + card: token, + }); + + return response; +} + +async function buySubscription (sub, coupon, email, user, token, groupId, stripeApi) { + if (sub.discount) { + if (!coupon) throw new BadRequest(shared.i18n.t('couponCodeRequired')); + coupon = await Coupon.findOne({_id: cc.validate(coupon), event: sub.key}).exec(); + if (!coupon) throw new BadRequest(shared.i18n.t('invalidCoupon')); + } + + let customerObject = { + email, + metadata: { uuid: user._id }, + card: token, + plan: sub.key, + }; + + if (groupId) { + customerObject.quantity = sub.quantity; + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + const membersCount = await group.getMemberCount(); + customerObject.quantity = membersCount + sub.quantity - 1; + } + + const response = await stripeApi.customers.create(customerObject); + + let subscriptionId; + if (groupId) subscriptionId = response.subscriptions.data[0].id; + + return { subResponse: response, subId: subscriptionId }; +} + +async function applyGemPayment (user, response, gift) { + let method = 'buyGems'; + const data = { + user, + customerId: response.id, + paymentMethod: stripeConstants.PAYMENT_METHOD, + gift, + }; + + if (gift) { + if (gift.type === 'subscription') method = 'createSubscription'; + data.paymentMethod = 'Gift'; + } + + await payments[method](data); +} + +async function checkout (options, stripeInc) { + let { + token, + user, + gift, + sub, + groupId, + email, + headers, + coupon, + } = options; + let response; + let subscriptionId; + + // @TODO: We need to mock this, but curently we don't have correct Dependency Injection. And the Stripe Api doesn't seem to be a singleton? + let stripeApi = getStripeApi(); + if (stripeInc) stripeApi = stripeInc; + + if (!token) throw new BadRequest('Missing req.body.id'); + + if (gift) { + const member = await User.findById(gift.uuid).exec(); + gift.member = member; + } + + if (sub) { + const { subId, subResponse } = await buySubscription(sub, coupon, email, user, token, groupId, stripeApi); + subscriptionId = subId; + response = subResponse; + } else { + response = await buyGems(gift, user, token, stripeApi); + } + + if (sub) { + await payments.createSubscription({ + user, + customerId: response.id, + paymentMethod: this.constants.PAYMENT_METHOD, + sub, + headers, + groupId, + subscriptionId, + }); + return; + } + + await applyGemPayment(user, response, gift); +} + +module.exports = { checkout }; diff --git a/website/server/libs/payments/stripe/constants.js b/website/server/libs/payments/stripe/constants.js new file mode 100644 index 0000000000..00bddb1e87 --- /dev/null +++ b/website/server/libs/payments/stripe/constants.js @@ -0,0 +1,15 @@ +module.exports = { + // CURRENCY_CODE: 'USD', + // SELLER_NOTE: 'Habitica Payment', + // SELLER_NOTE_SUBSCRIPTION: 'Habitica Subscription', + // SELLER_NOTE_ATHORIZATION_SUBSCRIPTION: 'Habitica Subscription Payment', + // STORE_NAME: 'Habitica', + // + // GIFT_TYPE_GEMS: 'gems', + // GIFT_TYPE_SUBSCRIPTION: 'subscription', + // + // METHOD_BUY_GEMS: 'buyGems', + // METHOD_CREATE_SUBSCRIPTION: 'createSubscription', + PAYMENT_METHOD: 'Stripe', + // PAYMENT_METHOD_GIFT: 'Amazon Payments (Gift)', +}; From 37ab257f5b7c8d0d8b4a0785704bb526f89934ee Mon Sep 17 00:00:00 2001 From: Keith Holliday Date: Sat, 19 May 2018 11:43:19 -0500 Subject: [PATCH 6/6] Added responsive fixes to home page (#10381) --- website/client/components/static/home.vue | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/website/client/components/static/home.vue b/website/client/components/static/home.vue index d516883663..6a2903df49 100644 --- a/website/client/components/static/home.vue +++ b/website/client/components/static/home.vue @@ -7,11 +7,11 @@ #intro-signup.purple-1 .container .row - .col-12.col-sm-6.col-md-6.col-lg-6 + .col-12.col-md-6.col-lg-6 img(src='~assets/images/home/home-main@3x.png', width='357px') h1 {{$t('motivateYourself')}} p.section-main {{$t('timeToGetThingsDone', {userCountInMillions})}} - .col-12.col-sm-6.col-md-6.col-lg-6 + .col-12.col-md-6.col-lg-6 h3.text-center {{$t('singUpForFree')}} div.text-center button.social-button(@click='socialAuth("facebook")') @@ -42,15 +42,15 @@ h2 {{$t('gamifyYourLife')}} p.section-main {{$t('aboutHabitica')}} .row - .col-12.col-sm-4 + .col-12.col-md-4 img.track-habits(src='~assets/images/home/track-habits@3x.png', width='354px', height='228px') strong {{$t('trackYourGoals')}} p {{$t('trackYourGoalsDesc')}} - .col-12.col-sm-4 + .col-12.col-md-4 img(src='~assets/images/home/earn-rewards@3x.png', width='316px', height='244px') strong {{$t('earnRewards')}} p {{$t('earnRewardsDesc')}} - .col-12.col-sm-4 + .col-12.col-md-4 img(src='~assets/images/home/battle-monsters@3x.png', width='303px', height='244px') strong {{$t('battleMonsters')}} p {{$t('battleMonstersDesc')}} @@ -83,9 +83,9 @@ #level-up-anywhere.purple-3 .container .row - .col-12.col-sm-6.col-md-6.col-lg-6 + .col-12.col-md-6.col-lg-6 .iphones - .col-12.col-sm-6.col-md-6.col-lg-6.text-column + .col-12.col-md-6.col-lg-6.text-column h2 {{ $t('levelUpAnywhere') }} p {{ $t('levelUpAnywhereDesc') }} a.app.svg-icon(v-html='icons.googlePlay', href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', target='_blank') @@ -345,6 +345,9 @@ text-align: center; img { + max-width: 100%; + display: block; + margin: 0 auto; margin-top: 1em; margin-bottom: 1.5em; } @@ -387,6 +390,8 @@ .iphones { width: 436px; height: 520px; + max-width: 100%; + background-repeat: no-repeat; background-size: 100%; background-image: url('~assets/images/home/mobile-preview@3x.png'); }