Fall Festival Gem Promo (#138)
* content: add gems blocks * gemsBlocks: include ios and android identifiers * wip: promo code * split common constants into multiple files * add second promo part * geCurrentEvent, refactor promo * fix lint * fix exports, use world state api * start adding world state tests * remove console.log * use gems block for purchases * remove comments * fix most unit tests * restore comment * fix lint * prevent apple/google gift tests from breaking other tests when stub is not reset * fix unit tests, clarify tests names * iap: use gift object when gifting gems * allow gift object with less data * fix iap tests, remove findById stubs * iap: require less data from the mobile apps * apply discounts * add missing worldState file * fix lint * add test event * start removing 20 gems option for web * start adding support for all gems packages on web * fix unit tests for apple, stripe and google * amazon: support all gems blocks * paypal: support all gems blocks * fix payments unit tests, add tests for getGemsBlock * web: add gems plans with discounts, update stripe * fix amazon and paypal clients, payments success modals * amazon pay: disabled state * update icons, start abstracting payments buttons * begin redesign * redesign gems modal * fix buttons * fix hover color for gems modal close icon * add key to world state current event * extend test event length * implement gems modals designs * early test fall2020 * fix header banner position * add missing files * use iso 8601 for dates, minor ui fixes * fix time zones * events: fix ISO8601 format * fix css indentation * start abstracting banners * refactor payments buttons * test spooky, fix group plans box * implement gems promo banners, refactor banners, fixes * fix lint * fix dates * remove unused i18n strings * fix stripe integration test * fix world state integration tests * the current active event * add missing unit tests * add storybook story for payments buttons component * fix typo * fix(stripe): correct label when gifting subscriptions
@@ -2,13 +2,14 @@ import { model as User } from '../../../../../../website/server/models/user';
|
||||
import amzLib from '../../../../../../website/server/libs/payments/amazon';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('Amazon Payments - Checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user; let orderReferenceId; let
|
||||
headers;
|
||||
headers; const gemsBlockKey = '21gems'; const gemsBlock = common.content.gems[gemsBlockKey];
|
||||
let setOrderReferenceDetailsSpy;
|
||||
let confirmOrderReferenceSpy;
|
||||
let authorizeSpy;
|
||||
@@ -16,7 +17,7 @@ describe('Amazon Payments - Checkout', () => {
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let amount = 5;
|
||||
let amount = gemsBlock.price / 100;
|
||||
|
||||
function expectOrderReferenceSpy () {
|
||||
expect(setOrderReferenceDetailsSpy).to.be.calledOnce;
|
||||
@@ -107,13 +108,20 @@ describe('Amazon Payments - Checkout', () => {
|
||||
paymentMethod,
|
||||
headers,
|
||||
};
|
||||
if (gift) expectedArgs.gift = gift;
|
||||
if (gift) {
|
||||
expectedArgs.gift = gift;
|
||||
expectedArgs.gemsBlock = undefined;
|
||||
} else {
|
||||
expectedArgs.gemsBlock = gemsBlock;
|
||||
}
|
||||
expect(paymentBuyGemsStub).to.be.calledWith(expectedArgs);
|
||||
}
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').resolves(true);
|
||||
await amzLib.checkout({ user, orderReferenceId, headers });
|
||||
await amzLib.checkout({
|
||||
user, orderReferenceId, headers, gemsBlock: gemsBlockKey,
|
||||
});
|
||||
|
||||
expectBuyGemsStub(amzLib.constants.PAYMENT_METHOD);
|
||||
expectAmazonStubs();
|
||||
@@ -144,7 +152,9 @@ describe('Amazon Payments - Checkout', () => {
|
||||
|
||||
it('should error if user cannot get gems gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').resolves(false);
|
||||
await expect(amzLib.checkout({ user, orderReferenceId, headers }))
|
||||
await expect(amzLib.checkout({
|
||||
user, orderReferenceId, headers, gemsBlock: gemsBlockKey,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
@@ -153,6 +163,17 @@ describe('Amazon Payments - Checkout', () => {
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('should error if gems block is not valid', async () => {
|
||||
await expect(amzLib.checkout({
|
||||
user, orderReferenceId, headers, gemsBlock: 'invalid',
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: apiError('invalidGemsBlock'),
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should gift gems', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
@@ -195,6 +216,7 @@ describe('Amazon Payments - Checkout', () => {
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ import applePayments from '../../../../../website/server/libs/payments/apple';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import { mockFindById, restoreFindById } from '../../../../helpers/mongoose.helper';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
@@ -84,7 +83,7 @@ describe('Apple Payments', () => {
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
it('errors if amount does not exist', async () => {
|
||||
it('errors if gemsBlock does not exist', async () => {
|
||||
sinon.stub(user, 'canGetGems').resolves(true);
|
||||
iapGetPurchaseDataStub.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
@@ -106,23 +105,23 @@ describe('Apple Payments', () => {
|
||||
const gemsCanPurchase = [
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.4gems',
|
||||
amount: 1,
|
||||
gemsBlock: '4gems',
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.20gems',
|
||||
amount: 5.25,
|
||||
gemsBlock: '21gems',
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
amount: 5.25,
|
||||
gemsBlock: '21gems',
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.42gems',
|
||||
amount: 10.5,
|
||||
gemsBlock: '42gems',
|
||||
},
|
||||
{
|
||||
productId: 'com.habitrpg.ios.Habitica.84gems',
|
||||
amount: 21,
|
||||
gemsBlock: '84gems',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -149,8 +148,9 @@ describe('Apple Payments', () => {
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
amount: gemTest.amount,
|
||||
gemsBlock: common.content.gems[gemTest.gemsBlock],
|
||||
headers,
|
||||
gift: undefined,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
@@ -161,8 +161,6 @@ describe('Apple Payments', () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
|
||||
mockFindById(receivingUser);
|
||||
|
||||
iapGetPurchaseDataStub.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
@@ -184,12 +182,17 @@ describe('Apple Payments', () => {
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user: receivingUser,
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
amount: gemsCanPurchase[0].amount,
|
||||
headers,
|
||||
gift: {
|
||||
type: 'gems',
|
||||
gems: { amount: 4 },
|
||||
member: sinon.match({ _id: receivingUser._id }),
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
gemsBlock: common.content.gems['4gems'],
|
||||
});
|
||||
restoreFindById();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
14
test/api/unit/libs/payments/gems.test.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import common from '../../../../../website/common';
|
||||
import { getGemsBlock } from '../../../../../website/server/libs/payments/gems';
|
||||
|
||||
describe('payments/gems', () => {
|
||||
describe('#getGemsBlock', () => {
|
||||
it('throws an error if the gem block key is invalid', () => {
|
||||
expect(() => getGemsBlock('invalid')).to.throw;
|
||||
});
|
||||
|
||||
it('returns the gem block for the given key', () => {
|
||||
expect(getGemsBlock('21gems')).to.equal(common.content.gems['21gems']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import googlePayments from '../../../../../website/server/libs/payments/google';
|
||||
import iap from '../../../../../website/server/libs/inAppPurchases';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import common from '../../../../../website/common';
|
||||
import { mockFindById, restoreFindById } from '../../../../helpers/mongoose.helper';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
@@ -14,7 +13,7 @@ describe('Google Payments', () => {
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers;
|
||||
headers; const gemsBlock = common.content.gems['21gems'];
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuyGemsStub;
|
||||
|
||||
@@ -103,8 +102,9 @@ describe('Google Payments', () => {
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
amount: 5.25,
|
||||
gemsBlock,
|
||||
headers,
|
||||
gift: undefined,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
@@ -114,8 +114,6 @@ describe('Google Payments', () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
|
||||
mockFindById(receivingUser);
|
||||
|
||||
const gift = { uuid: receivingUser._id };
|
||||
await googlePayments.verifyGemPurchase({
|
||||
user, gift, receipt, signature, headers,
|
||||
@@ -132,12 +130,17 @@ describe('Google Payments', () => {
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
user: receivingUser,
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
amount: 5.25,
|
||||
gemsBlock,
|
||||
headers,
|
||||
gift: {
|
||||
type: 'gems',
|
||||
gems: { amount: 21 },
|
||||
member: sinon.match({ _id: receivingUser._id }),
|
||||
uuid: receivingUser._id,
|
||||
},
|
||||
});
|
||||
restoreFindById();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import common from '../../../../../website/common';
|
||||
import api from '../../../../../website/server/libs/payments/payments';
|
||||
import * as analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import * as notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
@@ -9,6 +10,7 @@ import { translate as t } from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
generateGroup,
|
||||
} from '../../../../helpers/api-unit.helper';
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user; let group; let data; let
|
||||
@@ -555,6 +557,7 @@ describe('payments/index', () => {
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
user,
|
||||
gemsBlock: common.content.gems['21gems'],
|
||||
paymentMethod: 'payment',
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
@@ -564,22 +567,6 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Self Purchase', () => {
|
||||
it('amount property defaults to 5', async () => {
|
||||
expect(user.balance).to.eql(0);
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(user.balance).to.eql(5);
|
||||
});
|
||||
|
||||
it('can set amount that is purchased', async () => {
|
||||
data.amount = 13;
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
expect(user.balance).to.eql(13);
|
||||
});
|
||||
|
||||
it('sends a donation email', async () => {
|
||||
await api.buyGems(data);
|
||||
|
||||
@@ -588,6 +575,51 @@ describe('payments/index', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
});
|
||||
|
||||
it('does not apply a discount', async () => {
|
||||
const balanceBefore = user.balance;
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
const balanceAfter = user.balance;
|
||||
const balanceDiff = balanceAfter - balanceBefore;
|
||||
|
||||
expect(balanceDiff * 4).to.eql(21);
|
||||
});
|
||||
});
|
||||
|
||||
context('Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns({
|
||||
...common.content.events.fall2020,
|
||||
event: 'fall2020',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
});
|
||||
|
||||
it('applies a discount', async () => {
|
||||
const balanceBefore = user.balance;
|
||||
|
||||
await api.buyGems(data);
|
||||
|
||||
const balanceAfter = user.balance;
|
||||
const balanceDiff = balanceAfter - balanceBefore;
|
||||
|
||||
expect(balanceDiff * 4).to.eql(30);
|
||||
});
|
||||
});
|
||||
|
||||
context('Gift', () => {
|
||||
let recipient;
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
describe('checkout success', () => {
|
||||
describe('paypal - checkout success', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user; let gift; let customerId; let
|
||||
paymentId;
|
||||
const gemsBlockKey = '21gems'; const gemsBlock = common.content.gems[gemsBlockKey];
|
||||
let paypalPaymentExecuteStub; let paymentBuyGemsStub; let
|
||||
paymentsCreateSubscritionStub;
|
||||
|
||||
@@ -28,7 +30,7 @@ describe('checkout success', () => {
|
||||
|
||||
it('purchases gems', async () => {
|
||||
await paypalPayments.checkoutSuccess({
|
||||
user, gift, paymentId, customerId,
|
||||
user, gift, paymentId, customerId, gemsBlock: gemsBlockKey,
|
||||
});
|
||||
|
||||
expect(paypalPaymentExecuteStub).to.be.calledOnce;
|
||||
@@ -38,6 +40,7 @@ describe('checkout success', () => {
|
||||
user,
|
||||
customerId,
|
||||
paymentMethod: 'Paypal',
|
||||
gemsBlock,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ import nconf from 'nconf';
|
||||
import paypalPayments from '../../../../../../website/server/libs/payments/paypal';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
const { i18n } = common;
|
||||
|
||||
describe('checkout', () => {
|
||||
describe('paypal - checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const gemsBlockKey = '21gems';
|
||||
let paypalPaymentCreateStub;
|
||||
let approvalHerf;
|
||||
|
||||
@@ -53,10 +55,10 @@ describe('checkout', () => {
|
||||
});
|
||||
|
||||
it('creates a link for gem purchases', async () => {
|
||||
const link = await paypalPayments.checkout({ user: new User() });
|
||||
const link = await paypalPayments.checkout({ user: new User(), gemsBlock: gemsBlockKey });
|
||||
|
||||
expect(paypalPaymentCreateStub).to.be.calledOnce;
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 5.00));
|
||||
expect(paypalPaymentCreateStub).to.be.calledWith(getPaypalCreateOptions('Habitica Gems', 4.99));
|
||||
expect(link).to.eql(approvalHerf);
|
||||
});
|
||||
|
||||
@@ -83,13 +85,25 @@ describe('checkout', () => {
|
||||
const user = new User();
|
||||
sinon.stub(user, 'canGetGems').resolves(false);
|
||||
|
||||
await expect(paypalPayments.checkout({ user })).to.eventually.be.rejected.and.to.eql({
|
||||
await expect(paypalPayments.checkout({ user, gemsBlock: gemsBlockKey }))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
message: i18n.t('groupPolicyCannotGetGems'),
|
||||
name: 'NotAuthorized',
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the gems block is not valid', async () => {
|
||||
const user = new User();
|
||||
|
||||
await expect(paypalPayments.checkout({ user, gemsBlock: 'invalid' }))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: apiError('invalidGemsBlock'),
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a link for gifting gems', async () => {
|
||||
const receivingUser = new User();
|
||||
await receivingUser.save();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '../../../../../helpers/api-unit.helper';
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
|
||||
describe('ipn', () => {
|
||||
describe('paypal - ipn', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user; let group; let txn_type; let userPaymentId; let
|
||||
groupPaymentId;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { createNonLeaderGroupMember } from '../paymentHelpers';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('subscribeCancel', () => {
|
||||
describe('paypal - subscribeCancel', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user; let group; let groupId; let customerId; let groupCustomerId; let
|
||||
nextBillingDate;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { model as User } from '../../../../../../website/server/models/user';
|
||||
import common from '../../../../../../website/common';
|
||||
|
||||
describe('subscribeSuccess', () => {
|
||||
describe('paypal - subscribeSuccess', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let user; let group; let block; let groupId; let token; let headers; let
|
||||
customerId;
|
||||
|
||||
@@ -8,7 +8,7 @@ import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('subscribe', () => {
|
||||
describe('paypal - subscribe', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
let coupon; let sub; let
|
||||
approvalHerf;
|
||||
|
||||
@@ -4,7 +4,7 @@ import api from '../../../../../../website/server/libs/payments/payments';
|
||||
|
||||
const groupPlanId = api.constants.GROUP_PLAN_CUSTOMER_ID;
|
||||
|
||||
describe('#calculateSubscriptionTerminationDate', () => {
|
||||
describe('stripe - #calculateSubscriptionTerminationDate', () => {
|
||||
let plan;
|
||||
let nextBill;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('cancel subscription', () => {
|
||||
describe('stripe - cancel subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user; let groupId; let
|
||||
|
||||
@@ -12,7 +12,7 @@ import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('checkout with subscription', () => {
|
||||
describe('stripe - checkout with subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user; let group; let data; let gift; let sub;
|
||||
|
||||
@@ -4,16 +4,17 @@ import { model as User } from '../../../../../../website/server/models/user';
|
||||
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
|
||||
import payments from '../../../../../../website/server/libs/payments/payments';
|
||||
import common from '../../../../../../website/common';
|
||||
import apiError from '../../../../../../website/server/libs/apiError';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('checkout', () => {
|
||||
describe('stripe - checkout', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let stripeChargeStub; let paymentBuyGemsStub; let
|
||||
paymentCreateSubscritionStub;
|
||||
let user; let gift; let groupId; let email; let headers; let coupon; let customerIdResponse; let
|
||||
token;
|
||||
token; const gemsBlockKey = '21gems'; const gemsBlock = common.content.gems[gemsBlockKey];
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
@@ -89,6 +90,7 @@ describe('checkout', () => {
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gemsBlock: gemsBlockKey,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
@@ -101,6 +103,25 @@ describe('checkout', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should error if the gems block is invalid', async () => {
|
||||
gift = undefined;
|
||||
|
||||
await expect(stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gemsBlock: 'invalid',
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
headers,
|
||||
coupon,
|
||||
}, stripe)).to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
message: apiError('invalidGemsBlock'),
|
||||
name: 'BadRequest',
|
||||
});
|
||||
});
|
||||
|
||||
it('should purchase gems', async () => {
|
||||
gift = undefined;
|
||||
sinon.stub(user, 'canGetGems').resolves(true);
|
||||
@@ -108,6 +129,7 @@ describe('checkout', () => {
|
||||
await stripePayments.checkout({
|
||||
token,
|
||||
user,
|
||||
gemsBlock: gemsBlockKey,
|
||||
gift,
|
||||
groupId,
|
||||
email,
|
||||
@@ -117,7 +139,7 @@ describe('checkout', () => {
|
||||
|
||||
expect(stripeChargeStub).to.be.calledOnce;
|
||||
expect(stripeChargeStub).to.be.calledWith({
|
||||
amount: 500,
|
||||
amount: 499,
|
||||
currency: 'usd',
|
||||
card: token,
|
||||
});
|
||||
@@ -128,6 +150,7 @@ describe('checkout', () => {
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Stripe',
|
||||
gift,
|
||||
gemsBlock,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
@@ -167,6 +190,7 @@ describe('checkout', () => {
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,6 +229,7 @@ describe('checkout', () => {
|
||||
customerId: customerIdResponse,
|
||||
paymentMethod: 'Gift',
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import common from '../../../../../../website/common';
|
||||
|
||||
const { i18n } = common;
|
||||
|
||||
describe('edit subscription', () => {
|
||||
describe('stripe - edit subscription', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
const stripe = stripeModule('test');
|
||||
let user; let groupId; let group; let
|
||||
|
||||
@@ -14,7 +14,10 @@ describe('payments - stripe - #checkout', () => {
|
||||
});
|
||||
|
||||
it('verifies credentials', async () => {
|
||||
await expect(user.post(endpoint, { id: 123 })).to.eventually.be.rejected.and.include({
|
||||
await expect(user.post(
|
||||
`${endpoint}?gemsBlock=4gems`,
|
||||
{ id: 123 },
|
||||
)).to.eventually.be.rejected.and.include({
|
||||
code: 401,
|
||||
error: 'Error',
|
||||
// message: 'Invalid API Key provided: aaaabbbb********************1111',
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
requester,
|
||||
resetHabiticaDB,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import common from '../../../../../website/common';
|
||||
|
||||
describe('GET /world-state', () => {
|
||||
before(async () => {
|
||||
@@ -39,4 +41,41 @@ describe('GET /world-state', () => {
|
||||
expect(res).to.have.nested.property('npcImageSuffix');
|
||||
expect(res.npcImageSuffix).to.be.a('string');
|
||||
});
|
||||
|
||||
context('no current event', () => {
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
});
|
||||
|
||||
it('returns null for the current event when there is none active', async () => {
|
||||
const res = await requester().get('/world-state');
|
||||
|
||||
expect(res.currentEvent).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
context('no current event', () => {
|
||||
const evt = {
|
||||
...common.content.events.fall2020,
|
||||
event: 'fall2020',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
sinon.stub(worldState, 'getCurrentEvent').returns(evt);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
worldState.getCurrentEvent.restore();
|
||||
});
|
||||
|
||||
it('returns the current event when there is an active one', async () => {
|
||||
const res = await requester().get('/world-state');
|
||||
|
||||
expect(res.currentEvent).to.eql(evt);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
id="app"
|
||||
:class="{
|
||||
'casting-spell': castingSpell,
|
||||
'resting': showRestingBanner
|
||||
}"
|
||||
>
|
||||
<!-- <banned-account-modal /> -->
|
||||
@@ -38,31 +37,8 @@
|
||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||
<template v-else>
|
||||
<template v-if="isUserLoaded">
|
||||
<div
|
||||
v-show="showRestingBanner"
|
||||
ref="restingBanner"
|
||||
class="resting-banner"
|
||||
>
|
||||
<span class="content">
|
||||
<span class="label d-inline d-sm-none">{{ $t('innCheckOutBannerShort') }}</span>
|
||||
<span class="label d-none d-sm-inline">{{ $t('innCheckOutBanner') }}</span>
|
||||
<span class="separator">|</span>
|
||||
<span
|
||||
class="resume"
|
||||
@click="resumeDamage()"
|
||||
>{{ $t('resumeDamage') }}</span>
|
||||
</span>
|
||||
<div
|
||||
class="closepadding"
|
||||
@click="hideBanner()"
|
||||
>
|
||||
<span
|
||||
class="svg-icon inline icon-10"
|
||||
aria-hidden="true"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<damage-paused-banner />
|
||||
<gems-promo-banner />
|
||||
<notifications-display />
|
||||
<app-menu />
|
||||
<div
|
||||
@@ -99,20 +75,11 @@
|
||||
|
||||
<style lang='scss' scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/variables.scss';
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.resting {
|
||||
--banner-resting-height: #{$restingToolbarHeight};
|
||||
}
|
||||
|
||||
&.giftingBanner {
|
||||
--banner-gifting-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#loading-screen-inapp {
|
||||
@@ -143,15 +110,6 @@
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.closepadding {
|
||||
margin: 11px 24px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
right: 0;
|
||||
top: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
@@ -172,51 +130,11 @@
|
||||
margin-top: .5em;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.resting-banner {
|
||||
width: 100%;
|
||||
height: $restingToolbarHeight;
|
||||
background-color: $blue-10;
|
||||
top: 0;
|
||||
z-index: 1300;
|
||||
display: flex;
|
||||
|
||||
.content {
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
padding: 8px 38px 8px 8px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.content {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $blue-100;
|
||||
margin: 0px 15px;
|
||||
}
|
||||
|
||||
.resume {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
white-space:nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.closepadding span svg path {
|
||||
stroke: #FFF;
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
opacity: .9 !important;
|
||||
background-color: $purple-100 !important;
|
||||
@@ -234,6 +152,8 @@ import { loadProgressBar } from 'axios-progress-bar';
|
||||
|
||||
import AppMenu from './components/header/menu';
|
||||
import AppHeader from './components/header/index';
|
||||
import DamagePausedBanner from './components/header/banners/damagePaused';
|
||||
import GemsPromoBanner from './components/header/banners/gemsPromo';
|
||||
import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
@@ -255,8 +175,6 @@ import {
|
||||
removeLocalSetting,
|
||||
} from '@/libs/userlocalManager';
|
||||
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
|
||||
const COMMUNITY_MANAGER_EMAIL = process.env.EMAILS_COMMUNITY_MANAGER_EMAIL; // eslint-disable-line
|
||||
|
||||
export default {
|
||||
@@ -265,6 +183,8 @@ export default {
|
||||
AppMenu,
|
||||
AppHeader,
|
||||
AppFooter,
|
||||
DamagePausedBanner,
|
||||
GemsPromoBanner,
|
||||
notificationsDisplay,
|
||||
snackbars,
|
||||
BuyModal,
|
||||
@@ -277,9 +197,6 @@ export default {
|
||||
mixins: [notifications, spellsMixin],
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: svgClose,
|
||||
}),
|
||||
selectedItemToBuy: null,
|
||||
selectedSpellToBuy: null,
|
||||
|
||||
@@ -299,9 +216,6 @@ export default {
|
||||
castingSpell () {
|
||||
return this.$store.state.spellOptions.castingSpell;
|
||||
},
|
||||
showRestingBanner () {
|
||||
return !this.bannerHidden && this.user && this.user.preferences.sleep;
|
||||
},
|
||||
noMargin () {
|
||||
return ['privateMessages'].includes(this.$route.name);
|
||||
},
|
||||
@@ -589,12 +503,6 @@ export default {
|
||||
hideLoadingScreen () {
|
||||
this.loading = false;
|
||||
},
|
||||
hideBanner () {
|
||||
this.bannerHidden = true;
|
||||
},
|
||||
resumeDamage () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
BIN
website/client/src/assets/images/gems/fall-header-bg@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
website/client/src/assets/images/gems/fall-header.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
website/client/src/assets/images/gems/fall-header@2x.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
website/client/src/assets/images/gems/fall-header@3x.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
website/client/src/assets/images/gems/fall-text/text.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
website/client/src/assets/images/gems/fall-text/text@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
website/client/src/assets/images/gems/fall-text/text@3x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
BIN
website/client/src/assets/images/gems/spooky-header-bg@2x.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
website/client/src/assets/images/gems/spooky-header.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
website/client/src/assets/images/gems/spooky-header@2x.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
website/client/src/assets/images/gems/spooky-header@3x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/client/src/assets/images/gems/spooky-text/text.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
website/client/src/assets/images/gems/spooky-text/text@2x.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
website/client/src/assets/images/gems/spooky-text/text@3x.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
website/client/src/assets/images/gems/support-habitica.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
website/client/src/assets/images/gems/support-habitica@2x.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
website/client/src/assets/images/gems/support-habitica@3x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -33,7 +33,6 @@
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
|
||||
.icon-10 {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
|
||||
@@ -35,6 +35,5 @@
|
||||
@import './animals';
|
||||
@import './iconalert';
|
||||
@import './tiers';
|
||||
@import './payments';
|
||||
@import './spacing';
|
||||
@import './modal';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 5.5rem auto 3rem;
|
||||
margin: 3rem auto 3rem;
|
||||
width: auto;
|
||||
|
||||
.title {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
.payments-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 296px;
|
||||
justify-content: center;
|
||||
|
||||
&.payments-disabled {
|
||||
opacity: 0.64;
|
||||
|
||||
.btn, .btn:hover, .btn:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.payment-item > *{
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-item {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
|
||||
&.payment-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.credit-card-icon {
|
||||
width: 21.3px;
|
||||
height: 16px;
|
||||
margin-right: 8.7px;
|
||||
}
|
||||
|
||||
&.paypal-checkout {
|
||||
background: #009cde;
|
||||
|
||||
img {
|
||||
width: 157px;
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,4 @@ $npc_seasonal_flavor: 'normal';
|
||||
$npc_timetravelers_flavor: 'normal';
|
||||
$npc_tavern_flavor: 'normal';
|
||||
|
||||
$restingToolbarHeight: 40px;
|
||||
$menuToolbarHeight: 56px;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:title="title"
|
||||
ok-only
|
||||
:ok-title="$t('onwards')"
|
||||
v-bind:footer-class="{ greyed: displayRewardQuest }"
|
||||
:footer-class="{ greyed: displayRewardQuest }"
|
||||
>
|
||||
<section class="d-flex">
|
||||
<span
|
||||
@@ -23,8 +23,8 @@
|
||||
</section>
|
||||
|
||||
<p
|
||||
class="text"
|
||||
v-once
|
||||
class="text"
|
||||
>
|
||||
{{ $t('levelup') }}
|
||||
</p>
|
||||
@@ -34,8 +34,8 @@
|
||||
class="greyed"
|
||||
>
|
||||
<div
|
||||
class="your-rewards d-flex"
|
||||
v-once
|
||||
class="your-rewards d-flex"
|
||||
>
|
||||
<span
|
||||
class="sparkles"
|
||||
|
||||
@@ -83,24 +83,12 @@
|
||||
class="col-12"
|
||||
>
|
||||
<h2>{{ $t('choosePaymentMethod') }}</h2>
|
||||
<div class="payments-column">
|
||||
<button
|
||||
class="purchase btn btn-primary payment-button payment-item"
|
||||
@click="pay(PAYMENTS.STRIPE)"
|
||||
>
|
||||
<div
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<amazon-button
|
||||
class="payment-item"
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -148,21 +136,16 @@
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import amazonButton from '@/components/payments/amazonButton';
|
||||
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
paymentsButtons,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
}),
|
||||
PAGES: {
|
||||
CREATE_GROUP: 'create-group',
|
||||
UPGRADE_GROUP: 'upgrade-group',
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
Are you ready to upgrade?
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="col-12 text-center">
|
||||
<div class="col-12 text-center mb-4 d-flex justify-content-center">
|
||||
<div class="purple-box">
|
||||
<div class="amount-section">
|
||||
<div class="dollar">
|
||||
@@ -93,26 +93,14 @@
|
||||
</div>
|
||||
<div class="box payment-providers">
|
||||
<h3>Choose your payment method</h3>
|
||||
<div class="payments-column">
|
||||
<button
|
||||
class="purchase btn btn-primary payment-button payment-item"
|
||||
@click="pay(PAYMENTS.STRIPE)"
|
||||
>
|
||||
<div
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<amazon-button
|
||||
class="payment-item"
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!upgradingGroup._id"
|
||||
class="container col-6 offset-3 create-option"
|
||||
@@ -254,24 +242,12 @@
|
||||
>
|
||||
<div class="text-center">
|
||||
<h3>Choose your payment method</h3>
|
||||
<div class="payments-column mx-auto">
|
||||
<button
|
||||
class="purchase btn btn-primary payment-button payment-item"
|
||||
@click="pay(PAYMENTS.STRIPE)"
|
||||
>
|
||||
<div
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<amazon-button
|
||||
class="payment-item"
|
||||
<payments-buttons
|
||||
:stripe-fn="() => pay(PAYMENTS.STRIPE)"
|
||||
:amazon-data="pay(PAYMENTS.AMAZON)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -290,7 +266,6 @@
|
||||
|
||||
.purple-box {
|
||||
color: #bda8ff;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.number {
|
||||
@@ -464,19 +439,17 @@
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import { mapState } from '@/libs/store';
|
||||
import positiveIcon from '@/assets/svg/positive.svg';
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
import amazonButton from '@/components/payments/amazonButton';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
paymentsButtons,
|
||||
},
|
||||
mixins: [paymentsMixin],
|
||||
data () {
|
||||
return {
|
||||
amazonPayments: {},
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
positiveIcon,
|
||||
}),
|
||||
PAGES: {
|
||||
|
||||
122
website/client/src/components/header/banners/base.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="canShow"
|
||||
class="habitica-top-banner d-flex justify-content-between align-items-center"
|
||||
:class="bannerClass"
|
||||
:style="{height}"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
<div
|
||||
v-if="canClose"
|
||||
class="close-icon svg-icon icon-12"
|
||||
|
||||
@click="close()"
|
||||
v-html="icons.close"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
body.modal-open .habitica-top-banner {
|
||||
z-index: 1035;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.habitica-top-banner {
|
||||
width: 100%;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.625rem;
|
||||
z-index: 1300;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
position: relative;
|
||||
top: 0;
|
||||
right: 0;
|
||||
opacity: 0.48;
|
||||
|
||||
& ::v-deep svg path {
|
||||
stroke: $white !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import closeIcon from '@/assets/svg/close.svg';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
bannerId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
bannerClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
canClose: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// Used to correctly show the layout on certain pages with a fixed height
|
||||
// Like the PMs page
|
||||
height: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
close: closeIcon,
|
||||
}),
|
||||
hidden: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canShow () {
|
||||
return !this.hidden && this.show;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
canShow: {
|
||||
handler (newVal) {
|
||||
const valToSet = newVal === true ? this.height : '0px';
|
||||
document.documentElement.style
|
||||
.setProperty(`--banner-${this.bannerId}-height`, valToSet);
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
show (newVal) {
|
||||
// When the show condition is set to false externally, remove the session storage setting
|
||||
if (newVal === false) {
|
||||
window.sessionStorage.removeItem(`hide-banner-${this.bannerId}`);
|
||||
this.hidden = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
const hideStatus = window.sessionStorage.getItem(`hide-banner-${this.bannerId}`);
|
||||
if (hideStatus === 'true') {
|
||||
this.hidden = true;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
window.sessionStorage.setItem(`hide-banner-${this.bannerId}`, 'true');
|
||||
this.hidden = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<base-banner
|
||||
banner-id="damage-paused"
|
||||
banner-class="resting-banner"
|
||||
:show="showRestingBanner"
|
||||
height="40px"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="content"
|
||||
>
|
||||
<span
|
||||
v-once
|
||||
class="label d-inline d-sm-none"
|
||||
>{{ $t('innCheckOutBannerShort') }}</span>
|
||||
<span
|
||||
v-once
|
||||
class="label d-none d-sm-inline"
|
||||
>{{ $t('innCheckOutBanner') }}</span>
|
||||
<span class="separator">|</span>
|
||||
<span
|
||||
v-once
|
||||
class="resume"
|
||||
@click="resumeDamage()"
|
||||
>{{ $t('resumeDamage') }}</span>
|
||||
</div>
|
||||
</base-banner>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.resting-banner {
|
||||
background-color: $blue-10;
|
||||
|
||||
.content {
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
padding: 0.5rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.content {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: $blue-100;
|
||||
margin: 0 1rem;
|
||||
}
|
||||
|
||||
.resume {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import BaseBanner from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseBanner,
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
showRestingBanner () {
|
||||
return this.user && this.user.preferences.sleep;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
resumeDamage () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
132
website/client/src/components/header/banners/gemsPromo.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<base-banner
|
||||
banner-id="gems-promo"
|
||||
:banner-class="bannerClass"
|
||||
:show="showGemsPromoBanner"
|
||||
height="3rem"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
:aria-label="$t('gems')"
|
||||
class="content d-flex justify-content-around align-items-center"
|
||||
@click="openGemsModal"
|
||||
>
|
||||
<img
|
||||
v-if="eventName === 'fall2020'"
|
||||
class="d-none d-xl-block"
|
||||
srcset="
|
||||
~@/assets/images/gems/fall-confetti-left/confetti.png,
|
||||
~@/assets/images/gems/fall-confetti-left/confetti@2x.png 2x,
|
||||
~@/assets/images/gems/fall-confetti-left/confetti@3x.png 3x"
|
||||
src="~@/assets/images/gems/fall-confetti-left/confetti.png"
|
||||
>
|
||||
<img
|
||||
v-else-if="eventName === 'fall2020SecondPromo'"
|
||||
class="d-none d-xl-block"
|
||||
srcset="
|
||||
~@/assets/images/gems/spooky-confetti-left/confetti.png,
|
||||
~@/assets/images/gems/spooky-confetti-left/confetti@2x.png 2x,
|
||||
~@/assets/images/gems/spooky-confetti-left/confetti@3x.png 3x"
|
||||
src="~@/assets/images/gems/spooky-confetti-left/confetti.png"
|
||||
>
|
||||
<div class="promo-test">
|
||||
<img
|
||||
v-if="eventName === 'fall2020'"
|
||||
srcset="
|
||||
~@/assets/images/gems/fall-text/text.png,
|
||||
~@/assets/images/gems/fall-text/text@2x.png 2x,
|
||||
~@/assets/images/gems/fall-text/text@3x.png 3x"
|
||||
src="~@/assets/images/gems/fall-text/text.png"
|
||||
>
|
||||
<img
|
||||
v-else-if="eventName === 'fall2020SecondPromo'"
|
||||
srcset="
|
||||
~@/assets/images/gems/spooky-text/text.png,
|
||||
~@/assets/images/gems/spooky-text/text@2x.png 2x,
|
||||
~@/assets/images/gems/spooky-text/text@3x.png 3x"
|
||||
src="~@/assets/images/gems/spooky-text/text.png"
|
||||
>
|
||||
</div>
|
||||
<img
|
||||
v-if="eventName === 'fall2020'"
|
||||
class="d-none d-xl-block"
|
||||
srcset="
|
||||
~@/assets/images/gems/fall-confetti-right/confetti.png,
|
||||
~@/assets/images/gems/fall-confetti-right/confetti@2x.png 2x,
|
||||
~@/assets/images/gems/fall-confetti-right/confetti@3x.png 3x"
|
||||
src="~@/assets/images/gems/fall-confetti-right/confetti.png"
|
||||
>
|
||||
<img
|
||||
v-else-if="eventName === 'fall2020SecondPromo'"
|
||||
class="d-none d-xl-block"
|
||||
srcset="
|
||||
~@/assets/images/gems/spooky-confetti-right/confetti.png,
|
||||
~@/assets/images/gems/spooky-confetti-right/confetti@2x.png 2x,
|
||||
~@/assets/images/gems/spooky-confetti-right/confetti@3x.png 3x"
|
||||
src="~@/assets/images/gems/spooky-confetti-right/confetti.png"
|
||||
>
|
||||
</div>
|
||||
</base-banner>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.gems-promo-banner-fall2020 {
|
||||
background: $gray-10;
|
||||
}
|
||||
|
||||
.gems-promo-banner-fall2020SecondPromo {
|
||||
background: $black;
|
||||
}
|
||||
|
||||
.gems-promo-banner {
|
||||
.content {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import BaseBanner from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseBanner,
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
}),
|
||||
eventName () {
|
||||
return this.currentEvent && this.currentEvent.event;
|
||||
},
|
||||
showGemsPromoBanner () {
|
||||
const currEvt = this.currentEvent;
|
||||
if (!currEvt) return false;
|
||||
return currEvt.event === 'fall2020' || currEvt.event === 'fall2020SecondPromo';
|
||||
},
|
||||
bannerClass () {
|
||||
const bannerClass = 'gems-promo-banner';
|
||||
|
||||
if (!this.showGemsPromoBanner) return bannerClass;
|
||||
return `${bannerClass} ${bannerClass}-${this.eventName}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openGemsModal () {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventCategory: 'button',
|
||||
eventAction: 'click',
|
||||
eventLabel: 'Gems Promo Banner',
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -6,10 +6,10 @@
|
||||
<send-gems-modal />
|
||||
<select-user-modal />
|
||||
<b-navbar
|
||||
id="habitica-menu"
|
||||
class="topbar navbar-inverse static-top"
|
||||
toggleable="lg"
|
||||
type="dark"
|
||||
:class="navbarZIndexClass"
|
||||
>
|
||||
<b-navbar-brand
|
||||
class="brand"
|
||||
@@ -399,6 +399,12 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
body.modal-open #habitica-menu {
|
||||
z-index: 1035;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/utils.scss';
|
||||
@@ -546,6 +552,7 @@
|
||||
}
|
||||
|
||||
.topbar {
|
||||
z-index: 1080;
|
||||
background: $purple-100 url(~@/assets/svg/for-css/bits.svg) right top no-repeat;
|
||||
min-height: 56px;
|
||||
box-shadow: 0 1px 2px 0 rgba($black, 0.24);
|
||||
@@ -555,16 +562,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-z-index {
|
||||
&-normal {
|
||||
z-index: 1080;
|
||||
}
|
||||
|
||||
&-modal {
|
||||
z-index: 1035;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
padding-left: 10px;
|
||||
width: 128px;
|
||||
@@ -764,12 +761,6 @@ export default {
|
||||
groupPlans: 'groupPlans.data',
|
||||
modalStack: 'modalStack',
|
||||
}),
|
||||
navbarZIndexClass () {
|
||||
if (this.modalStack.length > 0) {
|
||||
return 'navbar-z-index-modal';
|
||||
}
|
||||
return 'navbar-z-index-normal';
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.getUserGroupPlans();
|
||||
|
||||
@@ -70,6 +70,7 @@ export default {
|
||||
amazonPayments: {
|
||||
modal: null,
|
||||
type: null,
|
||||
gemsBlock: null,
|
||||
gift: null,
|
||||
loggedIn: false,
|
||||
paymentSelected: false,
|
||||
@@ -201,6 +202,8 @@ export default {
|
||||
} else if (paymentType.indexOf('gift-') === 0) {
|
||||
appState.gift = this.amazonPayments.gift;
|
||||
appState.giftReceiver = this.amazonPayments.giftReceiver;
|
||||
} else if (paymentType === 'gems') {
|
||||
appState.gemsBlock = this.amazonPayments.gemsBlock;
|
||||
}
|
||||
|
||||
setLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE, JSON.stringify(appState));
|
||||
@@ -218,12 +221,17 @@ export default {
|
||||
// @TODO: A gift should not read the same as buying gems for yourself.
|
||||
if (this.amazonPayments.type === 'single') {
|
||||
const url = '/amazon/checkout';
|
||||
|
||||
try {
|
||||
await axios.post(url, {
|
||||
const data = {
|
||||
orderReferenceId: this.amazonPayments.orderReferenceId,
|
||||
gift: this.amazonPayments.gift,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.amazonPayments.gemsBlock) {
|
||||
data.gemsBlock = this.amazonPayments.gemsBlock.key;
|
||||
}
|
||||
|
||||
try {
|
||||
await axios.post(url, data);
|
||||
|
||||
this.$set(this, 'amazonButtonEnabled', true);
|
||||
this.storePaymentStatusAndReload();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div
|
||||
:id="buttonId"
|
||||
class="amazon-pay-button"
|
||||
:class="{disabled}"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
@@ -14,8 +15,11 @@ import paymentsMixin from '@/mixins/payments';
|
||||
export default {
|
||||
mixins: [paymentsMixin],
|
||||
props: {
|
||||
amazonData: Object,
|
||||
amazonDisabled: {
|
||||
amazonData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
@@ -25,6 +29,7 @@ export default {
|
||||
amazonPayments: {
|
||||
modal: null,
|
||||
type: null,
|
||||
gemsBlock: null,
|
||||
gift: null,
|
||||
loggedIn: false,
|
||||
paymentSelected: false,
|
||||
@@ -42,7 +47,6 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
...mapState(['isAmazonReady']),
|
||||
amazonPaymentsCanCheckout () {
|
||||
if (this.amazonPayments.type === 'single') {
|
||||
@@ -87,7 +91,7 @@ export default {
|
||||
size: 'large',
|
||||
agreementType: 'BillingAgreement',
|
||||
onSignIn: async contract => { // @TODO send to modal
|
||||
if (this.amazonDisabled === true) return null;
|
||||
if (this.disabled === true) return null;
|
||||
// if (!this.checkGemAmount(this.amazonData)) return;
|
||||
this.amazonPayments.billingAgreementId = contract.getAmazonBillingAgreementId();
|
||||
|
||||
@@ -96,7 +100,7 @@ export default {
|
||||
return this.$root.$emit('habitica::pay-with-amazon', this.amazonPayments);
|
||||
},
|
||||
authorization: () => {
|
||||
if (this.amazonDisabled === true) return;
|
||||
if (this.disabled === true) return;
|
||||
|
||||
window.amazon.Login.authorize({
|
||||
scope: 'payments:widget',
|
||||
@@ -117,3 +121,15 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.amazon-pay-button.disabled {
|
||||
.amazonpay-button-inner-image {
|
||||
cursor: default !important;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
|
||||
import PaymentsButtonsList from './list.vue';
|
||||
import getStore from '@/store';
|
||||
import { setup as setupPayments } from '@/libs/payments';
|
||||
|
||||
setupPayments();
|
||||
|
||||
storiesOf('Payments Buttons', module)
|
||||
.add('simple', () => ({
|
||||
components: { PaymentsButtonsList },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<payments-buttons-list
|
||||
:amazon-data="{type: 'single'}"
|
||||
:stripe-fn="() => {}"
|
||||
:paypal-fn="() => {}"
|
||||
></payments-buttons-list>
|
||||
</div>
|
||||
`,
|
||||
store: getStore(),
|
||||
}))
|
||||
.add('disabled', () => ({
|
||||
components: { PaymentsButtonsList },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<payments-buttons-list
|
||||
:disabled="true"
|
||||
:amazon-data="{type: 'single'}"
|
||||
:stripe-fn="() => {}"
|
||||
:paypal-fn="() => {}"
|
||||
></payments-buttons-list>
|
||||
</div>
|
||||
`,
|
||||
store: getStore(),
|
||||
}))
|
||||
.add('only stripe and amazon (example)', () => ({
|
||||
components: { PaymentsButtonsList },
|
||||
template: `
|
||||
<div style="position: absolute; margin: 20px">
|
||||
<payments-buttons-list
|
||||
:amazon-data="{type: 'single'}"
|
||||
:stripe-fn="() => {}"
|
||||
></payments-buttons-list>
|
||||
</div>
|
||||
`,
|
||||
store: getStore(),
|
||||
}));
|
||||
127
website/client/src/components/payments/buttons/list.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="payments-column mx-auto mt-auto">
|
||||
<button
|
||||
v-if="stripeAvailable"
|
||||
class="btn btn-primary payment-button payment-item"
|
||||
:class="{disabled}"
|
||||
:disabled="disabled"
|
||||
@click="stripeFn()"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="paypalAvailable"
|
||||
class="btn payment-item paypal-checkout payment-button"
|
||||
:class="{disabled}"
|
||||
:disabled="disabled"
|
||||
@click="paypalFn()"
|
||||
>
|
||||
|
||||
<img
|
||||
src="~@/assets/images/paypal-checkout.png"
|
||||
:alt="$t('paypal')"
|
||||
>
|
||||
</button>
|
||||
<amazon-button
|
||||
v-if="amazonAvailable"
|
||||
class="payment-item"
|
||||
:disabled="disabled"
|
||||
:amazon-data="amazonData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.payments-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 296px;
|
||||
justify-content: center;
|
||||
|
||||
.payment-item {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
|
||||
&.payment-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.credit-card-icon {
|
||||
width: 21.3px;
|
||||
height: 16px;
|
||||
margin-right: 8.7px;
|
||||
}
|
||||
|
||||
&.paypal-checkout {
|
||||
background: #009cde;
|
||||
|
||||
img {
|
||||
width: 157px;
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.64;
|
||||
|
||||
.btn, .btn:hover, .btn:active {
|
||||
box-shadow: none;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import amazonButton from '@/components/payments/buttons/amazon';
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
},
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
amazonData: {
|
||||
type: Object,
|
||||
},
|
||||
stripeFn: {
|
||||
type: Function,
|
||||
},
|
||||
paypalFn: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
stripeAvailable () {
|
||||
return typeof this.stripeFn === 'function';
|
||||
},
|
||||
paypalAvailable () {
|
||||
return typeof this.paypalFn === 'function';
|
||||
},
|
||||
amazonAvailable () {
|
||||
return this.amazonData !== undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -3,316 +3,333 @@
|
||||
<b-modal
|
||||
id="buy-gems"
|
||||
:hide-footer="true"
|
||||
size="lg"
|
||||
size="md"
|
||||
:modal-class="eventClass"
|
||||
>
|
||||
<div
|
||||
slot="modal-header"
|
||||
class="header-wrap"
|
||||
class="header-wrap container"
|
||||
>
|
||||
<div class="image-gemfall">
|
||||
<div class="row">
|
||||
<h2 class="header-invert mx-auto">
|
||||
{{ $t('support') }}
|
||||
</h2>
|
||||
</div>
|
||||
<span
|
||||
v-once
|
||||
class="close-icon svg-icon inline icon-12"
|
||||
@click="close()"
|
||||
v-html="icons.close"
|
||||
></span>
|
||||
<div class="row">
|
||||
<div
|
||||
class="logo svg-icon mx-auto"
|
||||
v-html="icons.logo"
|
||||
></div>
|
||||
class="col-12 text-center"
|
||||
>
|
||||
<img
|
||||
v-if="eventName === 'fall2020'"
|
||||
:alt="$t('supportHabitica')"
|
||||
srcset="
|
||||
~@/assets/images/gems/fall-header.png,
|
||||
~@/assets/images/gems/fall-header@2x.png 2x,
|
||||
~@/assets/images/gems/fall-header@3x.png 3x"
|
||||
src="~@/assets/images/gems/fall-header.png"
|
||||
>
|
||||
<img
|
||||
v-else-if="eventName === 'fall2020SecondPromo'"
|
||||
:alt="$t('supportHabitica')"
|
||||
srcset="
|
||||
~@/assets/images/gems/spooky-header.png,
|
||||
~@/assets/images/gems/spooky-header@2x.png 2x,
|
||||
~@/assets/images/gems/spooky-header@3x.png 3x"
|
||||
src="~@/assets/images/gems/spooky-header.png"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
:alt="$t('supportHabitica')"
|
||||
srcset="
|
||||
~@/assets/images/gems/support-habitica.png,
|
||||
~@/assets/images/gems/support-habitica@2x.png 2x,
|
||||
~@/assets/images/gems/support-habitica@3x.png 3x"
|
||||
src="~@/assets/images/gems/support-habitica.png"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasSubscription">
|
||||
<div class="container">
|
||||
<div class="row text-center">
|
||||
<h2 class="mx-auto text-leadin">
|
||||
{{ $t('subscriptionAlreadySubscribedLeadIn') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col-6 offset-3">
|
||||
<p>{{ $t("gemsPurchaseNote") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<h2 class="mx-auto text-leadin">
|
||||
<h2
|
||||
v-once
|
||||
class="col-12 text-leadin"
|
||||
>
|
||||
{{ $t('gemBenefitLeadin') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col-md-2 offset-1">
|
||||
<div class="d-flex bubble justify-content-center align-items-center">
|
||||
<div
|
||||
class="svg-icon check mx-auto"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 align-self-center">
|
||||
<p>{{ $t('gemBenefit1') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 offset-1">
|
||||
<div class="d-flex bubble justify-content-center align-items-center">
|
||||
<div
|
||||
class="svg-icon check mx-auto"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 align-self-center">
|
||||
<p>{{ $t('gemBenefit2') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col-md-2 offset-1">
|
||||
<div class="d-flex bubble justify-content-center align-items-center">
|
||||
<div
|
||||
class="svg-icon check mx-auto"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 align-self-center">
|
||||
<p>{{ $t('gemBenefit3') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-2 offset-1">
|
||||
<div class="d-flex bubble justify-content-center align-items-center">
|
||||
<div
|
||||
class="svg-icon check mx-auto"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 align-self-center">
|
||||
<p>{{ $t('gemBenefit4') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-deck gem-deck">
|
||||
<div
|
||||
class="card text-center col-3"
|
||||
:class="{active: gemAmount === 20 }"
|
||||
v-once
|
||||
class="row gem-benefits pb-2"
|
||||
>
|
||||
<div class="card-img-top">
|
||||
<div
|
||||
class="mx-auto"
|
||||
style="'height: 55px; width: 47.5px; margin-top: 1.85em;'"
|
||||
v-html="icons.twentyOneGems"
|
||||
v-for="benefit in [1,2,3,4]"
|
||||
:key="benefit"
|
||||
class="col-md-6 d-flex pl-4 pr-0 pb-3"
|
||||
>
|
||||
<div class="d-flex bubble justify-content-center align-items-center">
|
||||
<div
|
||||
class="svg-icon check mx-auto"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="gem-count">
|
||||
20
|
||||
<p class="small-text pl-2 mb-0">
|
||||
{{ $t(`gemBenefit${benefit}`) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="gem-text">
|
||||
</div>
|
||||
<div class="row gem-deck">
|
||||
<div
|
||||
v-for="gemsBlock in gemsBlocks"
|
||||
:key="gemsBlock.key"
|
||||
class="text-center col-3"
|
||||
:class="{active: selectedGemsBlock === gemsBlock }"
|
||||
>
|
||||
<div
|
||||
class="gem-icon"
|
||||
v-html="icons[gemsBlock.key]"
|
||||
></div>
|
||||
<div class="gem-count">
|
||||
{{ gemsBlock.gems }}
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="gem-text"
|
||||
>
|
||||
{{ $t('gems') }}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="gemAmount === 20 ? gemAmount = 0 : gemAmount = 20"
|
||||
v-if="!isSelected(gemsBlock)"
|
||||
class="btn btn-primary gem-btn"
|
||||
@click="selectGemsBlock(gemsBlock)"
|
||||
>
|
||||
{{ gemAmount === 20 ? $t('selected') : '$5.00' }}
|
||||
{{ `$${gemsBlock.price / 100}` }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-success gem-btn"
|
||||
@click="selectGemsBlock(gemsBlock)"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon check text-white mx-auto"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</button>
|
||||
<span
|
||||
v-if="gemsBlock.originalGems"
|
||||
class="small-text original-gems"
|
||||
>
|
||||
{{ $t('usuallyGems', {originalGems: gemsBlock.originalGems}) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<h2 class="mx-auto text-payment">
|
||||
<h4
|
||||
v-once
|
||||
class="col-12 text-payment mb-3"
|
||||
>
|
||||
{{ $t('choosePaymentMethod') }}
|
||||
</h2>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="payments-column">
|
||||
<button
|
||||
class="purchase btn btn-primary payment-button payment-item"
|
||||
@click="showStripe({})"
|
||||
>
|
||||
<div
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn payment-item paypal-checkout payment-button"
|
||||
@click="openPaypal(paypalCheckoutLink, 'gems')"
|
||||
>
|
||||
|
||||
<img
|
||||
src="~@/assets/images/paypal-checkout.png"
|
||||
:alt="$t('paypal')"
|
||||
>
|
||||
</button>
|
||||
<amazon-button
|
||||
class="payment-item"
|
||||
:amazon-data="{type: 'single'}"
|
||||
<payments-buttons
|
||||
:disabled="!selectedGemsBlock"
|
||||
:stripe-fn="() => showStripe({ gemsBlock: selectedGemsBlock })"
|
||||
:paypal-fn="() => openPaypal({
|
||||
url: paypalCheckoutLink, type: 'gems', gemsBlock: selectedGemsBlock
|
||||
})"
|
||||
:amazon-data="{type: 'single', gemsBlock: selectedGemsBlock}"
|
||||
/>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div
|
||||
class="svg-icon mx-auto"
|
||||
style="'height: 24px; width: 24px;'"
|
||||
v-html="icons.heart"
|
||||
></div>
|
||||
</div>
|
||||
<div class="row text-center text-outtro">
|
||||
<div class="col-6 offset-3">
|
||||
{{ $t('buyGemsSupportsDevs') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#buy-gems .modal-body {
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
#buy-gems {
|
||||
.close-icon svg path {
|
||||
stroke: $purple-400;
|
||||
}
|
||||
|
||||
.close-icon:hover svg path {
|
||||
stroke: $purple-600;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
max-width: 35.375rem;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 0;
|
||||
padding-bottom: 2rem;
|
||||
background: $white;
|
||||
border-radius: 0px 0px 8px 8px;
|
||||
}
|
||||
|
||||
#buy-gems .modal-content {
|
||||
border-radius: 8px;
|
||||
width: 824px;
|
||||
.modal-content {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#buy-gems .modal-header {
|
||||
.modal-header {
|
||||
padding: 0;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall 2020 events styles
|
||||
#buy-gems.event-fall2020, #buy-gems.event-fall2020SecondPromo {
|
||||
.header-wrap {
|
||||
padding-top: 4.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.gem-btn {
|
||||
background-image: linear-gradient(293deg, $red-100, $orange-50 51%, $yellow-50);
|
||||
border: none;
|
||||
|
||||
&.btn-success {
|
||||
background: $green-50 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-count {
|
||||
color: $orange-50;
|
||||
}
|
||||
|
||||
.close-icon svg path {
|
||||
stroke: $gray-200;
|
||||
}
|
||||
|
||||
.close-icon:hover svg path {
|
||||
stroke: $gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
#buy-gems.event-fall2020 {
|
||||
.header-wrap {
|
||||
background-image: url('~@/assets/images/gems/fall-header-bg@2x.png');
|
||||
background-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#buy-gems.event-fall2020SecondPromo {
|
||||
.header-wrap {
|
||||
background-image: url('~@/assets/images/gems/spooky-header-bg@2x.png');
|
||||
background-size: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
|
||||
.payments-column {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
a.mx-auto {
|
||||
color: #2995cd;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
.gem-btn {
|
||||
min-width: 4.813rem;
|
||||
min-height: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 1000px;
|
||||
border: solid 2px #e1e0e3;
|
||||
border: 2px solid $gray-400;
|
||||
}
|
||||
|
||||
.gem-benefits {
|
||||
p {
|
||||
font-style: normal;
|
||||
color: $gray-100;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.gem-icon {
|
||||
margin: 0 auto;
|
||||
height: 55px;
|
||||
width: 47.5px;
|
||||
margin-top: 1.85em;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.original-gems {
|
||||
margin-top: 0.5rem;
|
||||
font-style: normal;
|
||||
color: $gray-300;
|
||||
}
|
||||
|
||||
.gem-deck {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 80%;
|
||||
height: 1px;
|
||||
background-color: #e1e0e3;
|
||||
margin: 1em auto;
|
||||
background: $gray-700;
|
||||
color: $gray-100;
|
||||
padding-bottom: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gem-count {
|
||||
font-family: Roboto;
|
||||
font-size: 40px;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #2995cd;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.gem-text {
|
||||
font-family: Roboto;
|
||||
font-size: 16px;
|
||||
color: #a5a1ac;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.image-gemfall {
|
||||
background: url(~@/assets/images/gemfall.png) center repeat-y;
|
||||
height: 14em;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.header-wrap {
|
||||
background-image: linear-gradient(74deg, #4f2a93, #6133b4);
|
||||
height: 14em;
|
||||
background-image: linear-gradient(75deg, $purple-300, $purple-200 100%);
|
||||
width: 100%;
|
||||
color: #4e4a57;
|
||||
padding: 0;
|
||||
border-top-left-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 256px;
|
||||
height: 56px;
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 2.5rem;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
}
|
||||
|
||||
.svg-icon.check {
|
||||
color: #bda8ff;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.header-invert {
|
||||
margin: 3rem auto 1.5rem;
|
||||
color: #FFFFFF;
|
||||
font-family: Roboto;
|
||||
font-weight: normal;
|
||||
letter-spacing: 0.18em;
|
||||
color: $purple-400;
|
||||
width: 0.77rem;
|
||||
height: 0.615rem;
|
||||
}
|
||||
|
||||
.text-leadin {
|
||||
margin: 1rem;
|
||||
margin: 1.5rem auto;
|
||||
font-weight: bold;
|
||||
color: $purple-200;
|
||||
}
|
||||
|
||||
.text-outtro {
|
||||
margin-bottom: 1em;
|
||||
color: #a5a1ac;
|
||||
}
|
||||
|
||||
.text-payment {
|
||||
color: #4e4a57;
|
||||
font-size: 24px;
|
||||
margin: 1em;
|
||||
line-height: 1.71;
|
||||
color: $gray-50;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
import markdown from '@/directives/markdown';
|
||||
import planGemLimits from '@/../../common/script/libs/planGemLimits';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
|
||||
import checkIcon from '@/assets/svg/check.svg';
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
import heart from '@/assets/svg/health.svg';
|
||||
import logo from '@/assets/svg/habitica-logo.svg';
|
||||
|
||||
import fourGems from '@/assets/svg/4-gems.svg';
|
||||
import twentyOneGems from '@/assets/svg/21-gems.svg';
|
||||
import fortyTwoGems from '@/assets/svg/42-gems.svg';
|
||||
import eightyFourGems from '@/assets/svg/84-gems.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
|
||||
import amazonButton from '@/components/payments/amazonButton';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
paymentsButtons,
|
||||
},
|
||||
directives: {
|
||||
markdown,
|
||||
@@ -321,30 +338,69 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
logo,
|
||||
close: svgClose,
|
||||
check: checkIcon,
|
||||
creditCardIcon,
|
||||
heart,
|
||||
twentyOneGems,
|
||||
'4gems': fourGems,
|
||||
'21gems': twentyOneGems,
|
||||
'42gems': fortyTwoGems,
|
||||
'84gems': eightyFourGems,
|
||||
}),
|
||||
gemAmount: 0,
|
||||
planGemLimits,
|
||||
selectedGemsBlock: null,
|
||||
alreadyTracked: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
hasSubscription () {
|
||||
return Boolean(this.user.purchased.plan.customerId);
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
originalGemsBlocks: 'content.gems',
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
}),
|
||||
eventName () {
|
||||
return this.currentEvent && this.currentEvent.event;
|
||||
},
|
||||
userReachedGemCap () {
|
||||
return this.user.purchased.plan.customerId
|
||||
&& this.user.purchased.plan.gemsBought
|
||||
>= (this.user.purchased.plan.consecutive.gemCapExtra + this.planGemLimits.convCap);
|
||||
eventClass () {
|
||||
if (this.currentEvent && this.currentEvent.gemsPromo) {
|
||||
return `event-${this.eventName}`;
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isGemsPromoActive () {
|
||||
const currEvt = this.currentEvent;
|
||||
if (currEvt && currEvt.gemsPromo && moment().isBefore(currEvt.end)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
gemsBlocks () {
|
||||
// We don't want to modify the original gems blocks when a promotion is running
|
||||
// Also the content data is frozen with Object.freeze and can't be changed
|
||||
// So we clone the blocks and adjust the number of gems if necessary
|
||||
const blocks = {};
|
||||
|
||||
Object.keys(this.originalGemsBlocks).forEach(gemsBlockKey => {
|
||||
const originalBlock = this.originalGemsBlocks[gemsBlockKey];
|
||||
const newBlock = blocks[gemsBlockKey] = { ...originalBlock }; // eslint-disable-line no-multi-assign, max-len
|
||||
|
||||
if (this.isGemsPromoActive) {
|
||||
newBlock.originalGems = originalBlock.gems;
|
||||
newBlock.gems = (
|
||||
this.currentEvent.gemsPromo[gemsBlockKey] || originalBlock.gems
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return blocks;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
await this.$store.dispatch('worldState:getWorldState');
|
||||
|
||||
this.$root.$on('bv::show::modal', (modalId, data = {}) => {
|
||||
// We force reloading the world state every time the modal is reopened
|
||||
// To make sure the promo status is always up to date
|
||||
this.$store.dispatch('worldState:getWorldState', { forceLoad: true });
|
||||
|
||||
// Track opening of gems modal unless it's been already tracked
|
||||
// For example the gems button in the menu already tracks the event by itself
|
||||
if (modalId === 'buy-gems' && data.alreadyTracked !== true) {
|
||||
@@ -358,6 +414,16 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
selectGemsBlock (gemsBlock) {
|
||||
if (gemsBlock === this.selectedGemsBlock) {
|
||||
this.selectedGemsBlock = null;
|
||||
} else {
|
||||
this.selectedGemsBlock = gemsBlock;
|
||||
}
|
||||
},
|
||||
isSelected (gemsBlock) {
|
||||
return this.selectedGemsBlock === gemsBlock;
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'buy-gems');
|
||||
},
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<h3 class="panel-heading clearfix">
|
||||
<div class="float-right">
|
||||
<span
|
||||
v-if="gift.gems.fromBalance"
|
||||
v-if="fromBal"
|
||||
>{{ $t('sendGiftGemsBalance', {number: userLoggedIn.balance * 4}) }}</span>
|
||||
<span
|
||||
v-if="!gift.gems.fromBalance"
|
||||
v-else
|
||||
>{{ $t('sendGiftCost', {cost: gift.gems.amount / 4}) }}</span>
|
||||
</div>
|
||||
{{ $t('gemsPopoverTitle') }}
|
||||
@@ -32,15 +32,15 @@
|
||||
type="number"
|
||||
placeholder="Number of Gems"
|
||||
min="0"
|
||||
:max="gift.gems.fromBalance ? userLoggedIn.balance * 4 : 9999"
|
||||
:max="fromBal ? userLoggedIn.balance * 4 : 9999"
|
||||
>
|
||||
</div>
|
||||
<div class="btn-group ml-auto">
|
||||
<button
|
||||
class="btn"
|
||||
:class="{
|
||||
'btn-primary': gift.gems.fromBalance,
|
||||
'btn-secondary': !gift.gems.fromBalance,
|
||||
'btn-primary': fromBal,
|
||||
'btn-secondary': !fromBal,
|
||||
}"
|
||||
@click="gift.gems.fromBalance = true"
|
||||
>
|
||||
@@ -49,8 +49,8 @@
|
||||
<button
|
||||
class="btn"
|
||||
:class="{
|
||||
'btn-primary': !gift.gems.fromBalance,
|
||||
'btn-secondary': gift.gems.fromBalance,
|
||||
'btn-primary': !fromBal,
|
||||
'btn-secondary': fromBal,
|
||||
}"
|
||||
@click="gift.gems.fromBalance = false"
|
||||
>
|
||||
@@ -120,40 +120,16 @@
|
||||
>
|
||||
{{ $t("send") }}
|
||||
</button>
|
||||
<div
|
||||
<payments-buttons
|
||||
v-else
|
||||
class="payments-column mx-auto"
|
||||
:class="{'payments-disabled': !gift.subscription.key && gift.gems.amount < 1}"
|
||||
>
|
||||
<button
|
||||
class="purchase btn btn-primary payment-button payment-item"
|
||||
:disabled="!gift.subscription.key && gift.gems.amount < 1"
|
||||
@click="showStripe({gift, uuid: userReceivingGems._id, receiverName})"
|
||||
>
|
||||
<div
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn payment-item paypal-checkout payment-button"
|
||||
:disabled="!gift.subscription.key && gift.gems.amount < 1"
|
||||
@click="openPaypalGift({gift: gift, giftedTo: userReceivingGems._id, receiverName})"
|
||||
>
|
||||
|
||||
<img
|
||||
src="~@/assets/images/paypal-checkout.png"
|
||||
:alt="$t('paypal')"
|
||||
>
|
||||
</button>
|
||||
<amazon-button
|
||||
class="payment-item mb-0"
|
||||
:stripe-fn="() => showStripe({gift, uuid: userReceivingGems._id, receiverName})"
|
||||
:paypal-fn="() => openPaypalGift({
|
||||
gift: gift, giftedTo: userReceivingGems._id, receiverName,
|
||||
})"
|
||||
:amazon-data="{type: 'single', gift, giftedTo: userReceivingGems._id, receiverName}"
|
||||
:amazon-disabled="!gift.subscription.key && gift.gems.amount < 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
@@ -194,15 +170,14 @@ import { mapState } from '@/libs/store';
|
||||
import planGemLimits from '@/../../common/script/libs/planGemLimits';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import amazonButton from '@/components/payments/amazonButton';
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
|
||||
// @TODO: EMAILS.TECH_ASSISTANCE_EMAIL, load from config
|
||||
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
paymentsButtons,
|
||||
},
|
||||
mixins: [paymentsMixin, notificationsMixin],
|
||||
data () {
|
||||
@@ -223,9 +198,6 @@ export default {
|
||||
},
|
||||
sendingInProgress: false,
|
||||
userReceivingGems: null,
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
class="svg-icon"
|
||||
v-html="icons.gem"
|
||||
></div>
|
||||
<span>20</span>
|
||||
<span>{{ paymentData.gemsBlock.gems }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
|
||||
@@ -25,36 +25,10 @@
|
||||
</div>
|
||||
</b-form-radio>
|
||||
</b-form-group>
|
||||
<div class="payments-column mx-auto mt-auto">
|
||||
<button
|
||||
class="purchase btn btn-primary payment-button payment-item"
|
||||
:class="{disabled: !subscription.key}"
|
||||
:disabled="!subscription.key"
|
||||
@click="showStripe({subscription:subscription.key, coupon:subscription.coupon})"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon credit-card-icon"
|
||||
v-html="icons.creditCardIcon"
|
||||
></div>
|
||||
{{ $t('card') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn payment-item paypal-checkout payment-button"
|
||||
:class="{disabled: !subscription.key}"
|
||||
:disabled="!subscription.key"
|
||||
@click="openPaypal(paypalPurchaseLink, 'subscription')"
|
||||
>
|
||||
|
||||
<img
|
||||
src="~@/assets/images/paypal-checkout.png"
|
||||
:alt="$t('paypal')"
|
||||
>
|
||||
</button>
|
||||
<amazon-button
|
||||
class="payment-item"
|
||||
:class="{disabled: !subscription.key}"
|
||||
<payments-buttons
|
||||
:disabled="!subscription.key"
|
||||
:stripe-fn="() => showStripe({subscription:subscription.key, coupon:subscription.coupon})"
|
||||
:paypal-fn="() => openPaypal({url: paypalPurchaseLink, type: 'subscription'})"
|
||||
:amazon-data="{
|
||||
type: 'subscription',
|
||||
subscription: subscription.key,
|
||||
@@ -62,17 +36,12 @@
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
#subscription-form {
|
||||
.disabled .amazonpay-button-inner-image {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.custom-control .custom-control-label::before,
|
||||
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
|
||||
margin-top: 0.75rem;
|
||||
@@ -110,15 +79,6 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.disabled {
|
||||
opacity: 0.64;
|
||||
|
||||
.btn, .btn:hover, .btn:active {
|
||||
box-shadow: none;
|
||||
cursor: default !important;
|
||||
}
|
||||
}
|
||||
|
||||
.subscribe-option {
|
||||
border-bottom: 1px solid $gray-600;
|
||||
}
|
||||
@@ -128,14 +88,13 @@
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import amazonButton from '@/components/payments/amazonButton';
|
||||
import creditCardIcon from '@/assets/svg/credit-card-icon.svg';
|
||||
import paymentsButtons from '@/components/payments/buttons/list';
|
||||
import paymentsMixin from '../../mixins/payments';
|
||||
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
paymentsButtons,
|
||||
},
|
||||
mixins: [
|
||||
paymentsMixin,
|
||||
@@ -145,9 +104,6 @@ export default {
|
||||
subscription: {
|
||||
key: null,
|
||||
},
|
||||
icons: Object.freeze({
|
||||
creditCardIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -363,12 +363,6 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.close-icon {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.hourglass-nonsub {
|
||||
color: $yellow-5;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
calendar-class="calendar-padding"
|
||||
@input="upDate($event)"
|
||||
>
|
||||
<div slot="afterDateInput"
|
||||
class="vdp-datepicker__clear-button"
|
||||
<div
|
||||
v-if="clearButton && value"
|
||||
slot="afterDateInput"
|
||||
class="vdp-datepicker__clear-button"
|
||||
@click="upDate(null)"
|
||||
v-html="icons.close"
|
||||
@click="upDate(null)">
|
||||
>
|
||||
</div>
|
||||
<div slot="beforeCalendarHeader">
|
||||
<div class="datetime-buttons">
|
||||
|
||||
@@ -46,15 +46,26 @@ export default {
|
||||
const encodedString = JSON.stringify(gift);
|
||||
return encodeURIComponent(encodedString);
|
||||
},
|
||||
openPaypalGift (data) {
|
||||
if (!this.checkGemAmount(data)) return;
|
||||
openPaypalGift (giftData) {
|
||||
if (!this.checkGemAmount(giftData)) return;
|
||||
|
||||
const gift = this.encodeGift(data.giftedTo, data.gift);
|
||||
const gift = this.encodeGift(giftData.giftedTo, giftData.gift);
|
||||
const url = `/paypal/checkout?gift=${gift}`;
|
||||
|
||||
this.openPaypal(url, `gift-${data.gift.type === 'gems' ? 'gems' : 'subscription'}`, data);
|
||||
this.openPaypal({
|
||||
url,
|
||||
type: `gift-${giftData.gift.type === 'gems' ? 'gems' : 'subscription'}`,
|
||||
giftData,
|
||||
});
|
||||
},
|
||||
openPaypal (url, type, giftData) {
|
||||
openPaypal (data = {}) {
|
||||
const {
|
||||
type,
|
||||
giftData,
|
||||
gemsBlock,
|
||||
} = data;
|
||||
let { url } = data;
|
||||
|
||||
const appState = {
|
||||
paymentMethod: 'paypal',
|
||||
paymentCompleted: false,
|
||||
@@ -70,6 +81,11 @@ export default {
|
||||
appState.giftReceiver = giftData.receiverName;
|
||||
}
|
||||
|
||||
if (type === 'gems') {
|
||||
appState.gemsBlock = gemsBlock;
|
||||
url += `?gemsBlock=${gemsBlock.key}`;
|
||||
}
|
||||
|
||||
setLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE, JSON.stringify(appState));
|
||||
window.open(url, '_blank');
|
||||
|
||||
@@ -97,7 +113,8 @@ export default {
|
||||
|
||||
sub = sub && subscriptionBlocks[sub];
|
||||
|
||||
let amount = 500; // 500 = $5
|
||||
let amount;
|
||||
if (data.gemsBlock) amount = data.gemsBlock.price;
|
||||
if (sub) amount = sub.price * 100;
|
||||
if (data.gift && data.gift.type === 'gems') amount = (data.gift.gems.amount / 4) * 100;
|
||||
if (data.group) amount = (sub.price + 3 * (data.group.memberCount - 1)) * 100;
|
||||
@@ -109,14 +126,18 @@ export default {
|
||||
if (data.gift && data.gift.type === 'gems') paymentType = 'gift-gems';
|
||||
if (data.gift && data.gift.type === 'subscription') paymentType = 'gift-subscription';
|
||||
|
||||
const label = (sub && paymentType !== 'gift-subscription')
|
||||
? this.$t('subscribe')
|
||||
: this.$t('checkout');
|
||||
|
||||
window.StripeCheckout.open({
|
||||
key: STRIPE_PUB_KEY,
|
||||
address: false,
|
||||
amount,
|
||||
name: 'Habitica',
|
||||
description: sub ? this.$t('subscribe') : this.$t('checkout'),
|
||||
description: label,
|
||||
// image: '/apple-touch-icon-144-precomposed.png',
|
||||
panelLabel: sub ? this.$t('subscribe') : this.$t('checkout'),
|
||||
panelLabel: label,
|
||||
token: async res => {
|
||||
let url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
|
||||
|
||||
@@ -126,6 +147,7 @@ export default {
|
||||
res.paymentType = 'Stripe';
|
||||
}
|
||||
|
||||
if (data.gemsBlock) url += `&gemsBlock=${data.gemsBlock.key}`;
|
||||
if (data.gift) url += `&gift=${this.encodeGift(data.uuid, data.gift)}`;
|
||||
if (data.subscription) url += `&sub=${sub.key}`;
|
||||
if (data.coupon) url += `&coupon=${data.coupon}`;
|
||||
@@ -160,6 +182,8 @@ export default {
|
||||
} else if (paymentType.indexOf('gift-') === 0) {
|
||||
appState.gift = data.gift;
|
||||
appState.giftReceiver = data.receiverName;
|
||||
} else if (paymentType === 'gems') {
|
||||
appState.gemsBlock = data.gemsBlock;
|
||||
}
|
||||
|
||||
|
||||
@@ -233,6 +257,10 @@ export default {
|
||||
amazonPaymentsInit (data) {
|
||||
if (data.type !== 'single' && data.type !== 'subscription') return;
|
||||
|
||||
if (data.type === 'single') {
|
||||
this.amazonPayments.gemsBlock = data.gemsBlock;
|
||||
}
|
||||
|
||||
if (data.gift) {
|
||||
if (data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return;
|
||||
data.gift.uuid = data.giftedTo;
|
||||
|
||||
@@ -193,13 +193,16 @@
|
||||
#private-message {
|
||||
height: calc(100vh - #{$menuToolbarHeight} -
|
||||
var(--banner-gifting-height, 0px) -
|
||||
var(--banner-resting-height, 0px)); // css variable magic :), must be 0px, 0 alone won't work
|
||||
var(--banner-damage-paused-height, 0px) -
|
||||
var(--banner-gems-promo-height, 0px)
|
||||
); // css variable magic :), must be 0px, 0 alone won't work
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
height: calc(100vh - #{$menuToolbarHeight} - #{$pmHeaderHeight} -
|
||||
var(--banner-gifting-height, 0px) -
|
||||
var(--banner-resting-height, 0px)
|
||||
var(--banner-damage-paused-height, 0px) -
|
||||
var(--banner-gems-promo-height, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -146,13 +146,14 @@
|
||||
"couponCodeRequired": "The coupon code is required.",
|
||||
"subCanceledTitle": "Subscription Canceled",
|
||||
"choosePaymentMethod": "Choose your payment method",
|
||||
"buyGemsSupportsDevs": "Purchasing Gems supports the developers and helps keep Habitica running",
|
||||
"support": "SUPPORT",
|
||||
"gemBenefitLeadin": "Gems allow you to buy fun extras for your account, including:",
|
||||
"supportHabitica": "Support Habitica",
|
||||
"gemBenefitLeadin": "What can you buy with Gems?",
|
||||
"gemBenefit1": "Unique and fashionable costumes for your avatar.",
|
||||
"gemBenefit2": "Backgrounds to immerse your avatar in the world of Habitica!",
|
||||
"gemBenefit3": "Exciting Quest chains that drop pet eggs.",
|
||||
"gemBenefit4": "Reset your avatar's Stat Points and change its Class.",
|
||||
"usuallyGems": "Usually <%= originalGems %>",
|
||||
"subscriptionBenefit1": "Alexander the Merchant will now sell you Gems from the Market for 20 Gold each!",
|
||||
"subscriptionBenefit3": "Discover even more items in Habitica with a 2x daily drop-cap.",
|
||||
"subscriptionBenefit4": "Unique cosmetic item for you to decorate your avatar each month.",
|
||||
@@ -164,8 +165,6 @@
|
||||
"subscribersReceiveBenefits": "Subscribers receive these useful benefits!",
|
||||
"monthlyMysteryItems": "Monthly Mystery Items",
|
||||
"doubleDropCap": "Double the Drops",
|
||||
"subscriptionAlreadySubscribedLeadIn": "Thanks for subscribing!",
|
||||
"gemsPurchaseNote": "Subscribers can buy gems for gold in the Market! For easy access, you can also pin the gem to your Rewards column.",
|
||||
"youAreSubscribed": "You are subscribed to Habitica",
|
||||
"subscriptionCanceled": "Your subscription is canceled",
|
||||
"subscriptionInactiveDate": "Your subscription benefits will become inactive on <strong><%= date %></strong>",
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
export const CURRENT_SEASON = moment().isBefore('2020-08-02') ? 'summer' : '_NONE_';
|
||||
|
||||
export const CLASSES = [
|
||||
'warrior',
|
||||
'rogue',
|
||||
'healer',
|
||||
'wizard',
|
||||
];
|
||||
|
||||
// IMPORTANT: The end date should be one to two days AFTER the actual end of
|
||||
// the event, to allow people in different timezones to still buy the
|
||||
// event gear up until at least the actual end of the event.
|
||||
|
||||
export const EVENTS = {
|
||||
winter: { start: '2013-12-31', end: '2014-02-01' },
|
||||
birthday: { start: '2017-01-31', end: '2017-02-02' },
|
||||
spring: { start: '2014-03-21', end: '2014-05-01' },
|
||||
summer: { start: '2014-06-20', end: '2014-08-01' },
|
||||
fall: { start: '2014-09-21', end: '2014-11-01' },
|
||||
winter2015: { start: '2014-12-21', end: '2015-02-02' },
|
||||
spring2015: { start: '2015-03-20', end: '2015-05-02' },
|
||||
summer2015: { start: '2015-06-20', end: '2015-08-02' },
|
||||
fall2015: { start: '2015-09-21', end: '2015-11-01' },
|
||||
gaymerx: { start: '2016-09-29', end: '2016-10-03' },
|
||||
winter2016: { start: '2015-12-18', end: '2016-02-02' },
|
||||
spring2016: { start: '2016-03-18', end: '2016-05-02' },
|
||||
summer2016: { start: '2016-06-21', end: '2016-08-02' },
|
||||
fall2016: { start: '2016-09-20', end: '2016-11-02' },
|
||||
winter2017: { start: '2016-12-16', end: '2017-02-02' },
|
||||
spring2017: { start: '2017-03-21', end: '2017-05-02' },
|
||||
summer2017: { start: '2017-06-20', end: '2017-08-02' },
|
||||
fall2017: { start: '2017-09-21', end: '2017-11-02' },
|
||||
winter2018: { start: '2017-12-19', end: '2018-02-02' },
|
||||
spring2018: { start: '2018-03-20', end: '2018-05-02' },
|
||||
summer2018: { start: '2018-06-19', end: '2018-08-02' },
|
||||
fall2018: { start: '2018-09-20', end: '2018-11-02' },
|
||||
winter2019: { start: '2018-12-19', end: '2019-02-02' },
|
||||
spring2019: { start: '2019-03-19', end: '2019-05-02' },
|
||||
summer2019: { start: '2019-06-18', end: '2019-08-02' },
|
||||
fall2019: { start: '2019-09-24', end: '2019-11-02' },
|
||||
winter2020: { start: '2019-12-19', end: '2020-02-02' },
|
||||
spring2020: { start: '2020-03-17', end: '2020-05-02' },
|
||||
summer2020: { start: '2020-06-18', end: '2020-08-02' },
|
||||
};
|
||||
|
||||
export const SEASONAL_SETS = {
|
||||
winter: [
|
||||
// winter 2014
|
||||
'candycaneSet',
|
||||
'skiSet',
|
||||
'snowflakeSet',
|
||||
'yetiSet',
|
||||
|
||||
// winter 2015
|
||||
'northMageSet',
|
||||
'icicleDrakeSet',
|
||||
'soothingSkaterSet',
|
||||
'gingerbreadSet',
|
||||
|
||||
// winter 2016
|
||||
'snowDaySet',
|
||||
'snowboardingSet',
|
||||
'festiveFairySet',
|
||||
'cocoaSet',
|
||||
|
||||
// winter 2017
|
||||
'winter2017IceHockeySet',
|
||||
'winter2017WinterWolfSet',
|
||||
'winter2017SugarPlumSet',
|
||||
'winter2017FrostyRogueSet',
|
||||
|
||||
// winter 2018
|
||||
'winter2018ConfettiSet',
|
||||
'winter2018GiftWrappedSet',
|
||||
'winter2018MistletoeSet',
|
||||
'winter2018ReindeerSet',
|
||||
|
||||
// winter 2019
|
||||
'winter2019PoinsettiaSet',
|
||||
'winter2019WinterStarSet',
|
||||
'winter2019BlizzardSet',
|
||||
'winter2019PyrotechnicSet',
|
||||
|
||||
// winter 2020
|
||||
'winter2020CarolOfTheMageSet',
|
||||
'winter2020LanternSet',
|
||||
'winter2020EvergreenSet',
|
||||
'winter2020WinterSpiceSet',
|
||||
],
|
||||
spring: [
|
||||
// spring 2014
|
||||
'mightyBunnySet',
|
||||
'magicMouseSet',
|
||||
'lovingPupSet',
|
||||
'stealthyKittySet',
|
||||
|
||||
// spring 2015
|
||||
'bewareDogSet',
|
||||
'magicianBunnySet',
|
||||
'comfortingKittySet',
|
||||
'sneakySqueakerSet',
|
||||
|
||||
// spring 2016
|
||||
'springingBunnySet',
|
||||
'grandMalkinSet',
|
||||
'cleverDogSet',
|
||||
'braveMouseSet',
|
||||
|
||||
// spring 2017
|
||||
'spring2017FelineWarriorSet',
|
||||
'spring2017CanineConjurorSet',
|
||||
'spring2017FloralMouseSet',
|
||||
'spring2017SneakyBunnySet',
|
||||
|
||||
// spring 2018
|
||||
'spring2018TulipMageSet',
|
||||
'spring2018SunriseWarriorSet',
|
||||
'spring2018DucklingRogueSet',
|
||||
'spring2018GarnetHealerSet',
|
||||
|
||||
// spring 2019
|
||||
'spring2019AmberMageSet',
|
||||
'spring2019OrchidWarriorSet',
|
||||
'spring2019CloudRogueSet',
|
||||
'spring2019RobinHealerSet',
|
||||
|
||||
// spring 2020
|
||||
|
||||
'spring2020BeetleWarriorSet',
|
||||
'spring2020IrisHealerSet',
|
||||
'spring2020LapisLazuliRogueSet',
|
||||
'spring2020PuddleMageSet',
|
||||
],
|
||||
summer: [
|
||||
// summer 2014
|
||||
'daringSwashbucklerSet',
|
||||
'emeraldMermageSet',
|
||||
'reefSeahealerSet',
|
||||
'roguishPirateSet',
|
||||
|
||||
// summer 2015
|
||||
'sunfishWarriorSet',
|
||||
'shipSoothsayerSet',
|
||||
'strappingSailorSet',
|
||||
'reefRenegadeSet',
|
||||
|
||||
// summer 2016
|
||||
'summer2016SharkWarriorSet',
|
||||
'summer2016DolphinMageSet',
|
||||
'summer2016SeahorseHealerSet',
|
||||
'summer2016EelSet',
|
||||
|
||||
// summer 2017
|
||||
'summer2017SandcastleWarriorSet',
|
||||
'summer2017WhirlpoolMageSet',
|
||||
'summer2017SeashellSeahealerSet',
|
||||
'summer2017SeaDragonSet',
|
||||
|
||||
// summer 2018
|
||||
'summer2018BettaFishWarriorSet',
|
||||
'summer2018LionfishMageSet',
|
||||
'summer2018MerfolkMonarchSet',
|
||||
'summer2018FisherRogueSet',
|
||||
|
||||
// summer 2019
|
||||
'summer2019SeaTurtleWarriorSet',
|
||||
'summer2019WaterLilyMageSet',
|
||||
'summer2019ConchHealerSet',
|
||||
'summer2019HammerheadRogueSet',
|
||||
|
||||
// summer 2020
|
||||
'summer2020SeaGlassHealerSet',
|
||||
'summer2020OarfishMageSet',
|
||||
'summer2020CrocodileRogueSet',
|
||||
'summer2020RainbowTroutWarriorSet',
|
||||
],
|
||||
fall: [
|
||||
// fall 2014
|
||||
'vampireSmiterSet',
|
||||
'monsterOfScienceSet',
|
||||
'witchyWizardSet',
|
||||
'mummyMedicSet',
|
||||
|
||||
// fall 2015
|
||||
'battleRogueSet',
|
||||
'scarecrowWarriorSet',
|
||||
'stitchWitchSet',
|
||||
'potionerSet',
|
||||
|
||||
// fall 2016
|
||||
'fall2016BlackWidowSet',
|
||||
'fall2016SwampThingSet',
|
||||
'fall2016WickedSorcererSet',
|
||||
'fall2016GorgonHealerSet',
|
||||
|
||||
// fall 2017
|
||||
'fall2017TrickOrTreatSet',
|
||||
'fall2017HabitoweenSet',
|
||||
'fall2017MasqueradeSet',
|
||||
'fall2017HauntedHouseSet',
|
||||
|
||||
// fall 2018
|
||||
'fall2018MinotaurWarriorSet',
|
||||
'fall2018CandymancerMageSet',
|
||||
'fall2018CarnivorousPlantSet',
|
||||
'fall2018AlterEgoSet',
|
||||
|
||||
// fall 2019
|
||||
'fall2019CyclopsSet',
|
||||
'fall2019LichSet',
|
||||
'fall2019OperaticSpecterSet',
|
||||
'fall2019RavenSet',
|
||||
],
|
||||
};
|
||||
|
||||
export const GEAR_TYPES = [
|
||||
'weapon',
|
||||
'armor',
|
||||
'head',
|
||||
'shield',
|
||||
'body',
|
||||
'back',
|
||||
'headAccessory',
|
||||
'eyewear',
|
||||
];
|
||||
|
||||
export const ITEM_LIST = {
|
||||
weapon: { localeKey: 'weapon', isEquipment: true },
|
||||
armor: { localeKey: 'armor', isEquipment: true },
|
||||
head: { localeKey: 'headgear', isEquipment: true },
|
||||
shield: { localeKey: 'offhand', isEquipment: true },
|
||||
back: { localeKey: 'back', isEquipment: true },
|
||||
body: { localeKey: 'body', isEquipment: true },
|
||||
headAccessory: { localeKey: 'headAccessory', isEquipment: true },
|
||||
eyewear: { localeKey: 'eyewear', isEquipment: true },
|
||||
hatchingPotions: { localeKey: 'hatchingPotion', isEquipment: false },
|
||||
premiumHatchingPotions: { localeKey: 'hatchingPotion', isEquipment: false },
|
||||
eggs: { localeKey: 'eggSingular', isEquipment: false },
|
||||
quests: { localeKey: 'quest', isEquipment: false },
|
||||
food: { localeKey: 'foodTextThe', isEquipment: false },
|
||||
Saddle: { localeKey: 'foodSaddleText', isEquipment: false },
|
||||
bundles: { localeKey: 'discountBundle', isEquipment: false },
|
||||
};
|
||||
|
||||
export const USER_CAN_OWN_QUEST_CATEGORIES = [
|
||||
'unlockable',
|
||||
'gold',
|
||||
'hatchingPotion',
|
||||
'pet',
|
||||
];
|
||||
|
||||
export const QUEST_SERIES_ACHIEVEMENTS = {
|
||||
lostMasterclasser: [
|
||||
'dilatoryDistress1',
|
||||
'dilatoryDistress2',
|
||||
'dilatoryDistress3',
|
||||
'mayhemMistiflying1',
|
||||
'mayhemMistiflying2',
|
||||
'mayhemMistiflying3',
|
||||
'stoikalmCalamity1',
|
||||
'stoikalmCalamity2',
|
||||
'stoikalmCalamity3',
|
||||
'taskwoodsTerror1',
|
||||
'taskwoodsTerror2',
|
||||
'taskwoodsTerror3',
|
||||
'lostMasterclasser1',
|
||||
'lostMasterclasser2',
|
||||
'lostMasterclasser3',
|
||||
'lostMasterclasser4',
|
||||
],
|
||||
mindOverMatter: [
|
||||
'rock',
|
||||
'slime',
|
||||
'yarn',
|
||||
],
|
||||
justAddWater: [
|
||||
'octopus',
|
||||
'dilatory_derby',
|
||||
'kraken',
|
||||
'whale',
|
||||
'turtle',
|
||||
'nudibranch',
|
||||
'seaserpent',
|
||||
'dolphin',
|
||||
],
|
||||
bugBonanza: [
|
||||
'beetle',
|
||||
'butterfly',
|
||||
'snail',
|
||||
'spider',
|
||||
],
|
||||
bareNecessities: [
|
||||
'monkey',
|
||||
'sloth',
|
||||
'treeling',
|
||||
],
|
||||
freshwaterFriends: [
|
||||
'axolotl',
|
||||
'frog',
|
||||
'hippo',
|
||||
],
|
||||
};
|
||||
|
||||
export const ANIMAL_COLOR_ACHIEVEMENTS = [
|
||||
{
|
||||
color: 'Base',
|
||||
petAchievement: 'backToBasics',
|
||||
petNotificationType: 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
mountAchievement: 'allYourBase',
|
||||
mountNotificationType: 'ACHIEVEMENT_ALL_YOUR_BASE',
|
||||
},
|
||||
{
|
||||
color: 'Desert',
|
||||
petAchievement: 'dustDevil',
|
||||
petNotificationType: 'ACHIEVEMENT_DUST_DEVIL',
|
||||
mountAchievement: 'aridAuthority',
|
||||
mountNotificationType: 'ACHIEVEMENT_ARID_AUTHORITY',
|
||||
},
|
||||
{
|
||||
color: 'Zombie',
|
||||
petAchievement: 'monsterMagus',
|
||||
petNotificationType: 'ACHIEVEMENT_MONSTER_MAGUS',
|
||||
mountAchievement: 'undeadUndertaker',
|
||||
mountNotificationType: 'ACHIEVEMENT_UNDEAD_UNDERTAKER',
|
||||
},
|
||||
{
|
||||
color: 'White',
|
||||
petAchievement: 'primedForPainting',
|
||||
petNotificationType: 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
|
||||
mountAchievement: 'pearlyPro',
|
||||
mountNotificationType: 'ACHIEVEMENT_PEARLY_PRO',
|
||||
},
|
||||
{
|
||||
color: 'CottonCandyPink',
|
||||
petAchievement: 'tickledPink',
|
||||
petNotificationType: 'ACHIEVEMENT_TICKLED_PINK',
|
||||
mountAchievement: 'rosyOutlook',
|
||||
mountNotificationType: 'ACHIEVEMENT_ROSY_OUTLOOK',
|
||||
},
|
||||
{
|
||||
color: 'Golden',
|
||||
petAchievement: 'goodAsGold',
|
||||
petNotificationType: 'ACHIEVEMENT_GOOD_AS_GOLD',
|
||||
mountAchievement: 'allThatGlitters',
|
||||
mountNotificationType: 'ACHIEVEMENT_ALL_THAT_GLITTERS',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,46 @@
|
||||
const ANIMAL_COLOR_ACHIEVEMENTS = [
|
||||
{
|
||||
color: 'Base',
|
||||
petAchievement: 'backToBasics',
|
||||
petNotificationType: 'ACHIEVEMENT_BACK_TO_BASICS',
|
||||
mountAchievement: 'allYourBase',
|
||||
mountNotificationType: 'ACHIEVEMENT_ALL_YOUR_BASE',
|
||||
},
|
||||
{
|
||||
color: 'Desert',
|
||||
petAchievement: 'dustDevil',
|
||||
petNotificationType: 'ACHIEVEMENT_DUST_DEVIL',
|
||||
mountAchievement: 'aridAuthority',
|
||||
mountNotificationType: 'ACHIEVEMENT_ARID_AUTHORITY',
|
||||
},
|
||||
{
|
||||
color: 'Zombie',
|
||||
petAchievement: 'monsterMagus',
|
||||
petNotificationType: 'ACHIEVEMENT_MONSTER_MAGUS',
|
||||
mountAchievement: 'undeadUndertaker',
|
||||
mountNotificationType: 'ACHIEVEMENT_UNDEAD_UNDERTAKER',
|
||||
},
|
||||
{
|
||||
color: 'White',
|
||||
petAchievement: 'primedForPainting',
|
||||
petNotificationType: 'ACHIEVEMENT_PRIMED_FOR_PAINTING',
|
||||
mountAchievement: 'pearlyPro',
|
||||
mountNotificationType: 'ACHIEVEMENT_PEARLY_PRO',
|
||||
},
|
||||
{
|
||||
color: 'CottonCandyPink',
|
||||
petAchievement: 'tickledPink',
|
||||
petNotificationType: 'ACHIEVEMENT_TICKLED_PINK',
|
||||
mountAchievement: 'rosyOutlook',
|
||||
mountNotificationType: 'ACHIEVEMENT_ROSY_OUTLOOK',
|
||||
},
|
||||
{
|
||||
color: 'Golden',
|
||||
petAchievement: 'goodAsGold',
|
||||
petNotificationType: 'ACHIEVEMENT_GOOD_AS_GOLD',
|
||||
mountAchievement: 'allThatGlitters',
|
||||
mountNotificationType: 'ACHIEVEMENT_ALL_THAT_GLITTERS',
|
||||
},
|
||||
];
|
||||
|
||||
export default ANIMAL_COLOR_ACHIEVEMENTS;
|
||||
58
website/common/script/content/constants/events.js
Normal file
@@ -0,0 +1,58 @@
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
// IMPORTANT: The end date should be one to two days AFTER the actual end of
|
||||
// the event, to allow people in different timezones to still buy the
|
||||
// event gear up until at least the actual end of the event.
|
||||
// Unless we want a precise ending, for example during a gems sale.
|
||||
|
||||
// gem block: number of gems
|
||||
const gemsPromo = {
|
||||
'4gems': 5,
|
||||
'21gems': 30,
|
||||
'42gems': 60,
|
||||
'84gems': 125,
|
||||
};
|
||||
|
||||
export const EVENTS = {
|
||||
fall2020SecondPromo: {
|
||||
start: '2020-10-29T08:00-04:00',
|
||||
end: '2020-11-02T20:00-05:00',
|
||||
gemsPromo,
|
||||
},
|
||||
fall2020: {
|
||||
start: '2020-09-22T08:00-04:00',
|
||||
end: '2020-09-30T20:00-04:00',
|
||||
gemsPromo,
|
||||
},
|
||||
// Dates from this point on (^) are in the RFC 2822 format, see https://momentjs.com/docs/#/parsing/string/
|
||||
|
||||
summer2020: { start: '2020-06-18', end: '2020-08-02' },
|
||||
spring2020: { start: '2020-03-17', end: '2020-05-02' },
|
||||
winter2020: { start: '2019-12-19', end: '2020-02-02' },
|
||||
fall2019: { start: '2019-09-24', end: '2019-11-02' },
|
||||
summer2019: { start: '2019-06-18', end: '2019-08-02' },
|
||||
spring2019: { start: '2019-03-19', end: '2019-05-02' },
|
||||
winter2019: { start: '2018-12-19', end: '2019-02-02' },
|
||||
fall2018: { start: '2018-09-20', end: '2018-11-02' },
|
||||
summer2018: { start: '2018-06-19', end: '2018-08-02' },
|
||||
spring2018: { start: '2018-03-20', end: '2018-05-02' },
|
||||
winter2018: { start: '2017-12-19', end: '2018-02-02' },
|
||||
fall2017: { start: '2017-09-21', end: '2017-11-02' },
|
||||
summer2017: { start: '2017-06-20', end: '2017-08-02' },
|
||||
spring2017: { start: '2017-03-21', end: '2017-05-02' },
|
||||
winter2017: { start: '2016-12-16', end: '2017-02-02' },
|
||||
fall2016: { start: '2016-09-20', end: '2016-11-02' },
|
||||
summer2016: { start: '2016-06-21', end: '2016-08-02' },
|
||||
spring2016: { start: '2016-03-18', end: '2016-05-02' },
|
||||
winter2016: { start: '2015-12-18', end: '2016-02-02' },
|
||||
gaymerx: { start: '2016-09-29', end: '2016-10-03' },
|
||||
fall2015: { start: '2015-09-21', end: '2015-11-01' },
|
||||
summer2015: { start: '2015-06-20', end: '2015-08-02' },
|
||||
spring2015: { start: '2015-03-20', end: '2015-05-02' },
|
||||
winter2015: { start: '2014-12-21', end: '2015-02-02' },
|
||||
fall: { start: '2014-09-21', end: '2014-11-01' },
|
||||
summer: { start: '2014-06-20', end: '2014-08-01' },
|
||||
spring: { start: '2014-03-21', end: '2014-05-01' },
|
||||
birthday: { start: '2017-01-31', end: '2017-02-02' },
|
||||
winter: { start: '2013-12-31', end: '2014-02-01' },
|
||||
};
|
||||
34
website/common/script/content/constants/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import moment from 'moment';
|
||||
|
||||
export const CURRENT_SEASON = moment().isBefore('2020-08-02') ? 'summer' : '_NONE_';
|
||||
|
||||
export const CLASSES = [
|
||||
'warrior',
|
||||
'rogue',
|
||||
'healer',
|
||||
'wizard',
|
||||
];
|
||||
|
||||
export const GEAR_TYPES = [
|
||||
'weapon',
|
||||
'armor',
|
||||
'head',
|
||||
'shield',
|
||||
'body',
|
||||
'back',
|
||||
'headAccessory',
|
||||
'eyewear',
|
||||
];
|
||||
|
||||
export const USER_CAN_OWN_QUEST_CATEGORIES = [
|
||||
'unlockable',
|
||||
'gold',
|
||||
'hatchingPotion',
|
||||
'pet',
|
||||
];
|
||||
|
||||
export { EVENTS } from './events';
|
||||
export { default as SEASONAL_SETS } from './seasonalSets';
|
||||
export { default as ANIMAL_COLOR_ACHIEVEMENTS } from './animalColorAchievements';
|
||||
export { default as QUEST_SERIES_ACHIEVEMENTS } from './questSeriesAchievements';
|
||||
export { default as ITEM_LIST } from './itemList';
|
||||
21
website/common/script/content/constants/itemList.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/* eslint-disable key-spacing */
|
||||
|
||||
const ITEM_LIST = {
|
||||
weapon: { localeKey: 'weapon', isEquipment: true },
|
||||
armor: { localeKey: 'armor', isEquipment: true },
|
||||
head: { localeKey: 'headgear', isEquipment: true },
|
||||
shield: { localeKey: 'offhand', isEquipment: true },
|
||||
back: { localeKey: 'back', isEquipment: true },
|
||||
body: { localeKey: 'body', isEquipment: true },
|
||||
headAccessory: { localeKey: 'headAccessory', isEquipment: true },
|
||||
eyewear: { localeKey: 'eyewear', isEquipment: true },
|
||||
hatchingPotions: { localeKey: 'hatchingPotion', isEquipment: false },
|
||||
premiumHatchingPotions: { localeKey: 'hatchingPotion', isEquipment: false },
|
||||
eggs: { localeKey: 'eggSingular', isEquipment: false },
|
||||
quests: { localeKey: 'quest', isEquipment: false },
|
||||
food: { localeKey: 'foodTextThe', isEquipment: false },
|
||||
Saddle: { localeKey: 'foodSaddleText', isEquipment: false },
|
||||
bundles: { localeKey: 'discountBundle', isEquipment: false },
|
||||
};
|
||||
|
||||
export default ITEM_LIST;
|
||||
@@ -0,0 +1,53 @@
|
||||
const QUEST_SERIES_ACHIEVEMENTS = {
|
||||
lostMasterclasser: [
|
||||
'dilatoryDistress1',
|
||||
'dilatoryDistress2',
|
||||
'dilatoryDistress3',
|
||||
'mayhemMistiflying1',
|
||||
'mayhemMistiflying2',
|
||||
'mayhemMistiflying3',
|
||||
'stoikalmCalamity1',
|
||||
'stoikalmCalamity2',
|
||||
'stoikalmCalamity3',
|
||||
'taskwoodsTerror1',
|
||||
'taskwoodsTerror2',
|
||||
'taskwoodsTerror3',
|
||||
'lostMasterclasser1',
|
||||
'lostMasterclasser2',
|
||||
'lostMasterclasser3',
|
||||
'lostMasterclasser4',
|
||||
],
|
||||
mindOverMatter: [
|
||||
'rock',
|
||||
'slime',
|
||||
'yarn',
|
||||
],
|
||||
justAddWater: [
|
||||
'octopus',
|
||||
'dilatory_derby',
|
||||
'kraken',
|
||||
'whale',
|
||||
'turtle',
|
||||
'nudibranch',
|
||||
'seaserpent',
|
||||
'dolphin',
|
||||
],
|
||||
bugBonanza: [
|
||||
'beetle',
|
||||
'butterfly',
|
||||
'snail',
|
||||
'spider',
|
||||
],
|
||||
bareNecessities: [
|
||||
'monkey',
|
||||
'sloth',
|
||||
'treeling',
|
||||
],
|
||||
freshwaterFriends: [
|
||||
'axolotl',
|
||||
'frog',
|
||||
'hippo',
|
||||
],
|
||||
};
|
||||
|
||||
export default QUEST_SERIES_ACHIEVEMENTS;
|
||||
171
website/common/script/content/constants/seasonalSets.js
Normal file
@@ -0,0 +1,171 @@
|
||||
const SEASONAL_SETS = {
|
||||
winter: [
|
||||
// winter 2014
|
||||
'candycaneSet',
|
||||
'skiSet',
|
||||
'snowflakeSet',
|
||||
'yetiSet',
|
||||
|
||||
// winter 2015
|
||||
'northMageSet',
|
||||
'icicleDrakeSet',
|
||||
'soothingSkaterSet',
|
||||
'gingerbreadSet',
|
||||
|
||||
// winter 2016
|
||||
'snowDaySet',
|
||||
'snowboardingSet',
|
||||
'festiveFairySet',
|
||||
'cocoaSet',
|
||||
|
||||
// winter 2017
|
||||
'winter2017IceHockeySet',
|
||||
'winter2017WinterWolfSet',
|
||||
'winter2017SugarPlumSet',
|
||||
'winter2017FrostyRogueSet',
|
||||
|
||||
// winter 2018
|
||||
'winter2018ConfettiSet',
|
||||
'winter2018GiftWrappedSet',
|
||||
'winter2018MistletoeSet',
|
||||
'winter2018ReindeerSet',
|
||||
|
||||
// winter 2019
|
||||
'winter2019PoinsettiaSet',
|
||||
'winter2019WinterStarSet',
|
||||
'winter2019BlizzardSet',
|
||||
'winter2019PyrotechnicSet',
|
||||
|
||||
// winter 2020
|
||||
'winter2020CarolOfTheMageSet',
|
||||
'winter2020LanternSet',
|
||||
'winter2020EvergreenSet',
|
||||
'winter2020WinterSpiceSet',
|
||||
],
|
||||
spring: [
|
||||
// spring 2014
|
||||
'mightyBunnySet',
|
||||
'magicMouseSet',
|
||||
'lovingPupSet',
|
||||
'stealthyKittySet',
|
||||
|
||||
// spring 2015
|
||||
'bewareDogSet',
|
||||
'magicianBunnySet',
|
||||
'comfortingKittySet',
|
||||
'sneakySqueakerSet',
|
||||
|
||||
// spring 2016
|
||||
'springingBunnySet',
|
||||
'grandMalkinSet',
|
||||
'cleverDogSet',
|
||||
'braveMouseSet',
|
||||
|
||||
// spring 2017
|
||||
'spring2017FelineWarriorSet',
|
||||
'spring2017CanineConjurorSet',
|
||||
'spring2017FloralMouseSet',
|
||||
'spring2017SneakyBunnySet',
|
||||
|
||||
// spring 2018
|
||||
'spring2018TulipMageSet',
|
||||
'spring2018SunriseWarriorSet',
|
||||
'spring2018DucklingRogueSet',
|
||||
'spring2018GarnetHealerSet',
|
||||
|
||||
// spring 2019
|
||||
'spring2019AmberMageSet',
|
||||
'spring2019OrchidWarriorSet',
|
||||
'spring2019CloudRogueSet',
|
||||
'spring2019RobinHealerSet',
|
||||
|
||||
// spring 2020
|
||||
|
||||
'spring2020BeetleWarriorSet',
|
||||
'spring2020IrisHealerSet',
|
||||
'spring2020LapisLazuliRogueSet',
|
||||
'spring2020PuddleMageSet',
|
||||
],
|
||||
summer: [
|
||||
// summer 2014
|
||||
'daringSwashbucklerSet',
|
||||
'emeraldMermageSet',
|
||||
'reefSeahealerSet',
|
||||
'roguishPirateSet',
|
||||
|
||||
// summer 2015
|
||||
'sunfishWarriorSet',
|
||||
'shipSoothsayerSet',
|
||||
'strappingSailorSet',
|
||||
'reefRenegadeSet',
|
||||
|
||||
// summer 2016
|
||||
'summer2016SharkWarriorSet',
|
||||
'summer2016DolphinMageSet',
|
||||
'summer2016SeahorseHealerSet',
|
||||
'summer2016EelSet',
|
||||
|
||||
// summer 2017
|
||||
'summer2017SandcastleWarriorSet',
|
||||
'summer2017WhirlpoolMageSet',
|
||||
'summer2017SeashellSeahealerSet',
|
||||
'summer2017SeaDragonSet',
|
||||
|
||||
// summer 2018
|
||||
'summer2018BettaFishWarriorSet',
|
||||
'summer2018LionfishMageSet',
|
||||
'summer2018MerfolkMonarchSet',
|
||||
'summer2018FisherRogueSet',
|
||||
|
||||
// summer 2019
|
||||
'summer2019SeaTurtleWarriorSet',
|
||||
'summer2019WaterLilyMageSet',
|
||||
'summer2019ConchHealerSet',
|
||||
'summer2019HammerheadRogueSet',
|
||||
|
||||
// summer 2020
|
||||
'summer2020SeaGlassHealerSet',
|
||||
'summer2020OarfishMageSet',
|
||||
'summer2020CrocodileRogueSet',
|
||||
'summer2020RainbowTroutWarriorSet',
|
||||
],
|
||||
fall: [
|
||||
// fall 2014
|
||||
'vampireSmiterSet',
|
||||
'monsterOfScienceSet',
|
||||
'witchyWizardSet',
|
||||
'mummyMedicSet',
|
||||
|
||||
// fall 2015
|
||||
'battleRogueSet',
|
||||
'scarecrowWarriorSet',
|
||||
'stitchWitchSet',
|
||||
'potionerSet',
|
||||
|
||||
// fall 2016
|
||||
'fall2016BlackWidowSet',
|
||||
'fall2016SwampThingSet',
|
||||
'fall2016WickedSorcererSet',
|
||||
'fall2016GorgonHealerSet',
|
||||
|
||||
// fall 2017
|
||||
'fall2017TrickOrTreatSet',
|
||||
'fall2017HabitoweenSet',
|
||||
'fall2017MasqueradeSet',
|
||||
'fall2017HauntedHouseSet',
|
||||
|
||||
// fall 2018
|
||||
'fall2018MinotaurWarriorSet',
|
||||
'fall2018CandymancerMageSet',
|
||||
'fall2018CarnivorousPlantSet',
|
||||
'fall2018AlterEgoSet',
|
||||
|
||||
// fall 2019
|
||||
'fall2019CyclopsSet',
|
||||
'fall2019LichSet',
|
||||
'fall2019OperaticSpecterSet',
|
||||
'fall2019RavenSet',
|
||||
],
|
||||
};
|
||||
|
||||
export default SEASONAL_SETS;
|
||||
40
website/common/script/content/gems.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const blocks = {
|
||||
'4gems': {
|
||||
gems: 4,
|
||||
iosProducts: ['com.habitrpg.ios.Habitica.4gems'],
|
||||
androidProducts: ['com.habitrpg.android.habitica.iap.4gems'],
|
||||
price: 99, // in cents, web only
|
||||
},
|
||||
'21gems': {
|
||||
gems: 21,
|
||||
iosProducts: [
|
||||
'com.habitrpg.ios.Habitica.20gems',
|
||||
'com.habitrpg.ios.Habitica.21gems',
|
||||
],
|
||||
androidProducts: [
|
||||
'com.habitrpg.android.habitica.iap.20.gems',
|
||||
'com.habitrpg.android.habitica.iap.21.gems',
|
||||
],
|
||||
price: 499, // in cents, web only
|
||||
},
|
||||
'42gems': {
|
||||
gems: 42,
|
||||
iosProducts: ['com.habitrpg.ios.Habitica.42gems'],
|
||||
androidProducts: ['com.habitrpg.android.habitica.iap.42gems'],
|
||||
price: 999, // in cents, web only
|
||||
},
|
||||
'84gems': {
|
||||
gems: 84,
|
||||
iosProducts: ['com.habitrpg.ios.Habitica.84gems'],
|
||||
androidProducts: ['com.habitrpg.android.habitica.iap.84gems'],
|
||||
price: 1999, // in cents, web only
|
||||
},
|
||||
};
|
||||
|
||||
// Add the block key to all blocks
|
||||
Object.keys(blocks).forEach(blockKey => {
|
||||
const block = blocks[blockKey];
|
||||
block.key = blockKey;
|
||||
});
|
||||
|
||||
export default blocks;
|
||||
@@ -5,6 +5,7 @@ import t from './translation';
|
||||
import { tasksByCategory } from './tasks';
|
||||
|
||||
import {
|
||||
EVENTS,
|
||||
CLASSES,
|
||||
GEAR_TYPES,
|
||||
ITEM_LIST,
|
||||
@@ -29,6 +30,7 @@ import { backgroundsTree, backgroundsFlat } from './appearance/backgrounds';
|
||||
import bundles from './bundles';
|
||||
import spells from './spells'; // eslint-disable-line import/no-cycle
|
||||
import subscriptionBlocks from './subscriptionBlocks';
|
||||
import gemsBlock from './gems';
|
||||
import faq from './faq';
|
||||
import timeTravelers from './time-travelers';
|
||||
|
||||
@@ -51,6 +53,7 @@ api.itemList = ITEM_LIST;
|
||||
api.gear = gear;
|
||||
api.spells = spells;
|
||||
api.subscriptionBlocks = subscriptionBlocks;
|
||||
api.gems = gemsBlock;
|
||||
|
||||
api.audioThemes = ['danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme', 'rosstavoTheme', 'dewinTheme', 'airuTheme', 'beatscribeNesTheme', 'arashiTheme', 'maflTheme', 'pizildenTheme', 'farvoidTheme', 'spacePenguinTheme', 'lunasolTheme', 'triumphTheme'];
|
||||
|
||||
@@ -91,20 +94,10 @@ api.armoire = {
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------
|
||||
Classes
|
||||
---------------------------------------------------------------
|
||||
*/
|
||||
api.events = EVENTS;
|
||||
|
||||
api.classes = CLASSES;
|
||||
|
||||
/*
|
||||
---------------------------------------------------------------
|
||||
Gear Types
|
||||
---------------------------------------------------------------
|
||||
*/
|
||||
|
||||
api.gearTypes = GEAR_TYPES;
|
||||
|
||||
api.cardTypes = {
|
||||
@@ -192,7 +185,6 @@ api.specialMounts = stable.specialMounts;
|
||||
api.mountInfo = stable.mountInfo;
|
||||
|
||||
// For seasonal events, change this constant:
|
||||
|
||||
const FOOD_SEASON = 'Normal';
|
||||
|
||||
api.food = {
|
||||
|
||||
@@ -25,6 +25,7 @@ export default {
|
||||
missingCustomerId: 'Missing "req.query.customerId"',
|
||||
missingPaypalBlock: 'Missing "req.session.paypalBlock"',
|
||||
missingSubKey: 'Missing "req.query.sub"',
|
||||
invalidGemsBlock: 'The supplied gemsBlock does not exists',
|
||||
|
||||
ipAddressBlocked: 'Your access to Habitica has been blocked. This may be due to a breach of our Terms of Service or for other reasons. For details or to ask to be unblocked, please email admin@habitica.com or ask your parent or guardian to email them. Include your Habitica @Username or User Id in the email if you know it.',
|
||||
clientRateLimited: 'This User ID or IP address has been rate limited due to an excess amount of requests to the Habitica API v3. More info can be found in the response headers and at https://habitica.fandom.com/wiki/Guidance_for_Comrades#Rules_for_Third-Party_Tools under the section Rate Limiting.',
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import {
|
||||
model as Group,
|
||||
TAVERN_ID as tavernId,
|
||||
} from '../../models/group';
|
||||
getCurrentEvent,
|
||||
getWorldBoss,
|
||||
} from '../../libs/worldState';
|
||||
|
||||
const api = {};
|
||||
|
||||
async function getWorldBoss () {
|
||||
const tavern = await Group
|
||||
.findById(tavernId)
|
||||
.select('quest.progress quest.key quest.active quest.extra')
|
||||
.exec();
|
||||
if (tavern && tavern.quest && tavern.quest.active) {
|
||||
return tavern.quest;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/world-state Get the state for the game world
|
||||
* @apiDescription Does not require authentication.
|
||||
@@ -30,6 +19,7 @@ async function getWorldBoss () {
|
||||
* @apiSuccess {Object} data.worldBoss.progress.hp Number, Current Health of the world boss
|
||||
* @apiSuccess {Object} data.worldBoss.progress.rage Number, Current Rage of the world boss
|
||||
* @apiSuccess {Object} data.npcImageSuffix String, trailing component of NPC image filenames
|
||||
* @apiSuccess {Object} data.currentEvent The current active event
|
||||
*
|
||||
*/
|
||||
api.getWorldState = {
|
||||
@@ -41,6 +31,8 @@ api.getWorldState = {
|
||||
worldState.worldBoss = await getWorldBoss();
|
||||
worldState.npcImageSuffix = 'spring';
|
||||
|
||||
worldState.currentEvent = getCurrentEvent();
|
||||
|
||||
res.respond(200, worldState);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,14 +74,13 @@ api.checkout = {
|
||||
url: '/amazon/checkout',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
const { gift } = req.body;
|
||||
const { user } = res.locals;
|
||||
const { orderReferenceId } = req.body;
|
||||
const { orderReferenceId, gift, gemsBlock } = req.body;
|
||||
|
||||
if (!orderReferenceId) throw new BadRequest('Missing req.body.orderReferenceId');
|
||||
|
||||
await amzLib.checkout({
|
||||
gift, user, orderReferenceId, headers: req.headers,
|
||||
gemsBlock, gift, user, orderReferenceId, headers: req.headers,
|
||||
});
|
||||
|
||||
res.respond(200);
|
||||
|
||||
@@ -27,7 +27,10 @@ api.checkout = {
|
||||
const gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
req.session.gift = req.query.gift;
|
||||
|
||||
const link = await paypalPayments.checkout({ gift, user: res.locals.user });
|
||||
const { gemsBlock } = req.query;
|
||||
req.session.gemsBlock = gemsBlock;
|
||||
|
||||
const link = await paypalPayments.checkout({ gift, gemsBlock, user: res.locals.user });
|
||||
|
||||
if (req.query.noRedirect) {
|
||||
res.respond(200);
|
||||
@@ -53,12 +56,14 @@ api.checkoutSuccess = {
|
||||
const { user } = res.locals;
|
||||
const gift = req.session.gift ? JSON.parse(req.session.gift) : undefined;
|
||||
delete req.session.gift;
|
||||
const { gemsBlock } = req.session;
|
||||
delete req.session.gemsBlock;
|
||||
|
||||
if (!paymentId) throw new BadRequest(apiError('missingPaymentId'));
|
||||
if (!customerId) throw new BadRequest(apiError('missingCustomerId'));
|
||||
|
||||
await paypalPayments.checkoutSuccess({
|
||||
user, gift, paymentId, customerId,
|
||||
user, gemsBlock, gift, paymentId, customerId,
|
||||
});
|
||||
|
||||
if (req.query.noRedirect) {
|
||||
|
||||
@@ -32,11 +32,10 @@ api.checkout = {
|
||||
const { user } = res.locals;
|
||||
const gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
||||
const sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
||||
const { groupId } = req.query;
|
||||
const { coupon } = req.query;
|
||||
const { groupId, coupon, gemsBlock } = req.query;
|
||||
|
||||
await stripePayments.checkout({
|
||||
token, user, gift, sub, groupId, coupon,
|
||||
token, user, gemsBlock, gift, sub, groupId, coupon,
|
||||
});
|
||||
|
||||
res.respond(200, {});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { // eslint-disable-line import/no-cycle
|
||||
basicFields as basicGroupFields,
|
||||
} from '../../models/group';
|
||||
import { model as Coupon } from '../../models/coupon';
|
||||
import { getGemsBlock } from './gems'; // eslint-disable-line import/no-cycle
|
||||
|
||||
// TODO better handling of errors
|
||||
|
||||
@@ -109,9 +110,10 @@ api.authorize = function authorize (inputSet) {
|
||||
*/
|
||||
api.checkout = async function checkout (options = {}) {
|
||||
const {
|
||||
gift, user, orderReferenceId, headers,
|
||||
gift, user, orderReferenceId, headers, gemsBlock: gemsBlockKey,
|
||||
} = options;
|
||||
let amount = 5;
|
||||
let amount;
|
||||
let gemsBlock;
|
||||
|
||||
if (gift) {
|
||||
gift.member = await User.findById(gift.uuid).exec();
|
||||
@@ -124,6 +126,9 @@ api.checkout = async function checkout (options = {}) {
|
||||
} else if (gift.type === this.constants.GIFT_TYPE_SUBSCRIPTION) {
|
||||
amount = common.content.subscriptionBlocks[gift.subscription.key].price;
|
||||
}
|
||||
} else {
|
||||
gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
amount = gemsBlock.price / 100;
|
||||
}
|
||||
|
||||
if (!gift || gift.type === this.constants.GIFT_TYPE_GEMS) {
|
||||
@@ -170,6 +175,7 @@ api.checkout = async function checkout (options = {}) {
|
||||
user,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD,
|
||||
headers,
|
||||
gemsBlock,
|
||||
};
|
||||
|
||||
if (gift) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import moment from 'moment';
|
||||
import shared from '../../../common';
|
||||
import iap from '../inAppPurchases';
|
||||
import payments from './payments';
|
||||
import { getGemsBlock } from './gems';
|
||||
import {
|
||||
NotAuthorized,
|
||||
BadRequest,
|
||||
@@ -29,6 +30,7 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
if (gift) {
|
||||
gift.member = await User.findById(gift.uuid).exec();
|
||||
}
|
||||
|
||||
const receiver = gift ? gift.member : user;
|
||||
const receiverCanGetGems = await receiver.canGetGems();
|
||||
if (!receiverCanGetGems) throw new NotAuthorized(shared.i18n.t('groupPolicyCannotGetGems', user.preferences.language));
|
||||
@@ -59,28 +61,38 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
let amount;
|
||||
let gemsBlockKey;
|
||||
switch (purchaseData.productId) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.ios.Habitica.4gems':
|
||||
amount = 1;
|
||||
gemsBlockKey = '4gems';
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.20gems':
|
||||
case 'com.habitrpg.ios.Habitica.21gems':
|
||||
amount = 5.25;
|
||||
gemsBlockKey = '21gems';
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.42gems':
|
||||
amount = 10.5;
|
||||
gemsBlockKey = '42gems';
|
||||
break;
|
||||
case 'com.habitrpg.ios.Habitica.84gems':
|
||||
amount = 21;
|
||||
gemsBlockKey = '84gems';
|
||||
break;
|
||||
}
|
||||
if (amount) {
|
||||
if (!gemsBlockKey) throw new NotAuthorized(api.constants.RESPONSE_INVALID_ITEM);
|
||||
const gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
|
||||
if (gift) {
|
||||
gift.type = 'gems';
|
||||
if (!gift.gems) gift.gems = {};
|
||||
gift.gems.amount = shared.content.gems[gemsBlock.key].gems;
|
||||
}
|
||||
|
||||
if (gemsBlock) {
|
||||
correctReceipt = true;
|
||||
await payments.buyGems({ // eslint-disable-line no-await-in-loop
|
||||
user: receiver,
|
||||
user,
|
||||
gift,
|
||||
paymentMethod: api.constants.PAYMENT_METHOD_APPLE,
|
||||
amount,
|
||||
gemsBlock,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import * as analytics from '../analyticsService';
|
||||
import { getCurrentEvent } from '../worldState'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from '../email';
|
||||
import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle
|
||||
import shared from '../../../common';
|
||||
import {
|
||||
BadRequest,
|
||||
} from '../errors';
|
||||
import apiError from '../apiError';
|
||||
|
||||
function getGiftMessage (data, byUsername, gemAmount, language) {
|
||||
const senderMsg = shared.i18n.t('giftedGemsFull', {
|
||||
@@ -55,12 +60,25 @@ async function buyGemGift (data) {
|
||||
await data.gift.member.save();
|
||||
}
|
||||
|
||||
function getAmountForGems (data) {
|
||||
const amount = data.amount || 5;
|
||||
export function getGemsBlock (gemsBlock) {
|
||||
const block = shared.content.gems[gemsBlock];
|
||||
|
||||
if (!block) throw new BadRequest(apiError('invalidGemsBlock'));
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
function getAmountForGems (data) {
|
||||
if (data.gift) return data.gift.gems.amount / 4;
|
||||
|
||||
return amount;
|
||||
const { gemsBlock } = data;
|
||||
|
||||
const currentEvent = getCurrentEvent();
|
||||
if (currentEvent && currentEvent.gemsPromo && currentEvent.gemsPromo[gemsBlock.key]) {
|
||||
return currentEvent.gemsPromo[gemsBlock.key] / 4;
|
||||
}
|
||||
|
||||
return gemsBlock.gems / 4;
|
||||
}
|
||||
|
||||
function updateUserBalance (data, amount) {
|
||||
@@ -72,7 +90,7 @@ function updateUserBalance (data, amount) {
|
||||
data.user.balance += amount;
|
||||
}
|
||||
|
||||
async function buyGems (data) {
|
||||
export async function buyGems (data) {
|
||||
const amt = getAmountForGems(data);
|
||||
|
||||
updateUserBalance(data, amt);
|
||||
@@ -96,5 +114,3 @@ async function buyGems (data) {
|
||||
|
||||
await data.user.save();
|
||||
}
|
||||
|
||||
export { buyGems }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from '../errors';
|
||||
import { model as IapPurchaseReceipt } from '../../models/iapPurchaseReceipt';
|
||||
import { model as User } from '../../models/user';
|
||||
import { getGemsBlock } from './gems';
|
||||
|
||||
const api = {};
|
||||
|
||||
@@ -59,30 +60,39 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
userId: user._id,
|
||||
});
|
||||
|
||||
let amount;
|
||||
let gemsBlockKey;
|
||||
|
||||
switch (receiptObj.productId) { // eslint-disable-line default-case
|
||||
case 'com.habitrpg.android.habitica.iap.4gems':
|
||||
amount = 1;
|
||||
gemsBlockKey = '4gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.20.gems':
|
||||
case 'com.habitrpg.android.habitica.iap.20gems':
|
||||
case 'com.habitrpg.android.habitica.iap.21gems':
|
||||
amount = 5.25;
|
||||
gemsBlockKey = '21gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.42gems':
|
||||
amount = 10.5;
|
||||
gemsBlockKey = '42gems';
|
||||
break;
|
||||
case 'com.habitrpg.android.habitica.iap.84gems':
|
||||
amount = 21;
|
||||
gemsBlockKey = '84gems';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!amount) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
|
||||
if (!gemsBlockKey) throw new NotAuthorized(this.constants.RESPONSE_INVALID_ITEM);
|
||||
|
||||
const gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
|
||||
if (gift) {
|
||||
gift.type = 'gems';
|
||||
if (!gift.gems) gift.gems = {};
|
||||
gift.gems.amount = shared.content.gems[gemsBlock.key].gems;
|
||||
}
|
||||
|
||||
await payments.buyGems({
|
||||
user: receiver,
|
||||
user,
|
||||
gift,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_GOOGLE,
|
||||
amount,
|
||||
gemsBlock,
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import paypal from 'paypal-rest-sdk';
|
||||
import cc from 'coupon-code';
|
||||
import shared from '../../../common';
|
||||
import payments from './payments'; // eslint-disable-line import/no-cycle
|
||||
import { getGemsBlock } from './gems'; // eslint-disable-line import/no-cycle
|
||||
import { model as Coupon } from '../../models/coupon';
|
||||
import { model as User } from '../../models/user'; // eslint-disable-line import/no-cycle
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
@@ -76,9 +77,10 @@ api.paypalBillingAgreementCancel = util
|
||||
api.ipnVerifyAsync = util.promisify(paypalIpn.verify.bind(paypalIpn));
|
||||
|
||||
api.checkout = async function checkout (options = {}) {
|
||||
const { gift, user } = options;
|
||||
const { gift, user, gemsBlock: gemsBlockKey } = options;
|
||||
|
||||
let amount = 5.00;
|
||||
let amount;
|
||||
let gemsBlock;
|
||||
let description = 'Habitica Gems';
|
||||
|
||||
if (gift) {
|
||||
@@ -95,6 +97,9 @@ api.checkout = async function checkout (options = {}) {
|
||||
amount = Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
|
||||
description = 'mo. Habitica Subscription (Gift)';
|
||||
}
|
||||
} else {
|
||||
gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
amount = gemsBlock.price / 100;
|
||||
}
|
||||
|
||||
if (!gift || gift.type === 'gems') {
|
||||
@@ -139,7 +144,7 @@ api.checkout = async function checkout (options = {}) {
|
||||
|
||||
api.checkoutSuccess = async function checkoutSuccess (options = {}) {
|
||||
const {
|
||||
user, gift, paymentId, customerId,
|
||||
user, gift, gemsBlock: gemsBlockKey, paymentId, customerId,
|
||||
} = options;
|
||||
|
||||
let method = 'buyGems';
|
||||
@@ -157,6 +162,8 @@ api.checkoutSuccess = async function checkoutSuccess (options = {}) {
|
||||
|
||||
data.paymentMethod = 'PayPal (Gift)';
|
||||
data.gift = gift;
|
||||
} else {
|
||||
data.gemsBlock = getGemsBlock(gemsBlockKey);
|
||||
}
|
||||
|
||||
await this.paypalPaymentExecute(paymentId, { payer_id: customerId });
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../../errors';
|
||||
import payments from '../payments'; // eslint-disable-line import/no-cycle
|
||||
import { getGemsBlock } from '../gems'; // eslint-disable-line import/no-cycle
|
||||
import stripeConstants from './constants';
|
||||
|
||||
function getGiftAmount (gift) {
|
||||
@@ -27,10 +28,14 @@ function getGiftAmount (gift) {
|
||||
return `${(gift.gems.amount / 4) * 100}`;
|
||||
}
|
||||
|
||||
async function buyGems (gift, user, token, stripeApi) {
|
||||
let amount = 500; // $5
|
||||
async function buyGems (gemsBlock, gift, user, token, stripeApi) {
|
||||
let amount;
|
||||
|
||||
if (gift) amount = getGiftAmount(gift);
|
||||
if (gift) {
|
||||
amount = getGiftAmount(gift);
|
||||
} else {
|
||||
amount = gemsBlock.price;
|
||||
}
|
||||
|
||||
if (!gift || gift.type === 'gems') {
|
||||
const receiver = gift ? gift.member : user;
|
||||
@@ -80,12 +85,13 @@ async function buySubscription (sub, coupon, email, user, token, groupId, stripe
|
||||
return { subResponse: response, subId: subscriptionId };
|
||||
}
|
||||
|
||||
async function applyGemPayment (user, response, gift) {
|
||||
async function applyGemPayment (user, response, gemsBlock, gift) {
|
||||
let method = 'buyGems';
|
||||
const data = {
|
||||
user,
|
||||
customerId: response.id,
|
||||
paymentMethod: stripeConstants.PAYMENT_METHOD,
|
||||
gemsBlock,
|
||||
gift,
|
||||
};
|
||||
|
||||
@@ -97,11 +103,12 @@ async function applyGemPayment (user, response, gift) {
|
||||
await payments[method](data);
|
||||
}
|
||||
|
||||
async function checkout (options, stripeInc) {
|
||||
export async function checkout (options, stripeInc) {
|
||||
const {
|
||||
token,
|
||||
user,
|
||||
gift,
|
||||
gemsBlock,
|
||||
sub,
|
||||
groupId,
|
||||
email,
|
||||
@@ -123,6 +130,11 @@ async function checkout (options, stripeInc) {
|
||||
gift.member = member;
|
||||
}
|
||||
|
||||
let block;
|
||||
if (!sub && !gift) {
|
||||
block = getGemsBlock(gemsBlock);
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
const { subId, subResponse } = await buySubscription(
|
||||
sub, coupon, email, user, token, groupId, stripeApi,
|
||||
@@ -130,7 +142,7 @@ async function checkout (options, stripeInc) {
|
||||
subscriptionId = subId;
|
||||
response = subResponse;
|
||||
} else {
|
||||
response = await buyGems(gift, user, token, stripeApi);
|
||||
response = await buyGems(block, gift, user, token, stripeApi);
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
@@ -143,10 +155,7 @@ async function checkout (options, stripeInc) {
|
||||
groupId,
|
||||
subscriptionId,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
await applyGemPayment(user, response, block, gift);
|
||||
}
|
||||
|
||||
await applyGemPayment(user, response, gift);
|
||||
}
|
||||
|
||||
export { checkout }; // eslint-disable-line import/prefer-default-export
|
||||
|
||||
33
website/server/libs/worldState.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import moment from 'moment';
|
||||
import { // eslint-disable-line import/no-cycle
|
||||
model as Group,
|
||||
TAVERN_ID as tavernId,
|
||||
} from '../models/group';
|
||||
import common from '../../common';
|
||||
|
||||
export async function getWorldBoss () {
|
||||
const tavern = await Group
|
||||
.findById(tavernId)
|
||||
.select('quest.progress quest.key quest.active quest.extra')
|
||||
.exec();
|
||||
if (tavern && tavern.quest && tavern.quest.active) {
|
||||
return tavern.quest;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export function getCurrentEvent () {
|
||||
const currEvtKey = Object.keys(common.content.events).find(evtKey => {
|
||||
const event = common.content.events[evtKey];
|
||||
|
||||
const now = moment();
|
||||
|
||||
return now.isBetween(event.start, event.end);
|
||||
});
|
||||
|
||||
if (!currEvtKey) return null;
|
||||
return {
|
||||
event: currEvtKey,
|
||||
...common.content.events[currEvtKey],
|
||||
};
|
||||
}
|
||||