Compare commits
307 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7e5d2a8d | ||
|
|
afee09e7cb | ||
|
|
01fea6b968 | ||
|
|
8c96ac241a | ||
|
|
c0362c614e | ||
|
|
229ed46425 | ||
|
|
7ede3acd01 | ||
|
|
63453ce01b | ||
|
|
888f6f2486 | ||
|
|
e8501f5cf8 | ||
|
|
fc49015ff0 | ||
|
|
53c536b525 | ||
|
|
e2defc675e | ||
|
|
232a62ffc7 | ||
|
|
d2d4af227b | ||
|
|
5de2573521 | ||
|
|
b472af532c | ||
|
|
027e61a93e | ||
|
|
c35afb7cfe | ||
|
|
a16098ccda | ||
|
|
118c8421fe | ||
|
|
a363e68080 | ||
|
|
1940062200 | ||
|
|
aea075d0bf | ||
|
|
3e63d74b2c | ||
|
|
199ce3e6f7 | ||
|
|
c83a8c9766 | ||
|
|
c481354f78 | ||
|
|
ec9973f9d2 | ||
|
|
de3f1b3f5e | ||
|
|
7098d2a72e | ||
|
|
bbea789700 | ||
|
|
5359a2bf3d | ||
|
|
93e922e774 | ||
|
|
377b152ffd | ||
|
|
8ff8213954 | ||
|
|
00bdf81902 | ||
|
|
4ae50e4d44 | ||
|
|
383cd84016 | ||
|
|
f6f1202baf | ||
|
|
0c65ff6fec | ||
|
|
8dcacdc92e | ||
|
|
7874ae5092 | ||
|
|
481719e513 | ||
|
|
9657112bca | ||
|
|
f7026b2478 | ||
|
|
458aee9a3a | ||
|
|
0b6b967753 | ||
|
|
57f86bac70 | ||
|
|
f2fe83a469 | ||
|
|
3354ca048c | ||
|
|
99c46602c4 | ||
|
|
ee585c0ff3 | ||
|
|
0754c0ff05 | ||
|
|
5d1346e65c | ||
|
|
a2ce0ab099 | ||
|
|
16ae182f34 | ||
|
|
6887fd70c0 | ||
|
|
f327795761 | ||
|
|
8cf5a380da | ||
|
|
def24142ca | ||
|
|
c29049146d | ||
|
|
cc419385f6 | ||
|
|
45107fe48f | ||
|
|
80f517f1ad | ||
|
|
57fb7ca6f2 | ||
|
|
62b171ffa5 | ||
|
|
be18476292 | ||
|
|
b6e9d0c9c0 | ||
|
|
442d9ca9cd | ||
|
|
3f56b7fa3f | ||
|
|
14f9debfdb | ||
|
|
4a1011f1af | ||
|
|
d69de2948b | ||
|
|
c5f5da1d32 | ||
|
|
e338fb8ce7 | ||
|
|
2d5dcae406 | ||
|
|
9ec1917e6d | ||
|
|
409ce5dbfb | ||
|
|
ab706abed5 | ||
|
|
3203b09b7a | ||
|
|
2ea023299c | ||
|
|
ce1ce47d18 | ||
|
|
0d1e8ec3f9 | ||
|
|
4a849e6d15 | ||
|
|
7f65079cfe | ||
|
|
04f54d5e03 | ||
|
|
925e2e5ec6 | ||
|
|
a549668522 | ||
|
|
02e0e45da6 | ||
|
|
1e72dbe155 | ||
|
|
e71f0558fe | ||
|
|
b3d83431e6 | ||
|
|
2ac21104a4 | ||
|
|
6b95f648c4 | ||
|
|
f9db4b9b5b | ||
|
|
6ee2e3a379 | ||
|
|
74da6d8798 | ||
|
|
a73e4d399e | ||
|
|
8f4d668b0f | ||
|
|
945c19cc80 | ||
|
|
7b4cfee290 | ||
|
|
77229f3e5e | ||
|
|
41cdab1672 | ||
|
|
58f4dd0c43 | ||
|
|
0ce64a0197 | ||
|
|
0b8f2bc58e | ||
|
|
015631685b | ||
|
|
6c536c0b89 | ||
|
|
1de2adf301 | ||
|
|
0335eb1f7e | ||
|
|
e0a5938711 | ||
|
|
ad6555c92b | ||
|
|
c04e8ea514 | ||
|
|
aec2409227 | ||
|
|
87aebcc19e | ||
|
|
a3bc20f855 | ||
|
|
86e33b2364 | ||
|
|
12479edb77 | ||
|
|
c0c6657536 | ||
|
|
e81a052f66 | ||
|
|
82a1d6ff0e | ||
|
|
0f7001b609 | ||
|
|
87558a325e | ||
|
|
de48925341 | ||
|
|
614850e56c | ||
|
|
64a3515c10 | ||
|
|
8dfa21a4b8 | ||
|
|
f9a9d4919b | ||
|
|
ddf1b4060d | ||
|
|
967717a010 | ||
|
|
9b791b4ba0 | ||
|
|
5aca5b4be7 | ||
|
|
0dd25b6431 | ||
|
|
cf75d941fa | ||
|
|
777f7887b4 | ||
|
|
f07d0f6441 | ||
|
|
98ec1757f9 | ||
|
|
742da1f2c6 | ||
|
|
b3d5a8d083 | ||
|
|
b5f2e66025 | ||
|
|
9a40674d8d | ||
|
|
c7deb1eb19 | ||
|
|
a213fb723a | ||
|
|
5f66aa35f2 | ||
|
|
96a8c1a41c | ||
|
|
0f9b6ab591 | ||
|
|
3470382528 | ||
|
|
4d953890c3 | ||
|
|
dd6897ac53 | ||
|
|
a19b5356b5 | ||
|
|
b59fcd203b | ||
|
|
0ca339829f | ||
|
|
059269f9b0 | ||
|
|
5eda99b0b8 | ||
|
|
cfa85850bf | ||
|
|
dd9e03044f | ||
|
|
27964a2d86 | ||
|
|
ecac3f0c5f | ||
|
|
9f64633a57 | ||
|
|
5dc4fccddc | ||
|
|
f03c37f420 | ||
|
|
f31103094b | ||
|
|
f30074ed7a | ||
|
|
9aa8b6d64d | ||
|
|
ce96f4065d | ||
|
|
def9aa16b5 | ||
|
|
efae9429c0 | ||
|
|
ac239e32ce | ||
|
|
e1deb6adff | ||
|
|
3474cbf138 | ||
|
|
f845bbd7a0 | ||
|
|
0dfc8de300 | ||
|
|
1988ef957d | ||
|
|
e5bbde7e97 | ||
|
|
b87cfb71f1 | ||
|
|
352b1170c4 | ||
|
|
19d4c5102a | ||
|
|
2c880708e3 | ||
|
|
9d0e2217d5 | ||
|
|
076c090197 | ||
|
|
c8a9730ea1 | ||
|
|
652d792467 | ||
|
|
b9994f5c49 | ||
|
|
c164209c47 | ||
|
|
a8cb303f46 | ||
|
|
2f5fd4019d | ||
|
|
d85436afbf | ||
|
|
d9455101d7 | ||
|
|
a80ac76015 | ||
|
|
dd569ab178 | ||
|
|
6726a2a7ac | ||
|
|
5dc372d143 | ||
|
|
e251fad12c | ||
|
|
4fc880d6de | ||
|
|
f0c3be4800 | ||
|
|
c7aadede4d | ||
|
|
5a07e5cbf3 | ||
|
|
b1dab729b6 | ||
|
|
95231b1ede | ||
|
|
43a196ffea | ||
|
|
f72224f9f1 | ||
|
|
ec2322bdd9 | ||
|
|
3adbc33546 | ||
|
|
37d48b3193 | ||
|
|
b79f53a108 | ||
|
|
98c4910051 | ||
|
|
55e7ef138e | ||
|
|
474d3fb76f | ||
|
|
b74c7aa009 | ||
|
|
825baaf7e9 | ||
|
|
079279e5c1 | ||
|
|
01c7791fd9 | ||
|
|
9ed06223e0 | ||
|
|
6d33ec02a8 | ||
|
|
c6d36ad6b1 | ||
|
|
64bf4ee4b6 | ||
|
|
fd9d738cc6 | ||
|
|
0d6dbfdc95 | ||
|
|
5162f8c2a0 | ||
|
|
ae1c9c37c9 | ||
|
|
0ed8a220d6 | ||
|
|
d2cbcbd062 | ||
|
|
349a0eba44 | ||
|
|
4f7ed6e7cc | ||
|
|
2eb7bab1dd | ||
|
|
0224ce7e3e | ||
|
|
0cbc2b5ffc | ||
|
|
1f59d95465 | ||
|
|
cdd1bf1cf0 | ||
|
|
7309ab4fd4 | ||
|
|
42e0bad4ac | ||
|
|
41cd99c920 | ||
|
|
0902c63a79 | ||
|
|
b97da5fe57 | ||
|
|
8a76561259 | ||
|
|
d345e0d4a4 | ||
|
|
65ee50739f | ||
|
|
2c9ee04c6d | ||
|
|
3893d38583 | ||
|
|
1587827b22 | ||
|
|
cfdef760d5 | ||
|
|
eb2cb9e921 | ||
|
|
591279c1a8 | ||
|
|
ee91780f20 | ||
|
|
a9629bdc0a | ||
|
|
9c10cb3b88 | ||
|
|
2d1fca402b | ||
|
|
a774d32b8a | ||
|
|
573c932565 | ||
|
|
cde5fbef85 | ||
|
|
580139ff69 | ||
|
|
e0860e604e | ||
|
|
9fc69456bb | ||
|
|
5bf14e05cc | ||
|
|
7d081056ba | ||
|
|
2ff7bef2a6 | ||
|
|
51b3b0c4c7 | ||
|
|
174a4e69f9 | ||
|
|
1ce060eac6 | ||
|
|
55f07f8ab2 | ||
|
|
df25e0574d | ||
|
|
4fe8b63748 | ||
|
|
b5c64185f0 | ||
|
|
debeee7569 | ||
|
|
64b8a28363 | ||
|
|
8b2af1ef56 | ||
|
|
894558f2df | ||
|
|
21652c2670 | ||
|
|
d1ee679810 | ||
|
|
67988da33c | ||
|
|
fae26a517d | ||
|
|
57be0fbe45 | ||
|
|
cf9fbd43bb | ||
|
|
ea817eecf7 | ||
|
|
f1381878e7 | ||
|
|
9bd039b17b | ||
|
|
8804892135 | ||
|
|
90b34c4dac | ||
|
|
96a919ed4b | ||
|
|
e56b672226 | ||
|
|
91cbf7a2a9 | ||
|
|
04e2a39a9f | ||
|
|
bdd926e110 | ||
|
|
a8e9c9bc70 | ||
|
|
e3c86349b4 | ||
|
|
6604f38144 | ||
|
|
037882b50a | ||
|
|
15deb778fd | ||
|
|
7d2529f5e1 | ||
|
|
8d732c59c4 | ||
|
|
3a34aa4cc5 | ||
|
|
e7fc7feddd | ||
|
|
7fd899b642 | ||
|
|
36d2ad6b9b | ||
|
|
164dbdcf10 | ||
|
|
b65fa941b9 | ||
|
|
ab953440e3 | ||
|
|
1143f690d1 | ||
|
|
08469c556b | ||
|
|
13a25ad89e | ||
|
|
8e2e170930 | ||
|
|
e6a7d15644 | ||
|
|
6a4b08203f | ||
|
|
c9016c8d42 | ||
|
|
31685c3e94 | ||
|
|
c25b09c7ed |
108
migrations/archive/2022/20221213_pet_group_achievements.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20221213_pet_group_achievements';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
};
|
||||
|
||||
if (user && user.items && user.items.pets) {
|
||||
const pets = user.items.pets;
|
||||
if (pets['BearCub-Base']
|
||||
&& pets['BearCub-CottonCandyBlue']
|
||||
&& pets['BearCub-CottonCandyPink']
|
||||
&& pets['BearCub-Desert']
|
||||
&& pets['BearCub-Golden']
|
||||
&& pets['BearCub-Red']
|
||||
&& pets['BearCub-Shade']
|
||||
&& pets['BearCub-Skeleton']
|
||||
&& pets['BearCub-White']
|
||||
&& pets['BearCub-Zombie']
|
||||
&& pets['Fox-Base']
|
||||
&& pets['Fox-CottonCandyBlue']
|
||||
&& pets['Fox-CottonCandyPink']
|
||||
&& pets['Fox-Desert']
|
||||
&& pets['Fox-Golden']
|
||||
&& pets['Fox-Red']
|
||||
&& pets['Fox-Shade']
|
||||
&& pets['Fox-Skeleton']
|
||||
&& pets['Fox-White']
|
||||
&& pets['Fox-Zombie']
|
||||
&& pets['Penguin-Base']
|
||||
&& pets['Penguin-CottonCandyBlue']
|
||||
&& pets['Penguin-CottonCandyPink']
|
||||
&& pets['Penguin-Desert']
|
||||
&& pets['Penguin-Golden']
|
||||
&& pets['Penguin-Red']
|
||||
&& pets['Penguin-Shade']
|
||||
&& pets['Penguin-Skeleton']
|
||||
&& pets['Penguin-White']
|
||||
&& pets['Penguin-Zombie']
|
||||
&& pets['Whale-Base']
|
||||
&& pets['Whale-CottonCandyBlue']
|
||||
&& pets['Whale-CottonCandyPink']
|
||||
&& pets['Whale-Desert']
|
||||
&& pets['Whale-Golden']
|
||||
&& pets['Whale-Red']
|
||||
&& pets['Whale-Shade']
|
||||
&& pets['Whale-Skeleton']
|
||||
&& pets['Whale-White']
|
||||
&& pets['Whale-Zombie']
|
||||
&& pets['Wolf-Base']
|
||||
&& pets['Wolf-CottonCandyBlue']
|
||||
&& pets['Wolf-CottonCandyPink']
|
||||
&& pets['Wolf-Desert']
|
||||
&& pets['Wolf-Golden']
|
||||
&& pets['Wolf-Red']
|
||||
&& pets['Wolf-Shade']
|
||||
&& pets['Wolf-Skeleton']
|
||||
&& pets['Wolf-White']
|
||||
&& pets['Wolf-Zombie'] {
|
||||
set['achievements.polarPro'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({ _id: user._id }, { $set: set }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
// migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2022-11-01') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1]._id,
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
144
migrations/archive/2022/20221227_nye.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/* eslint-disable no-console */
|
||||
const MIGRATION_NAME = '20221227_nye';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count++;
|
||||
|
||||
const set = { migration: MIGRATION_NAME };
|
||||
let push;
|
||||
|
||||
if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2022'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2022',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2021'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2021',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2020'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2020',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2019'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2019',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2018'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2018',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2017'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2017',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2016'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2016',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2015'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2015',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
|
||||
set['items.gear.owned.head_special_nye2014'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye2014',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
} else {
|
||||
set['items.gear.owned.head_special_nye'] = false;
|
||||
push = [
|
||||
{
|
||||
type: 'marketGear',
|
||||
path: 'gear.flat.head_special_nye',
|
||||
_id: uuid(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set, $push: {pinnedItems: {$each: push}}}).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2022-12-01')},
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
88
migrations/archive/2023/20230123_habit_birthday.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable no-console */
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20230123_habit_birthday';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const inc = { 'balance': 5 };
|
||||
const set = {};
|
||||
const push = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
if (typeof user.items.gear.owned.armor_special_birthday2022 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2023'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2021 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2022'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2020 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2021'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2019 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2020'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2018 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2019'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2017 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2018'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2016 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2017'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday2015 !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2016'] = true;
|
||||
} else if (typeof user.items.gear.owned.armor_special_birthday !== 'undefined') {
|
||||
set['items.gear.owned.armor_special_birthday2015'] = true;
|
||||
} else {
|
||||
set['items.gear.owned.armor_special_birthday'] = true;
|
||||
}
|
||||
|
||||
push.notifications = {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_head_special_nye',
|
||||
title: 'Birthday Bash Day 1!',
|
||||
text: 'Enjoy your new Birthday Robe and 20 Gems on us!',
|
||||
destination: 'equipment',
|
||||
},
|
||||
seen: false,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$inc: inc, $set: set, $push: push}).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2022-12-23')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
69
migrations/archive/2023/20230127_habit_birthday_day5.js
Normal file
@@ -0,0 +1,69 @@
|
||||
/* eslint-disable no-console */
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20230127_habit_birthday_day5';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const set = {};
|
||||
const push = {};
|
||||
|
||||
set.migration = MIGRATION_NAME;
|
||||
|
||||
set['items.gear.owned.back_special_anniversary'] = true;
|
||||
set['items.gear.owned.body_special_anniversary'] = true;
|
||||
set['items.gear.owned.eyewear_special_anniversary'] = true;
|
||||
|
||||
push.notifications = {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_head_special_nye',
|
||||
title: 'Birthday Bash Day 5!',
|
||||
text: 'Come celebrate by wearing your new Habitica Hero Cape, Collar, and Mask!',
|
||||
destination: 'equipment',
|
||||
},
|
||||
seen: false,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2022-12-23')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
79
migrations/archive/2023/20230201_habit_birthday_day10.js
Normal file
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable no-console */
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { model as User } from '../../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20230201_habit_birthday_day10';
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
|
||||
async function updateUser (user) {
|
||||
count += 1;
|
||||
|
||||
const set = {
|
||||
migration: MIGRATION_NAME,
|
||||
'purchased.background.birthday_bash': true,
|
||||
};
|
||||
const push = {
|
||||
notifications: {
|
||||
type: 'ITEM_RECEIVED',
|
||||
data: {
|
||||
icon: 'notif_head_special_nye',
|
||||
title: 'Birthday Bash Day 10!',
|
||||
text: 'Join in for the end of our birthday celebrations with 10th Birthday background, Cake, and achievement!',
|
||||
destination: 'backgrounds',
|
||||
},
|
||||
seen: false,
|
||||
},
|
||||
};
|
||||
const inc = {
|
||||
'items.food.Cake_Skeleton': 1,
|
||||
'items.food.Cake_Base': 1,
|
||||
'items.food.Cake_CottonCandyBlue': 1,
|
||||
'items.food.Cake_CottonCandyPink': 1,
|
||||
'items.food.Cake_Shade': 1,
|
||||
'items.food.Cake_White': 1,
|
||||
'items.food.Cake_Golden': 1,
|
||||
'items.food.Cake_Zombie': 1,
|
||||
'items.food.Cake_Desert': 1,
|
||||
'items.food.Cake_Red': 1,
|
||||
'achievements.habitBirthdays': 1,
|
||||
};
|
||||
|
||||
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
|
||||
|
||||
return await User.update({_id: user._id}, {$set: set, $push: push, $inc: inc }).exec();
|
||||
}
|
||||
|
||||
export default async function processUsers () {
|
||||
let query = {
|
||||
migration: {$ne: MIGRATION_NAME},
|
||||
'auth.timestamps.loggedin': {$gt: new Date('2022-12-23')},
|
||||
};
|
||||
|
||||
const fields = {
|
||||
_id: 1,
|
||||
items: 1,
|
||||
};
|
||||
|
||||
while (true) { // eslint-disable-line no-constant-condition
|
||||
const users = await User // eslint-disable-line no-await-in-loop
|
||||
.find(query)
|
||||
.limit(250)
|
||||
.sort({_id: 1})
|
||||
.select(fields)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (users.length === 0) {
|
||||
console.warn('All appropriate users found and modified.');
|
||||
console.warn(`\n${count} users processed\n`);
|
||||
break;
|
||||
} else {
|
||||
query._id = {
|
||||
$gt: users[users.length - 1],
|
||||
};
|
||||
}
|
||||
|
||||
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { model as User } from '../../website/server/models/user';
|
||||
|
||||
const MIGRATION_NAME = '20220314_pi_day';
|
||||
const MIGRATION_NAME = '20230314_pi_day';
|
||||
|
||||
const progressCount = 1000;
|
||||
let count = 0;
|
||||
@@ -54,7 +54,7 @@ async function updateUser (user) {
|
||||
export default async function processUsers () {
|
||||
const query = {
|
||||
migration: { $ne: MIGRATION_NAME },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2022-02-15') },
|
||||
'auth.timestamps.loggedin': { $gt: new Date('2023-02-15') },
|
||||
};
|
||||
|
||||
const fields = {
|
||||
|
||||
844
package-lock.json
generated
24
package.json
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.249.5",
|
||||
"version": "4.266.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@babel/core": "^7.20.12",
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/register": "^7.18.9",
|
||||
"@google-cloud/trace-agent": "^7.1.2",
|
||||
@@ -13,7 +13,7 @@
|
||||
"accepts": "^1.3.8",
|
||||
"amazon-payments": "^0.2.9",
|
||||
"amplitude": "^6.0.0",
|
||||
"apidoc": "^0.53.1",
|
||||
"apidoc": "^0.54.0",
|
||||
"apple-auth": "^1.0.7",
|
||||
"bcrypt": "^5.1.0",
|
||||
"body-parser": "^1.20.1",
|
||||
@@ -30,7 +30,7 @@
|
||||
"express": "^4.18.2",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"express-validator": "^5.2.0",
|
||||
"glob": "^8.0.3",
|
||||
"glob": "^8.1.0",
|
||||
"got": "^11.8.3",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^8.0.0",
|
||||
@@ -63,16 +63,16 @@
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.0",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
"stripe": "^10.13.0",
|
||||
"superagent": "^8.0.3",
|
||||
"stripe": "^11.10.0",
|
||||
"superagent": "^8.0.6",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.7.0",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.9.0",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"winston": "^3.8.2",
|
||||
"winston-loggly-bulk": "^3.2.1",
|
||||
@@ -110,11 +110,11 @@
|
||||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"axios": "^1.2.2",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^5.1.2",
|
||||
"chalk": "^5.2.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"expect.js": "^0.3.1",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
@@ -122,7 +122,7 @@
|
||||
"monk": "^7.3.4",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^14.0.2",
|
||||
"sinon": "^15.0.1",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -231,13 +231,16 @@ describe('cron', async () => {
|
||||
},
|
||||
});
|
||||
// user1 has a 1-month recurring subscription starting today
|
||||
user1.purchased.plan.customerId = 'subscribedId';
|
||||
user1.purchased.plan.dateUpdated = moment().toDate();
|
||||
user1.purchased.plan.planId = 'basic';
|
||||
user1.purchased.plan.consecutive.count = 0;
|
||||
user1.purchased.plan.consecutive.offset = 0;
|
||||
user1.purchased.plan.consecutive.trinkets = 0;
|
||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
beforeEach(async () => {
|
||||
user1.purchased.plan.customerId = 'subscribedId';
|
||||
user1.purchased.plan.dateUpdated = moment().toDate();
|
||||
user1.purchased.plan.planId = 'basic';
|
||||
user1.purchased.plan.consecutive.count = 0;
|
||||
user1.purchased.plan.perkMonthCount = 0;
|
||||
user1.purchased.plan.consecutive.offset = 0;
|
||||
user1.purchased.plan.consecutive.trinkets = 0;
|
||||
user1.purchased.plan.consecutive.gemCapExtra = 0;
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits after the first month', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
@@ -271,6 +274,24 @@ describe('cron', async () => {
|
||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits after the second month if they also received a 1 month gift subscription', async () => {
|
||||
user1.purchased.plan.perkMonthCount = 1;
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
// Add 1 month to simulate what happens a month after the subscription was created.
|
||||
// Add 2 days so that we're sure we're not affected by any start-of-month effects
|
||||
// e.g., from time zone oddness.
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user1.purchased.plan.perkMonthCount).to.equal(0);
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(2);
|
||||
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits after the third month', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
|
||||
.add(2, 'days')
|
||||
@@ -315,6 +336,30 @@ describe('cron', async () => {
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
|
||||
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
|
||||
});
|
||||
|
||||
it('initializes plan.perkMonthCount if necessary', async () => {
|
||||
user.purchased.plan.perkMonthCount = undefined;
|
||||
clock = sinon.useFakeTimers(moment(user.purchased.plan.dateUpdated)
|
||||
.utcOffset(0)
|
||||
.startOf('month')
|
||||
.add(1, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.perkMonthCount).to.equal(1);
|
||||
user.purchased.plan.perkMonthCount = undefined;
|
||||
user.purchased.plan.consecutive.count = 8;
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.perkMonthCount).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for a 3-month recurring subscription', async () => {
|
||||
@@ -330,13 +375,16 @@ describe('cron', async () => {
|
||||
},
|
||||
});
|
||||
// user3 has a 3-month recurring subscription starting today
|
||||
user3.purchased.plan.customerId = 'subscribedId';
|
||||
user3.purchased.plan.dateUpdated = moment().toDate();
|
||||
user3.purchased.plan.planId = 'basic_3mo';
|
||||
user3.purchased.plan.consecutive.count = 0;
|
||||
user3.purchased.plan.consecutive.offset = 3;
|
||||
user3.purchased.plan.consecutive.trinkets = 1;
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
beforeEach(async () => {
|
||||
user3.purchased.plan.customerId = 'subscribedId';
|
||||
user3.purchased.plan.dateUpdated = moment().toDate();
|
||||
user3.purchased.plan.planId = 'basic_3mo';
|
||||
user3.purchased.plan.perkMonthCount = 0;
|
||||
user3.purchased.plan.consecutive.count = 0;
|
||||
user3.purchased.plan.consecutive.offset = 3;
|
||||
user3.purchased.plan.consecutive.trinkets = 1;
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
@@ -390,6 +438,21 @@ describe('cron', async () => {
|
||||
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
|
||||
});
|
||||
|
||||
it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
|
||||
user3.purchased.plan.perkMonthCount = 2;
|
||||
user3.purchased.plan.consecutive.trinkets = 1;
|
||||
user3.purchased.plan.consecutive.gemCapExtra = 5;
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3.purchased.plan.perkMonthCount).to.equal(2);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
|
||||
.add(2, 'days')
|
||||
@@ -456,13 +519,16 @@ describe('cron', async () => {
|
||||
},
|
||||
});
|
||||
// user6 has a 6-month recurring subscription starting today
|
||||
user6.purchased.plan.customerId = 'subscribedId';
|
||||
user6.purchased.plan.dateUpdated = moment().toDate();
|
||||
user6.purchased.plan.planId = 'google_6mo';
|
||||
user6.purchased.plan.consecutive.count = 0;
|
||||
user6.purchased.plan.consecutive.offset = 6;
|
||||
user6.purchased.plan.consecutive.trinkets = 2;
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
beforeEach(async () => {
|
||||
user6.purchased.plan.customerId = 'subscribedId';
|
||||
user6.purchased.plan.dateUpdated = moment().toDate();
|
||||
user6.purchased.plan.planId = 'google_6mo';
|
||||
user6.purchased.plan.perkMonthCount = 0;
|
||||
user6.purchased.plan.consecutive.count = 0;
|
||||
user6.purchased.plan.consecutive.offset = 6;
|
||||
user6.purchased.plan.consecutive.trinkets = 2;
|
||||
user6.purchased.plan.consecutive.gemCapExtra = 10;
|
||||
});
|
||||
|
||||
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
|
||||
@@ -503,6 +569,19 @@ describe('cron', async () => {
|
||||
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
||||
});
|
||||
|
||||
it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
|
||||
user6.purchased.plan.perkMonthCount = 2;
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user6, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user6.purchased.plan.perkMonthCount).to.equal(2);
|
||||
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
|
||||
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
|
||||
});
|
||||
|
||||
it('increments consecutive benefits the month after the third paid period has started', async () => {
|
||||
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
|
||||
.add(2, 'days')
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('Amazon Payments - Checkout', () => {
|
||||
let closeOrderReferenceSpy;
|
||||
|
||||
let paymentBuyGemsStub;
|
||||
let paymentCreateSubscritionStub;
|
||||
let paymentCreateSubscriptionStub;
|
||||
let amount = gemsBlock.price / 100;
|
||||
|
||||
function expectOrderReferenceSpy () {
|
||||
@@ -85,8 +85,8 @@ describe('Amazon Payments - Checkout', () => {
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems');
|
||||
paymentBuyGemsStub.resolves({});
|
||||
|
||||
paymentCreateSubscritionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscritionStub.resolves({});
|
||||
paymentCreateSubscriptionStub = sinon.stub(payments, 'createSubscription');
|
||||
paymentCreateSubscriptionStub.resolves({});
|
||||
|
||||
sinon.stub(common, 'uuid').returns('uuid-generated');
|
||||
sandbox.stub(gems, 'validateGiftMessage');
|
||||
@@ -109,6 +109,7 @@ describe('Amazon Payments - Checkout', () => {
|
||||
user,
|
||||
paymentMethod,
|
||||
headers,
|
||||
sku: undefined,
|
||||
};
|
||||
if (gift) {
|
||||
expectedArgs.gift = gift;
|
||||
@@ -215,13 +216,14 @@ describe('Amazon Payments - Checkout', () => {
|
||||
});
|
||||
|
||||
gift.member = receivingUser;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscritionStub).to.be.calledWith({
|
||||
expect(paymentCreateSubscriptionStub).to.be.calledOnce;
|
||||
expect(paymentCreateSubscriptionStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: amzLib.constants.PAYMENT_METHOD_GIFT,
|
||||
headers,
|
||||
gift,
|
||||
gemsBlock: undefined,
|
||||
sku: undefined,
|
||||
});
|
||||
expectAmazonStubs();
|
||||
});
|
||||
|
||||
@@ -12,10 +12,10 @@ const { i18n } = common;
|
||||
describe('Apple Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuyGemsStub; let
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let paymentBuySkuStub; let
|
||||
iapGetPurchaseDataStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -29,14 +29,15 @@ describe('Apple Payments', () => {
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
productId: 'com.habitrpg.ios.Habitica.21gems',
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
@@ -44,8 +45,10 @@ describe('Apple Payments', () => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
@@ -54,7 +57,7 @@ describe('Apple Payments', () => {
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
|
||||
await expect(applePayments.verifyPurchase({ user, receipt, headers }))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
@@ -66,7 +69,7 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData').returns([]);
|
||||
|
||||
await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
|
||||
await expect(applePayments.verifyPurchase({ user, receipt, headers }))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
@@ -76,7 +79,7 @@ describe('Apple Payments', () => {
|
||||
|
||||
it('errors if the user cannot purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').resolves(false);
|
||||
await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
|
||||
await expect(applePayments.verifyPurchase({ user, receipt, headers }))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
@@ -94,14 +97,16 @@ describe('Apple Payments', () => {
|
||||
productId: 'badProduct',
|
||||
transactionId: token,
|
||||
}]);
|
||||
paymentBuySkuStub.restore();
|
||||
|
||||
await expect(applePayments.verifyGemPurchase({ user, receipt, headers }))
|
||||
await expect(applePayments.verifyPurchase({ user, receipt, headers }))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: applePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
user.canGetGems.restore();
|
||||
});
|
||||
|
||||
@@ -138,7 +143,7 @@ describe('Apple Payments', () => {
|
||||
}]);
|
||||
|
||||
sinon.stub(user, 'canGetGems').resolves(true);
|
||||
await applePayments.verifyGemPurchase({ user, receipt, headers });
|
||||
await applePayments.verifyPurchase({ user, receipt, headers });
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
@@ -148,13 +153,13 @@ describe('Apple Payments', () => {
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
expect(validateGiftMessageStub).to.not.be.called;
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
expect(paymentBuySkuStub).to.be.calledOnce;
|
||||
expect(paymentBuySkuStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
gemsBlock: common.content.gems[gemTest.gemsBlock],
|
||||
headers,
|
||||
gift: undefined,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sku: gemTest.productId,
|
||||
headers,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
@@ -173,7 +178,7 @@ describe('Apple Payments', () => {
|
||||
}]);
|
||||
|
||||
const gift = { uuid: receivingUser._id };
|
||||
await applePayments.verifyGemPurchase({
|
||||
await applePayments.verifyPurchase({
|
||||
user, gift, receipt, headers,
|
||||
});
|
||||
|
||||
@@ -187,18 +192,16 @@ describe('Apple Payments', () => {
|
||||
expect(validateGiftMessageStub).to.be.calledOnce;
|
||||
expect(validateGiftMessageStub).to.be.calledWith(gift, user);
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
expect(paymentBuySkuStub).to.be.calledOnce;
|
||||
expect(paymentBuySkuStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
headers,
|
||||
gift: {
|
||||
type: 'gems',
|
||||
gems: { amount: 4 },
|
||||
member: sinon.match({ _id: receivingUser._id }),
|
||||
uuid: receivingUser._id,
|
||||
member: sinon.match({ _id: receivingUser._id }),
|
||||
},
|
||||
gemsBlock: common.content.gems['4gems'],
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sku: 'com.habitrpg.ios.Habitica.4gems',
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -218,6 +221,7 @@ describe('Apple Payments', () => {
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
@@ -228,14 +232,17 @@ describe('Apple Payments', () => {
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().valueOf(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().valueOf(),
|
||||
productId: 'wrongsku',
|
||||
transactionId: token,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().valueOf(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
}]);
|
||||
@@ -250,21 +257,12 @@ describe('Apple Payments', () => {
|
||||
if (payments.createSubscription.restore) payments.createSubscription.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if sku is empty', async () => {
|
||||
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: i18n.t('missingSubscriptionCode'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if receipt is invalid', async () => {
|
||||
iap.isValidated.restore();
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
@@ -295,13 +293,15 @@ describe('Apple Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||
purchaseDate: new Date(),
|
||||
productId: option.sku,
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[option.subKey];
|
||||
|
||||
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
@@ -321,21 +321,253 @@ describe('Apple Payments', () => {
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
if (option !== subOptions[3]) {
|
||||
const newOption = subOptions[3];
|
||||
it(`upgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => {
|
||||
const oldSub = common.content.subscriptionBlocks[option.subKey];
|
||||
oldSub.logic = 'refundAndRepay';
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = token;
|
||||
user.purchased.plan.planId = option.subKey;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||
purchaseDate: moment.utc().valueOf(),
|
||||
productId: newOption.sku,
|
||||
transactionId: `${token}new`,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[newOption.subKey];
|
||||
|
||||
await applePayments.subscribe(user,
|
||||
receipt,
|
||||
headers,
|
||||
nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
updatedFrom: oldSub,
|
||||
});
|
||||
});
|
||||
}
|
||||
if (option !== subOptions[0]) {
|
||||
const newOption = subOptions[0];
|
||||
it(`downgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => {
|
||||
const oldSub = common.content.subscriptionBlocks[option.subKey];
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
user.purchased.plan.customerId = token;
|
||||
user.purchased.plan.planId = option.subKey;
|
||||
user.purchased.plan.additionalData = receipt;
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||
purchaseDate: moment.utc().valueOf(),
|
||||
productId: newOption.sku,
|
||||
transactionId: `${token}new`,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[newOption.subKey];
|
||||
|
||||
await applePayments.subscribe(user,
|
||||
receipt,
|
||||
headers,
|
||||
nextPaymentProcessing);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: token,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
updatedFrom: oldSub,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('errors when a user is already subscribed', async () => {
|
||||
payments.createSubscription.restore();
|
||||
user = new User();
|
||||
await user.save();
|
||||
it('uses the most recent subscription data', async () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 4 }).toDate(),
|
||||
purchaseDate: moment.utc().subtract({ day: 5 }).toDate(),
|
||||
productId: 'com.habitrpg.ios.habitica.subscription.3month',
|
||||
transactionId: `${token}oldest`,
|
||||
originalTransactionId: `${token}evenOlder`,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({ day: 2 }).toDate(),
|
||||
purchaseDate: moment.utc().subtract({ day: 1 }).toDate(),
|
||||
productId: 'com.habitrpg.ios.habitica.subscription.12month',
|
||||
transactionId: `${token}newest`,
|
||||
originalTransactionId: `${token}newest`,
|
||||
}, {
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().subtract({ day: 2 }).toDate(),
|
||||
productId: 'com.habitrpg.ios.habitica.subscription.6month',
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks.basic_12mo;
|
||||
|
||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
||||
});
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
|
||||
expect(paymentsCreateSubscritionStub).to.be.calledWith({
|
||||
user,
|
||||
customerId: `${token}newest`,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
additionalData: receipt,
|
||||
nextPaymentProcessing,
|
||||
});
|
||||
});
|
||||
|
||||
describe('does not apply multiple times', async () => {
|
||||
it('errors when a user is using the same subscription', async () => {
|
||||
payments.createSubscription.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when a user is using a rebill of the same subscription', async () => {
|
||||
user = new User();
|
||||
await user.save();
|
||||
payments.createSubscription.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: `${token}renew`,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when a different user is using the subscription', async () => {
|
||||
user = new User();
|
||||
await user.save();
|
||||
payments.createSubscription.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
|
||||
const secondUser = new User();
|
||||
await secondUser.save();
|
||||
await expect(applePayments.subscribe(
|
||||
secondUser, receipt, headers, nextPaymentProcessing,
|
||||
))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
||||
});
|
||||
});
|
||||
|
||||
it('errors when a multiple users exist using the subscription', async () => {
|
||||
user = new User();
|
||||
await user.save();
|
||||
payments.createSubscription.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
|
||||
const secondUser = new User();
|
||||
secondUser.purchased.plan = user.purchased.plan;
|
||||
secondUser.purchased.plan.dateTerminate = new Date();
|
||||
secondUser.save();
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
purchaseDate: moment.utc().toDate(),
|
||||
productId: sku,
|
||||
transactionId: `${token}new`,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
|
||||
const thirdUser = new User();
|
||||
await thirdUser.save();
|
||||
await expect(applePayments.subscribe(
|
||||
thirdUser, receipt, headers, nextPaymentProcessing,
|
||||
))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: applePayments.constants.RESPONSE_ALREADY_USED,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,9 +592,9 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.toDate() }]);
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
|
||||
sinon.stub(iap, 'isCanceled').returns(false);
|
||||
sinon.stub(iap, 'isExpired').returns(true);
|
||||
user = new User();
|
||||
user.profile.name = 'sender';
|
||||
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
|
||||
@@ -377,6 +609,8 @@ describe('Apple Payments', () => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
iap.isExpired.restore();
|
||||
iap.isCanceled.restore();
|
||||
iap.getPurchaseData.restore();
|
||||
payments.cancelSubscription.restore();
|
||||
});
|
||||
@@ -396,6 +630,8 @@ describe('Apple Payments', () => {
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
|
||||
iap.isExpired.restore();
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
|
||||
await expect(applePayments.cancelSubscribe(user, headers))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
@@ -418,7 +654,38 @@ describe('Apple Payments', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel a user subscription', async () => {
|
||||
it('should cancel a cancelled subscription with termination date in the future', async () => {
|
||||
const futureDate = expirationDate.add({ day: 1 });
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{ expirationDate: futureDate }]);
|
||||
iap.isExpired.restore();
|
||||
sinon.stub(iap, 'isExpired').returns(false);
|
||||
|
||||
iap.isCanceled.restore();
|
||||
sinon.stub(iap, 'isCanceled').returns(true);
|
||||
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledOnce;
|
||||
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({
|
||||
expirationDate: futureDate,
|
||||
});
|
||||
expect(iapGetPurchaseDataStub).to.be.calledOnce;
|
||||
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
|
||||
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
|
||||
nextBill: futureDate.toDate(),
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
it('should cancel an expired subscription', async () => {
|
||||
await applePayments.cancelSubscribe(user, headers);
|
||||
|
||||
expect(iapSetupStub).to.be.calledOnce;
|
||||
|
||||
@@ -12,11 +12,11 @@ const { i18n } = common;
|
||||
describe('Google Payments', () => {
|
||||
const subKey = 'basic_3mo';
|
||||
|
||||
describe('verifyGemPurchase', () => {
|
||||
describe('verifyPurchase', () => {
|
||||
let sku; let user; let token; let receipt; let signature; let
|
||||
headers; const gemsBlock = common.content.gems['21gems'];
|
||||
headers;
|
||||
let iapSetupStub; let iapValidateStub; let iapIsValidatedStub; let
|
||||
paymentBuyGemsStub; let validateGiftMessageStub;
|
||||
paymentBuySkuStub; let validateGiftMessageStub;
|
||||
|
||||
beforeEach(() => {
|
||||
sku = 'com.habitrpg.android.habitica.iap.21gems';
|
||||
@@ -27,11 +27,10 @@ describe('Google Payments', () => {
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
iapValidateStub = sinon.stub(iap, 'validate')
|
||||
.resolves({});
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({ productId: sku });
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(true);
|
||||
paymentBuyGemsStub = sinon.stub(payments, 'buyGems').resolves({});
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
validateGiftMessageStub = sinon.stub(gems, 'validateGiftMessage');
|
||||
});
|
||||
|
||||
@@ -39,7 +38,7 @@ describe('Google Payments', () => {
|
||||
iap.setup.restore();
|
||||
iap.validate.restore();
|
||||
iap.isValidated.restore();
|
||||
payments.buyGems.restore();
|
||||
payments.buySkuItem.restore();
|
||||
gems.validateGiftMessage.restore();
|
||||
});
|
||||
|
||||
@@ -48,7 +47,7 @@ describe('Google Payments', () => {
|
||||
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
|
||||
.returns(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase({
|
||||
await expect(googlePayments.verifyPurchase({
|
||||
user, receipt, signature, headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
@@ -60,21 +59,25 @@ describe('Google Payments', () => {
|
||||
|
||||
it('should throw an error if productId is invalid', async () => {
|
||||
receipt = `{"token": "${token}", "productId": "invalid"}`;
|
||||
iapValidateStub.restore();
|
||||
iapValidateStub = sinon.stub(iap, 'validate').resolves({});
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase({
|
||||
paymentBuySkuStub.restore();
|
||||
await expect(googlePayments.verifyPurchase({
|
||||
user, receipt, signature, headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
httpCode: 400,
|
||||
name: 'BadRequest',
|
||||
message: googlePayments.constants.RESPONSE_INVALID_ITEM,
|
||||
});
|
||||
paymentBuySkuStub = sinon.stub(payments, 'buySkuItem').resolves({});
|
||||
});
|
||||
|
||||
it('should throw an error if user cannot purchase gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').resolves(false);
|
||||
|
||||
await expect(googlePayments.verifyGemPurchase({
|
||||
await expect(googlePayments.verifyPurchase({
|
||||
user, receipt, signature, headers,
|
||||
}))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
@@ -88,7 +91,7 @@ describe('Google Payments', () => {
|
||||
|
||||
it('purchases gems', async () => {
|
||||
sinon.stub(user, 'canGetGems').resolves(true);
|
||||
await googlePayments.verifyGemPurchase({
|
||||
await googlePayments.verifyPurchase({
|
||||
user, receipt, signature, headers,
|
||||
});
|
||||
|
||||
@@ -101,15 +104,17 @@ describe('Google Payments', () => {
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapIsValidatedStub).to.be.calledWith(
|
||||
{ productId: sku },
|
||||
);
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
expect(paymentBuySkuStub).to.be.calledOnce;
|
||||
expect(paymentBuySkuStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
gemsBlock,
|
||||
headers,
|
||||
gift: undefined,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
sku,
|
||||
headers,
|
||||
});
|
||||
expect(user.canGetGems).to.be.calledOnce;
|
||||
user.canGetGems.restore();
|
||||
@@ -120,7 +125,7 @@ describe('Google Payments', () => {
|
||||
await receivingUser.save();
|
||||
|
||||
const gift = { uuid: receivingUser._id };
|
||||
await googlePayments.verifyGemPurchase({
|
||||
await googlePayments.verifyPurchase({
|
||||
user, gift, receipt, signature, headers,
|
||||
});
|
||||
|
||||
@@ -134,20 +139,20 @@ describe('Google Payments', () => {
|
||||
signature,
|
||||
});
|
||||
expect(iapIsValidatedStub).to.be.calledOnce;
|
||||
expect(iapIsValidatedStub).to.be.calledWith({});
|
||||
expect(iapIsValidatedStub).to.be.calledWith(
|
||||
{ productId: sku },
|
||||
);
|
||||
|
||||
expect(paymentBuyGemsStub).to.be.calledOnce;
|
||||
expect(paymentBuyGemsStub).to.be.calledWith({
|
||||
expect(paymentBuySkuStub).to.be.calledOnce;
|
||||
expect(paymentBuySkuStub).to.be.calledWith({
|
||||
user,
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
gemsBlock,
|
||||
headers,
|
||||
gift: {
|
||||
type: 'gems',
|
||||
gems: { amount: 21 },
|
||||
member: sinon.match({ _id: receivingUser._id }),
|
||||
uuid: receivingUser._id,
|
||||
member: sinon.match({ _id: receivingUser._id }),
|
||||
},
|
||||
paymentMethod: googlePayments.constants.PAYMENT_METHOD_GOOGLE,
|
||||
sku,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -203,6 +203,28 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
|
||||
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.exist;
|
||||
});
|
||||
|
||||
it('keeps plan.dateCreated when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
const initialDate = recipient.purchased.plan.dateCreated;
|
||||
await api.createSubscription(data);
|
||||
expect(recipient.purchased.plan.dateCreated).to.eql(initialDate);
|
||||
});
|
||||
|
||||
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
const initialDate = recipient.purchased.plan.dateCurrentTypeCreated;
|
||||
await api.createSubscription(data);
|
||||
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
|
||||
});
|
||||
|
||||
it('does not change plan.customerId if it already exists', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
data.customerId = 'purchaserCustomerId';
|
||||
@@ -213,6 +235,116 @@ describe('payments/index', () => {
|
||||
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 1;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = -1;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
recipient.purchased.plan.customerId = undefined;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 1;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
data.sub.key = 'basic_earned';
|
||||
data.gift.subscription.key = 'basic_earned';
|
||||
data.gift.subscription.months = 1;
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
|
||||
recipient.purchased.plan.perkMonthCount = 0;
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('awards perks if plan.perkMonthCount goes over 3', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
recipient.purchased.plan.perkMonthCount = 2;
|
||||
data.sub.key = 'basic_earned';
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
|
||||
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
});
|
||||
|
||||
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
|
||||
expect(recipient.purchased.plan.customerId).to.not.exist;
|
||||
|
||||
@@ -379,6 +511,7 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
expect(user.purchased.plan.dateUpdated).to.exist;
|
||||
expect(user.purchased.plan.gemsBought).to.eql(0);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
expect(user.purchased.plan.dateTerminated).to.eql(null);
|
||||
@@ -386,6 +519,63 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(user.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
|
||||
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.dateCurrentTypeCreated).to.exist;
|
||||
});
|
||||
|
||||
it('keeps plan.dateCreated when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
const initialDate = user.purchased.plan.dateCreated;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.dateCreated).to.eql(initialDate);
|
||||
});
|
||||
|
||||
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
const initialDate = user.purchased.plan.dateCurrentTypeCreated;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
|
||||
});
|
||||
|
||||
it('keeps plan.perkMonthCount when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(2);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.perkMonthCount).to.eql(0);
|
||||
});
|
||||
|
||||
it('updates plan.consecutive.offset when changing subscription type', async () => {
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(3);
|
||||
data.sub.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.offset).to.eql(6);
|
||||
});
|
||||
|
||||
it('awards the Royal Purple Jackalope pet', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -465,6 +655,89 @@ describe('payments/index', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
const created = user.purchased.plan.dateCreated;
|
||||
const updated = user.purchased.plan.dateUpdated;
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom = { key: 'basic_earned' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
|
||||
it('from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
const created = user.purchased.plan.dateCreated;
|
||||
const updated = user.purchased.plan.dateUpdated;
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom = { key: 'basic_3mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
const created = user.purchased.plan.dateCreated;
|
||||
const updated = user.purchased.plan.dateUpdated;
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
|
||||
it('from basic_12mo to basic_3mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
const created = user.purchased.plan.dateCreated;
|
||||
const updated = user.purchased.plan.dateUpdated;
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.dateCreated).to.eql(created);
|
||||
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
|
||||
expect(user.purchased.plan.customerId).to.eql('customer-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Block subscription perks', () => {
|
||||
@@ -488,7 +761,6 @@ describe('payments/index', () => {
|
||||
|
||||
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
@@ -496,7 +768,6 @@ describe('payments/index', () => {
|
||||
|
||||
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
@@ -532,6 +803,532 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
context('Using payDifference logic', () => {
|
||||
beforeEach(async () => {
|
||||
data.updatedFrom = { logic: 'payDifference' };
|
||||
});
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
});
|
||||
|
||||
context('Using payFull logic', () => {
|
||||
beforeEach(async () => {
|
||||
data.updatedFrom = { logic: 'payFull' };
|
||||
});
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
});
|
||||
});
|
||||
|
||||
context('Using refundAndRepay logic', () => {
|
||||
let clock;
|
||||
beforeEach(async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-01'));
|
||||
data.updatedFrom = { logic: 'refundAndRepay' };
|
||||
});
|
||||
context('Upgrades within first half of subscription', () => {
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-10'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-02-05'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-08'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-31'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-08'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-08-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
|
||||
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-07-31'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
});
|
||||
context('Upgrades within second half of subscription', () => {
|
||||
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-20'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-02-24'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-01-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-03-03'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
});
|
||||
|
||||
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom.key = 'basic_earned';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2022-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_6mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2023-05-28'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
|
||||
});
|
||||
|
||||
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_3mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
data.updatedFrom.key = 'basic_3mo';
|
||||
clock.restore();
|
||||
clock = sinon.useFakeTimers(new Date('2023-09-03'));
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
if (clock !== null) clock.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_6mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.planId).to.eql('basic_earned');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
|
||||
});
|
||||
|
||||
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
data.sub.key = 'basic_12mo';
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.planId).to.eql('basic_12mo');
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
|
||||
data.sub.key = 'basic_3mo';
|
||||
data.updatedFrom = { key: 'basic_12mo' };
|
||||
await api.createSubscription(data);
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Mystery Items', () => {
|
||||
|
||||
40
test/api/unit/libs/payments/skuItem.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
canBuySkuItem,
|
||||
} from '../../../../../website/server/libs/payments/skuItem';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
|
||||
describe('payments/skuItems', () => {
|
||||
let user;
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
user = new User();
|
||||
clock = null;
|
||||
});
|
||||
afterEach(() => {
|
||||
if (clock !== null) clock.restore();
|
||||
});
|
||||
|
||||
describe('#canBuySkuItem', () => {
|
||||
it('returns true for random sku', () => {
|
||||
expect(canBuySkuItem('something', user)).to.be.true;
|
||||
});
|
||||
|
||||
describe('#gryphatrice', () => {
|
||||
const sku = 'Pet-Gryphatrice-Jubilant';
|
||||
it('returns true during birthday week', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-01-31'));
|
||||
expect(canBuySkuItem(sku, user)).to.be.true;
|
||||
});
|
||||
it('returns false outside of birthday week', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-01-20'));
|
||||
expect(canBuySkuItem(sku, user)).to.be.false;
|
||||
});
|
||||
it('returns false if user already owns it', () => {
|
||||
clock = sinon.useFakeTimers(new Date('2023-02-01'));
|
||||
user.items.pets['Gryphatrice-Jubilant'] = 5;
|
||||
expect(canBuySkuItem(sku, user)).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1359,6 +1359,7 @@ describe('Group Model', () => {
|
||||
describe('#sendChat', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.spy(User, 'update');
|
||||
sandbox.spy(User, 'updateMany');
|
||||
});
|
||||
|
||||
it('formats message', () => {
|
||||
@@ -1413,8 +1414,8 @@ describe('Group Model', () => {
|
||||
it('updates users about new messages in party', () => {
|
||||
party.sendChat({ message: 'message' });
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
expect(User.updateMany).to.be.calledOnce;
|
||||
expect(User.updateMany).to.be.calledWithMatch({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: '' },
|
||||
});
|
||||
@@ -1427,8 +1428,8 @@ describe('Group Model', () => {
|
||||
|
||||
group.sendChat({ message: 'message' });
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
expect(User.updateMany).to.be.calledOnce;
|
||||
expect(User.updateMany).to.be.calledWithMatch({
|
||||
guilds: group._id,
|
||||
_id: { $ne: '' },
|
||||
});
|
||||
@@ -1437,8 +1438,8 @@ describe('Group Model', () => {
|
||||
it('does not send update to user that sent the message', () => {
|
||||
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
|
||||
|
||||
expect(User.update).to.be.calledOnce;
|
||||
expect(User.update).to.be.calledWithMatch({
|
||||
expect(User.updateMany).to.be.calledOnce;
|
||||
expect(User.updateMany).to.be.calledWithMatch({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: 'user-id' },
|
||||
});
|
||||
|
||||
@@ -541,6 +541,35 @@ describe('POST /chat', () => {
|
||||
.to.eql(userWithStyle.preferences.background);
|
||||
});
|
||||
|
||||
it('creates equipped to user styles', async () => {
|
||||
const userWithStyle = await generateUser({
|
||||
'preferences.costume': false,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
await userWithStyle.sync();
|
||||
|
||||
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.userStyles.items.gear.equipped)
|
||||
.to.eql(userWithStyle.items.gear.equipped);
|
||||
expect(message.message.userStyles.items.gear.costume).to.not.exist;
|
||||
});
|
||||
|
||||
it('creates costume to user styles', async () => {
|
||||
const userWithStyle = await generateUser({
|
||||
'preferences.costume': true,
|
||||
'auth.timestamps.created': new Date('2022-01-01'),
|
||||
});
|
||||
await userWithStyle.sync();
|
||||
|
||||
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
|
||||
|
||||
expect(message.message.id).to.exist;
|
||||
expect(message.message.userStyles.items.gear.costume).to.eql(userWithStyle.items.gear.costume);
|
||||
expect(message.message.userStyles.items.gear.equipped).to.not.exist;
|
||||
});
|
||||
|
||||
it('adds backer info to chat', async () => {
|
||||
const backerInfo = {
|
||||
npc: 'Town Crier',
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
type: 'party',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
members: 2,
|
||||
});
|
||||
|
||||
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
|
||||
@@ -76,12 +76,17 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
|
||||
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/flag`);
|
||||
|
||||
// first test that the flag was actually successful
|
||||
// author always sees own message; flag count is hidden from non-admins
|
||||
let messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
expect(messages[0].flagCount).to.eql(5);
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
messages = await members[1].get(`/groups/${group._id}/chat`);
|
||||
expect(messages.length).to.eql(0);
|
||||
|
||||
// admin cannot directly request private group chat, but after unflag,
|
||||
// message should be revealed again and still have flagCount of 0
|
||||
await admin.post(`/groups/${group._id}/chat/${privateMessage.id}/clearflags`);
|
||||
|
||||
messages = await members[0].get(`/groups/${group._id}/chat`);
|
||||
messages = await members[1].get(`/groups/${group._id}/chat`);
|
||||
expect(messages.length).to.eql(1);
|
||||
expect(messages[0].flagCount).to.eql(0);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,19 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when recipient has blocked the senders', async () => {
|
||||
const inviterNoBlocks = await inviter.update({ 'inbox.blocks': [] });
|
||||
const userWithBlockedInviter = await generateUser({ 'inbox.blocks': [inviter._id] });
|
||||
await expect(inviterNoBlocks.post(`/groups/${group._id}/invite`, {
|
||||
usernames: [userWithBlockedInviter.auth.local.lowerCaseUsername],
|
||||
}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('notAuthorizedToSendMessageToThisUser'),
|
||||
});
|
||||
});
|
||||
|
||||
it('invites a user to a group by username', async () => {
|
||||
const userToInvite = await generateUser();
|
||||
|
||||
|
||||
@@ -45,11 +45,10 @@ describe('payments : apple #subscribe', () => {
|
||||
});
|
||||
|
||||
expect(subscribeStub).to.be.calledOnce;
|
||||
expect(subscribeStub.args[0][0]).to.eql(sku);
|
||||
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][2]).to.eql('receipt');
|
||||
expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][0]._id).to.eql(user._id);
|
||||
expect(subscribeStub.args[0][1]).to.eql('receipt');
|
||||
expect(subscribeStub.args[0][2]['x-api-key']).to.eql(user.apiToken);
|
||||
expect(subscribeStub.args[0][2]['x-api-user']).to.eql(user._id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,11 +21,11 @@ describe('payments : apple #verify', () => {
|
||||
let verifyStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
verifyStub = sinon.stub(applePayments, 'verifyGemPurchase').resolves({});
|
||||
verifyStub = sinon.stub(applePayments, 'verifyPurchase').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
applePayments.verifyGemPurchase.restore();
|
||||
applePayments.verifyPurchase.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
|
||||
@@ -21,11 +21,11 @@ describe('payments : google #verify', () => {
|
||||
let verifyStub;
|
||||
|
||||
beforeEach(async () => {
|
||||
verifyStub = sinon.stub(googlePayments, 'verifyGemPurchase').resolves({});
|
||||
verifyStub = sinon.stub(googlePayments, 'verifyPurchase').resolves({});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
googlePayments.verifyGemPurchase.restore();
|
||||
googlePayments.verifyPurchase.restore();
|
||||
});
|
||||
|
||||
it('makes a purchase', async () => {
|
||||
|
||||
@@ -96,6 +96,20 @@ describe('PUT /user/auth/update-password', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when newPassword is too long', async () => {
|
||||
const body = {
|
||||
password,
|
||||
newPassword: '12345678910111213141516171819202122232425262728293031323334353637383940',
|
||||
confirmPassword: '12345678910111213141516171819202122232425262728293031323334353637383940',
|
||||
};
|
||||
|
||||
await expect(user.put(ENDPOINT, body)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when confirmPassword is missing', async () => {
|
||||
const body = {
|
||||
password,
|
||||
|
||||
@@ -35,13 +35,6 @@ describe('GET /world-state', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a string representing the current season for NPC sprites', async () => {
|
||||
const res = await requester().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);
|
||||
|
||||
@@ -37,6 +37,8 @@ describe('GET /faq', () => {
|
||||
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0]).to.eql({
|
||||
exclusions: [],
|
||||
heading: 'overview',
|
||||
question: translate('faqQuestion0'),
|
||||
ios: translate('iosFaqAnswer0'),
|
||||
});
|
||||
@@ -57,6 +59,8 @@ describe('GET /faq', () => {
|
||||
|
||||
expect(res).to.have.property('questions');
|
||||
expect(res.questions[0]).to.eql({
|
||||
exclusions: [],
|
||||
heading: 'overview',
|
||||
question: translate('faqQuestion0'),
|
||||
android: translate('androidFaqAnswer0'),
|
||||
});
|
||||
|
||||
@@ -215,6 +215,7 @@ describe('cron utility functions', () => {
|
||||
|
||||
it('monthly plan, next date in 3 months', () => {
|
||||
const user = baseUserData(60, 0, 'group_plan_auto');
|
||||
user.purchased.plan.perkMonthCount = 0;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
@@ -224,6 +225,7 @@ describe('cron utility functions', () => {
|
||||
|
||||
it('monthly plan, next date in 1 month', () => {
|
||||
const user = baseUserData(62, 0, 'group_plan_auto');
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
@@ -248,5 +250,15 @@ describe('cron utility functions', () => {
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
||||
});
|
||||
|
||||
it('multi-month plan with perk count', () => {
|
||||
const user = baseUserData(60, 1, 'basic_3mo');
|
||||
user.purchased.plan.perkMonthCount = 2;
|
||||
|
||||
const planContext = getPlanContext(user, now);
|
||||
|
||||
expect(planContext.nextHourglassDate)
|
||||
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,8 +12,9 @@ const webhookData = {};
|
||||
|
||||
app.use(bodyParser.urlencoded({
|
||||
extended: true,
|
||||
limit: '10mb',
|
||||
}));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.json({ limit: '10mb' }));
|
||||
|
||||
app.post('/webhooks/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
13066
website/client/package-lock.json
generated
@@ -18,44 +18,44 @@
|
||||
"@storybook/addon-links": "6.5.8",
|
||||
"@storybook/addon-notes": "5.3.21",
|
||||
"@storybook/addons": "6.5.9",
|
||||
"@storybook/vue": "6.3.13",
|
||||
"@vue/cli-plugin-babel": "^4.5.15",
|
||||
"@storybook/vue": "6.5.14",
|
||||
"@vue/cli-plugin-babel": "^5.0.8",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-plugin-router": "^5.0.8",
|
||||
"@vue/cli-plugin-unit-mocha": "^4.5.15",
|
||||
"@vue/cli-plugin-unit-mocha": "^5.0.8",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"amplitude-js": "^8.21.1",
|
||||
"amplitude-js": "^8.21.3",
|
||||
"axios": "^0.27.2",
|
||||
"axios-progress-bar": "^1.2.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"chai": "^4.3.6",
|
||||
"core-js": "^3.26.0",
|
||||
"dompurify": "^2.4.1",
|
||||
"bootstrap-vue": "^2.23.1",
|
||||
"chai": "^4.3.7",
|
||||
"core-js": "^3.27.2",
|
||||
"dompurify": "^2.4.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"habitica-markdown": "^3.0.0",
|
||||
"hellojs": "^1.19.5",
|
||||
"hellojs": "^1.20.0",
|
||||
"inspectpack": "^4.7.1",
|
||||
"intro.js": "^6.0.0",
|
||||
"jquery": "^3.6.1",
|
||||
"jquery": "^3.6.3",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"nconf": "^0.12.0",
|
||||
"sass": "^1.34.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.19.1",
|
||||
"stopword": "^2.0.5",
|
||||
"stopword": "^2.0.7",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^7.1.1",
|
||||
"svgo": "^1.3.2",
|
||||
"svgo-loader": "^2.2.1",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.7.0",
|
||||
"validator": "^13.9.0",
|
||||
"vue": "^2.7.10",
|
||||
"vue-cli-plugin-storybook": "2.1.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
@@ -66,6 +66,6 @@
|
||||
"webpack": "^4.46.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.18.9"
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.20.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<sub-canceled-modal v-if="isUserLoaded" />
|
||||
<bug-report-modal v-if="isUserLoaded" />
|
||||
<bug-report-success-modal v-if="isUserLoaded" />
|
||||
<birthday-modal />
|
||||
<snackbars />
|
||||
<router-view v-if="!isUserLoggedIn || isStaticPage" />
|
||||
<template v-else>
|
||||
@@ -42,6 +43,7 @@
|
||||
<damage-paused-banner />
|
||||
<gems-promo-banner />
|
||||
<gift-promo-banner />
|
||||
<birthday-banner />
|
||||
<notifications-display />
|
||||
<app-menu />
|
||||
<div
|
||||
@@ -153,11 +155,13 @@
|
||||
import axios from 'axios';
|
||||
import { loadProgressBar } from 'axios-progress-bar';
|
||||
|
||||
import birthdayModal from '@/components/news/birthdayModal';
|
||||
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 GiftPromoBanner from './components/header/banners/giftPromo';
|
||||
import BirthdayBanner from './components/header/banners/birthdayBanner';
|
||||
import AppFooter from './components/appFooter';
|
||||
import notificationsDisplay from './components/notifications';
|
||||
import snackbars from './components/snackbars/notifications';
|
||||
@@ -191,9 +195,11 @@ export default {
|
||||
AppMenu,
|
||||
AppHeader,
|
||||
AppFooter,
|
||||
birthdayModal,
|
||||
DamagePausedBanner,
|
||||
GemsPromoBanner,
|
||||
GiftPromoBanner,
|
||||
BirthdayBanner,
|
||||
notificationsDisplay,
|
||||
snackbars,
|
||||
BuyModal,
|
||||
|
||||
@@ -156,6 +156,12 @@
|
||||
height: 99px;
|
||||
}
|
||||
|
||||
.Pet-Gryphatrice-Jubilant {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice, .Mount_Body_Gryphon-Gryphatrice {
|
||||
width: 135px;
|
||||
height: 135px;
|
||||
|
||||
BIN
website/client/src/assets/images/10-birthday.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 8.5 KiB |
BIN
website/client/src/assets/images/fancy-divider.png
Normal file
|
After Width: | Height: | Size: 332 B |
BIN
website/client/src/assets/images/glitter.png
Normal file
|
After Width: | Height: | Size: 358 B |
BIN
website/client/src/assets/images/habitica-hero-goober.webp
Normal file
|
After Width: | Height: | Size: 850 B |
BIN
website/client/src/assets/images/robes.webp
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
@@ -19,8 +19,12 @@
|
||||
top: -16px !important;
|
||||
}
|
||||
|
||||
.Pet.Pet-FlyingPig-Veggie, .Pet.Pet-FlyingPig-Dessert, .Pet.Pet-FlyingPig-VirtualPet {
|
||||
top: -28px !important;
|
||||
$foolPets: Veggie, Dessert, VirtualPet, TeaShop;
|
||||
|
||||
@each $foolPet in $foolPets {
|
||||
.Pet.Pet-FlyingPig-#{$foolPet} {
|
||||
top: -28px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.Pet[class*="Virtual"] {
|
||||
|
||||
@@ -50,10 +50,7 @@ h3.markdown {
|
||||
}
|
||||
|
||||
a {
|
||||
color: $blue-10;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
color: $blue-10;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,19 +26,17 @@ a:not([href]), a:not([href]):hover {
|
||||
|
||||
a, a:not([href]):not([tabindex]) {
|
||||
cursor: pointer;
|
||||
color: $purple-300;
|
||||
|
||||
&.standard-link {
|
||||
color: $blue-10;
|
||||
&:hover, &:active, &:focus {
|
||||
text-decoration: underline;
|
||||
color: $purple-300;
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&[disabled="disabled"] {
|
||||
color: $gray-300;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
&[disabled="disabled"] {
|
||||
color: $gray-300;
|
||||
text-decoration: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.small-link {
|
||||
|
||||
61
website/client/src/assets/svg/10th-birthday-linear.svg
Normal file
@@ -0,0 +1,61 @@
|
||||
<svg width="199" height="24" viewBox="0 0 199 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#c19w6aye5a)" fill="#fff">
|
||||
<path d="M56.47 18.83V6.003L56 3.662l.47-1.405h8.942c1.773 0 3.193.344 4.26 1.03 1.066.687 1.6 1.733 1.6 3.137 0 .765-.142 1.397-.424 1.896a4.175 4.175 0 0 1-1.035 1.24 4.14 4.14 0 0 1 1.505.703c.471.327.855.772 1.154 1.334.297.546.447 1.225.447 2.036 0 1.639-.487 2.918-1.46 3.839-.956.905-2.502 1.358-4.635 1.358H56.471zm5.177-10.136h2.777c.533 0 .918-.078 1.153-.234.235-.171.353-.429.353-.772 0-.359-.173-.609-.518-.75-.345-.155-.753-.233-1.223-.233h-2.542v1.99zm0 5.688h4c.628 0 1.067-.093 1.318-.28a.974.974 0 0 0 .377-.796c0-.75-.486-1.124-1.46-1.124h-4.235v2.2zM75.515 18.83V6.003l-.236-2.341.236-1.405h5.2V18.83h-5.2zM84 18.83V6.026l-.471-2.364.47-1.405h8.472c1.27 0 2.36.203 3.27.61.91.405 1.608 1.06 2.095 1.965.486.89.73 2.068.73 3.535 0 1.357-.228 2.473-.683 3.347a4.552 4.552 0 0 1-2 1.99l.4.327 1.623 2.2 1.341 1.194v1.405h-6.235l-2.588-4.284h-1.248v.515l.236 2.34-.236 1.429H84zm5.176-8.66h1.365c.55 0 1.02-.024 1.412-.071.408-.047.722-.187.941-.421.22-.25.33-.648.33-1.194 0-.578-.118-.991-.353-1.24-.236-.25-.565-.399-.989-.446a9.614 9.614 0 0 0-1.435-.093h-1.506l.235 3.464zM104.666 18.83V6.728l-4.706.234V2.257h14.707v4.705l-4.824-.234v8.357l.259 2.34-.259 1.405h-5.177zM116.785 18.83V6.026l-.235-2.34.235-1.429h5.177v6.344h4.918V2.257h5.177v8.802l.235 1.615v6.156h-5.412v-5.946h-4.918v1.639l.235 4.307h-5.412zM135.588 18.83V6.026l-.471-2.34.471-1.429h7.977c1.114 0 2.188.11 3.223.328 1.051.203 1.985.593 2.801 1.17.831.578 1.49 1.405 1.976 2.482.486 1.076.73 2.473.73 4.19 0 1.716-.251 3.128-.753 4.236-.487 1.092-1.146 1.943-1.977 2.552a7.477 7.477 0 0 1-2.8 1.264 14.463 14.463 0 0 1-3.2.35h-7.977zm5.224-4.448h1.788c.926 0 1.702-.101 2.33-.304a2.498 2.498 0 0 0 1.458-1.147c.33-.577.495-1.412.495-2.504 0-1.108-.173-1.92-.518-2.435-.33-.53-.816-.874-1.459-1.03-.628-.171-1.396-.257-2.306-.257h-1.788v7.677zM153.013 18.83l1.13-3.956V11.9l1.741-.702 3.294-8.918h7.083l4.024 10.486 1.812 3.324v2.739h-5.106l-1.177-3.488h-6.377l-1.012 3.488h-5.412zm7.977-7.584h3.53l-1.553-4.658h-.494l-1.483 4.658zM176.04 18.83v-6.788l-5.835-8.38V2.257h5.906l2.353 4.822h.47l2.33-4.822h5.883v1.405l-6.001 8.52.141 2.364v4.284h-5.247zM191.923 12.72l-2.07-8.847L192.676 2l2.8 1.896-2.141 8.824h-1.412zm.518 7.28-3.059-3.043 3.059-3.043 3.059 3.043L192.441 20z"/>
|
||||
</g>
|
||||
<g filter="url(#s1alkvv8kb)">
|
||||
<path d="M5.87 18.825V7.601H3V3.17l8.228-.937.239 1.406-.24 2.344v12.841H5.87z" fill="url(#xidihnl5xc)"/>
|
||||
<path d="M21.258 19.06a9.043 9.043 0 0 1-2.87-.446 6.484 6.484 0 0 1-2.369-1.453c-.67-.671-1.195-1.546-1.578-2.624-.383-1.094-.574-2.43-.574-4.007 0-1.562.191-2.883.574-3.96.382-1.094.909-1.977 1.578-2.648a6.092 6.092 0 0 1 2.368-1.453A8.63 8.63 0 0 1 21.257 2c1.356 0 2.584.281 3.684.844 1.116.562 2.001 1.468 2.655 2.718.67 1.234 1.004 2.89 1.004 4.968s-.335 3.741-1.004 4.991c-.654 1.25-1.539 2.156-2.655 2.718-1.1.547-2.328.82-3.683.82zm0-5.039c.701 0 1.187-.25 1.459-.75.27-.5.406-1.413.406-2.741 0-1.313-.136-2.219-.407-2.719-.27-.515-.757-.773-1.459-.773-.685 0-1.18.258-1.483.773-.287.516-.43 1.422-.43 2.719 0 1.312.143 2.226.43 2.742.303.5.798.75 1.483.75z" fill="url(#9hqzmmkygd)"/>
|
||||
<path d="M32.721 12.014V4.745l-2.87.14V2.06h8.97v2.826l-2.943-.141v5.02l.158 1.405-.158.844h-3.157z" fill="url(#bzq8gpt5ve)"/>
|
||||
<path d="M40.543 12.014v-7.69l-.144-1.407.144-.857H43.7v3.81h3V2.06h3.156v5.286l.144.97v3.698h-3.3V8.443h-3v.984l.143 2.587h-3.3z" fill="url(#4t6arxwa4f)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="xidihnl5xc" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6133B4"/>
|
||||
<stop offset="1" stop-color="#4F2A93"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="9hqzmmkygd" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6133B4"/>
|
||||
<stop offset="1" stop-color="#4F2A93"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="bzq8gpt5ve" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6133B4"/>
|
||||
<stop offset="1" stop-color="#4F2A93"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="4t6arxwa4f" x1="3" y1="2" x2="29.822" y2="35.308" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6133B4"/>
|
||||
<stop offset="1" stop-color="#4F2A93"/>
|
||||
</linearGradient>
|
||||
<filter id="c19w6aye5a" x="53" y="0" width="145.5" height="24" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.12 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_45_799"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.24 0"/>
|
||||
<feBlend in2="effect1_dropShadow_45_799" result="effect2_dropShadow_45_799"/>
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_45_799" result="shape"/>
|
||||
</filter>
|
||||
<filter id="s1alkvv8kb" x="0" y="0" width="53" height="23.059" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.12 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_45_799"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix values="0 0 0 0 0.101961 0 0 0 0 0.0941176 0 0 0 0 0.113725 0 0 0 0.24 0"/>
|
||||
<feBlend in2="effect1_dropShadow_45_799" result="effect2_dropShadow_45_799"/>
|
||||
<feBlend in="SourceGraphic" in2="effect2_dropShadow_45_799" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
22
website/client/src/assets/svg/birthday-gems.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg width="58" height="48" viewBox="0 0 58 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.853 4.36 7.959-1.453-2.71 7.556-2.708 7.557-5.25-6.103-5.25-6.103 7.959-1.453z" fill="#5DDEAB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.771 1.454 40.731 0l-2.71 7.556-2.709 7.556-5.25-6.102-5.25-6.103 7.96-1.453z" fill="#5DDEAB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m43.272 13.659-7.96 1.453 2.71-7.556L40.73 0l5.25 6.103 5.25 6.102-7.96 1.454z" fill="#38C38D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m27.353 16.566-7.96 1.453 2.71-7.556 2.709-7.556 5.25 6.103 5.25 6.102-7.96 1.454zM11.434 19.473l-7.959 1.453 2.71-7.556 2.708-7.556 5.25 6.103 5.25 6.102-7.959 1.454z" fill="#B0F1D7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m3.475 20.926 28.05 18.662L19.394 18.02 3.475 20.926z" fill="#38C38D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.249 12.202 31.525 39.588l3.805-24.48 15.919-2.906z" fill="#B0F1D7"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m19.394 18.02 12.131 21.568 3.787-24.476-15.918 2.907z" fill="#5DDEAB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m51.904 26.44-3.832-.897 1.132 3.736 1.132 3.737 2.7-2.84 2.7-2.84-3.832-.896zM44.24 24.647l-3.832-.897 1.132 3.736 1.132 3.736 2.7-2.84 2.7-2.839-3.832-.896z" fill="#87E3E1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m38.84 30.326 3.832.896-1.132-3.736-1.132-3.736-2.7 2.84-2.7 2.839 3.832.897zM46.504 32.12l3.832.896-1.132-3.736-1.132-3.737-2.7 2.84-2.7 2.84 3.832.896z" fill="#C0FBFA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.168 33.912 58 34.81l-1.132-3.736-1.133-3.736-2.7 2.84-2.7 2.839 3.833.896z" fill="#5EC5C2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m58 34.81-14.084 8.395 6.42-10.19L58 34.81z" fill="#C0FBFA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m35 29.427 8.916 13.779-1.252-11.986L35 29.427z" fill="#5EC5C2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m50.336 33.016-6.42 10.19-1.244-11.984 7.664 1.794z" fill="#87E3E1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.877 22.666-5.078 1.971 4.262 3.372 4.262 3.37.816-5.341.816-5.343-5.078 1.971zM6.721 26.609l-5.078 1.97 4.262 3.372 4.262 3.371.816-5.342.816-5.343-5.078 1.972z" fill="#7BE3CF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m5.09 37.294 5.077-1.972-4.262-3.371-4.261-3.371-.817 5.342-.816 5.343 5.078-1.971z" fill="#C5F3EA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m15.245 33.351 5.078-1.971-4.262-3.371-4.262-3.371-.816 5.342-.816 5.342 5.078-1.97z" fill="#C5F3EA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m25.4 29.41 5.078-1.972-4.262-3.371-4.261-3.372-.816 5.343-.816 5.342 5.078-1.97z" fill="#41C7AF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.478 27.438 21.117 48l-.794-16.62 10.155-3.942z" fill="#C5F3EA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m0 39.269 21.117 8.73-10.961-12.672L0 39.269z" fill="#41C7AF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.323 31.38 21.117 48l-10.95-12.678 10.156-3.942z" fill="#7BE3CF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
22
website/client/src/assets/svg/confetti.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg width="518" height="152" viewBox="0 0 518 152" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M144.48 65.487v5.042h-1.772v-5.042h1.772zm1.621 6.671h5.013v1.782h-5.013v-1.782zm-10.027 0h5.013v1.782h-5.013v-1.782zm8.406 3.412v5.041h-1.772V75.57h1.772z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".5"/>
|
||||
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m9.504 29.894 2.707-4.715 1.658.962-2.707 4.715-1.658-.962zm2.066-7.12-4.689-2.722.958-1.667 4.688 2.723-.957 1.667zm9.378 5.445-4.689-2.722.957-1.667 4.69 2.722-.958 1.667zm-6.03-7.755 2.707-4.715 1.658.962-2.707 4.715-1.658-.962z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m60.85 11.508.708-3.662 1.288.251-.708 3.662-1.288-.252zm-.24-5.076-3.642-.712.25-1.295 3.642.712-.25 1.295zm7.283 1.423-3.642-.711.25-1.295 3.642.712-.25 1.294zm-5.627-3.671.708-3.662 1.287.251-.708 3.662-1.287-.251z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".81"/>
|
||||
<path opacity=".76" fill-rule="evenodd" clip-rule="evenodd" d="m107.034 22.162.493 5.675-1.995.175-.494-5.674 1.996-.176zm2.477 7.349 5.643-.497.175 2.007-5.644.496-.174-2.006zm-11.287.993 5.644-.497.174 2.007-5.643.496-.175-2.006zm9.797 3.008.494 5.675-1.996.175-.493-5.675 1.995-.175z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m16.191 92.492-5.006 1.636-.575-1.78 5.006-1.636.575 1.78zm-6.098 3.792 1.626 5.034-1.77.578-1.626-5.034 1.77-.578zM6.839 86.215l1.627 5.035-1.77.578-1.627-5.034 1.77-.579zm-.66 9.549-5.006 1.635-.576-1.78 5.007-1.635.575 1.78z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".91"/>
|
||||
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m35.176 59.176 5.102-1.97.692 1.814-5.101 1.97-.693-1.814zm6.118-4.264-1.958-5.13 1.803-.696 1.958 5.13-1.803.696zm3.916 10.26-1.958-5.13 1.804-.696 1.958 5.13-1.804.696zm.17-9.935 5.1-1.969.693 1.814-5.101 1.969-.693-1.814z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m95.733 86.583-4.383 2.649-.931-1.559 4.383-2.648.93 1.558zm-4.949 4.93 2.634 4.407-1.55.936-2.633-4.407 1.55-.937zm-5.267-8.816 2.633 4.408-1.55.936-2.633-4.408 1.55-.936zm1.45 9.183-4.384 2.648-.93-1.558 4.382-2.648.931 1.558z" fill="#36205D" style="mix-blend-mode:multiply"/>
|
||||
<path opacity=".98" fill-rule="evenodd" clip-rule="evenodd" d="m24.804 132.406-2.1-3.015 1.06-.746 2.1 3.014-1.06.747zm-3.747-3.307-2.998 2.111-.742-1.066 2.998-2.111.742 1.066zm5.996-4.222-2.998 2.111-.742-1.066 2.998-2.11.742 1.065zm-6.447 1.5-2.1-3.015 1.06-.746 2.1 3.014-1.06.747z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m60.65 142.295-1.594 3.144-1.105-.567 1.593-3.144 1.105.567zm-1.098 4.678 3.126 1.602-.563 1.112-3.127-1.602.564-1.112zm-6.254-3.204 3.127 1.602-.563 1.112-3.127-1.603.563-1.111zm4.165 4.814-1.593 3.144-1.106-.566 1.593-3.144 1.106.566z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".82"/>
|
||||
<path opacity=".71" fill-rule="evenodd" clip-rule="evenodd" d="m110.507 140.233 2.321-4.582 1.611.826-2.321 4.581-1.611-.825zm1.599-6.817-4.556-2.335.821-1.62 4.556 2.335-.821 1.62zm9.112 4.669-4.556-2.335.821-1.62 4.556 2.335-.821 1.62zm-6.068-7.015 2.321-4.582 1.611.825-2.321 4.582-1.611-.825z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M373.52 65.487v5.042h1.772v-5.042h-1.772zm-1.621 6.671h-5.013v1.782h5.013v-1.782zm10.027 0h-5.013v1.782h5.013v-1.782zm-8.406 3.412v5.041h1.772V75.57h-1.772z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".5"/>
|
||||
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m508.496 29.894-2.707-4.715-1.658.962 2.707 4.715 1.658-.962zm-2.066-7.12 4.689-2.722-.958-1.667-4.689 2.723.958 1.667zm-9.378 5.445 4.689-2.722-.957-1.667-4.689 2.722.957 1.667zm6.03-7.755-2.707-4.715-1.658.962 2.707 4.715 1.658-.962z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m457.15 11.508-.708-3.662-1.287.251.707 3.662 1.288-.252zm.24-5.076 3.642-.712-.25-1.295-3.642.712.25 1.295zm-7.283 1.423 3.642-.711-.251-1.295-3.641.712.25 1.294zm5.627-3.671-.708-3.662-1.287.251.708 3.662 1.287-.251z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".81"/>
|
||||
<path opacity=".76" fill-rule="evenodd" clip-rule="evenodd" d="m410.966 22.162-.493 5.675 1.995.175.494-5.674-1.996-.176zm-2.477 7.349-5.643-.497-.175 2.007 5.644.496.174-2.006zm11.287.993-5.644-.497-.174 2.007 5.643.496.175-2.006zm-9.797 3.008-.494 5.675 1.996.175.493-5.675-1.995-.175z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m501.809 92.492 5.006 1.636.575-1.78-5.006-1.636-.575 1.78zm6.098 3.792-1.626 5.034 1.77.578 1.626-5.034-1.77-.578zm3.254-10.069-1.627 5.035 1.77.578 1.627-5.034-1.77-.579zm.66 9.549 5.006 1.635.576-1.78-5.007-1.635-.575 1.78z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".91"/>
|
||||
<path opacity=".92" fill-rule="evenodd" clip-rule="evenodd" d="m482.824 59.176-5.102-1.97-.692 1.814 5.101 1.97.693-1.814zm-6.118-4.264 1.958-5.13-1.803-.696-1.958 5.13 1.803.696zm-3.916 10.26 1.958-5.13-1.804-.696-1.958 5.13 1.804.696zm-.169-9.935-5.102-1.969-.692 1.814 5.101 1.969.693-1.814z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m422.267 86.583 4.383 2.649.932-1.559-4.384-2.648-.931 1.558zm4.949 4.93-2.634 4.407 1.55.936 2.634-4.407-1.55-.937zm5.267-8.816-2.633 4.408 1.549.936 2.634-4.408-1.55-.936zm-1.449 9.183 4.383 2.648.931-1.558-4.383-2.648-.931 1.558z" fill="#36205D" style="mix-blend-mode:multiply"/>
|
||||
<path opacity=".98" fill-rule="evenodd" clip-rule="evenodd" d="m493.196 132.406 2.099-3.015-1.06-.746-2.099 3.014 1.06.747zm3.747-3.307 2.998 2.111.742-1.066-2.998-2.111-.742 1.066zm-5.996-4.222 2.998 2.111.742-1.066-2.998-2.11-.742 1.065zm6.447 1.5 2.1-3.015-1.06-.746-2.099 3.014 1.059.747z" fill="#fff"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m457.351 142.295 1.593 3.144 1.105-.567-1.593-3.144-1.105.567zm1.097 4.678-3.126 1.602.563 1.112 3.127-1.602-.564-1.112zm6.254-3.204-3.127 1.602.563 1.112 3.127-1.603-.563-1.111zm-4.165 4.814 1.593 3.144 1.106-.566-1.593-3.144-1.106.566z" fill="#36205D" style="mix-blend-mode:multiply" opacity=".82"/>
|
||||
<path opacity=".71" fill-rule="evenodd" clip-rule="evenodd" d="m407.493 140.233-2.321-4.582-1.611.826 2.321 4.581 1.611-.825zm-1.599-6.817 4.556-2.335-.821-1.62-4.556 2.335.821 1.62zm-9.112 4.669 4.556-2.335-.821-1.62-4.556 2.335.821 1.62zm6.068-7.015-2.321-4.582-1.611.825 2.321 4.582 1.611-.825z" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
3
website/client/src/assets/svg/cross.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.26512 0L4.84341 3.57829L3.57829 4.84341L0 1.26512L1.26512 0ZM7.15659 3.57829L10.7349 5.33207e-08L12 1.26512L8.42171 4.84341L7.15659 3.57829ZM5.33207e-08 10.7349L3.57829 7.15659L4.84341 8.42171L1.26512 12L5.33207e-08 10.7349ZM8.42171 7.15659L12 10.7349L10.7349 12L7.15659 8.42171L8.42171 7.15659Z" fill="#FFB445"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 469 B |
4
website/client/src/assets/svg/divider.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="138" height="12" viewBox="0 0 138 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m127.265 0 3.578 3.578-1.265 1.265L126 1.265 127.265 0zm5.892 3.578L136.735 0 138 1.265l-3.578 3.578-1.265-1.265zM126 10.735l3.578-3.578 1.265 1.265L127.265 12 126 10.735zm8.422-3.578L138 10.735 136.735 12l-3.578-3.578 1.265-1.265z" fill="#FFB445"/>
|
||||
<path d="M114.445 4.555 112.5 1l-1.945 3.555L107.914 6h-3.828l-1.349-.737L101.5 3l-1.237 2.263L98.914 6H0v1h98.914l1.349.737L101.5 10l1.237-2.263L104.086 7h3.828l2.641 1.445L112.5 12l1.945-3.555L118 6.5l-3.555-1.945z" fill="#36205D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 647 B |
37
website/client/src/assets/svg/gifts-birthday.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg width="85" height="32" viewBox="0 0 85 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m4.93 12.255 2.466-.63-1.983-1.597-.63-2.468-1.595 1.986-2.465.63 1.983 1.597.63 2.468 1.595-1.986zM80.034 7.698l2.465-.63-1.983-1.597-.63-2.468-1.594 1.985-2.466.631 1.983 1.596.63 2.469 1.595-1.986zM42.27 7.427l2.929.487-1.368-2.638.487-2.932-2.635 1.37-2.928-.488 1.367 2.638-.486 2.932 2.634-1.37zM78.215 26.355l2.694 2.064.033-3.396 2.063-2.697-3.393-.034-2.694-2.065-.033 3.397-2.062 2.697 3.392.034zM38.321 28.092l2.092.348-.977-1.885.347-2.094-1.881.978-2.092-.348.977 1.884-.348 2.095 1.882-.978zM12.17 30.035l.916 1.915.981-1.882 1.913-.916-1.88-.982-.915-1.916-.981 1.882-1.913.917 1.88.982z" fill="#fff" fill-opacity=".5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m24.878 12.01 6.73-1.805 2.524 9.433-6.73 1.806-2.524-9.433z" fill="#F9F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m18.148 13.816 6.73-1.805 2.524 9.433-6.73 1.805-2.524-9.433z" fill="#E1E0E3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m23.532 12.372 1.346-.361 2.524 9.433-1.346.36-2.524-9.432z" fill="#6133B4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m24.878 12.01 1.346-.36 2.524 9.433-1.346.36-2.524-9.432z" fill="#9A62FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m25.696 20.457 1.345-.36.361 1.347-1.346.36-.36-1.347zM23.532 12.372l1.346-.361.36 1.347-1.346.361-.36-1.347z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m18.148 13.816 5.384-1.444.36 1.348-5.383 1.444-.36-1.348zM20.312 21.902l5.384-1.445.36 1.348-5.383 1.444-.36-1.347zM26.224 11.65l5.383-1.445.36 1.348-5.383 1.444-.36-1.347zM28.387 19.735l5.384-1.444.36 1.347-5.383 1.445-.36-1.348z" fill="#BDA8FF" fill-opacity=".3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m27.041 20.096 1.346-.36.361 1.347-1.346.36-.36-1.347zM24.878 12.01l1.346-.36.36 1.347-1.346.361-.36-1.347z" fill="#6133B4"/>
|
||||
<path clip-rule="evenodd" d="M24.735 4.954c-.335-1.183-1.148-2.301-2.285-2.51-1.138-.21-1.923.616-1.7 1.476.221.86 1 1.122 3.498 2.183.71.302.823.034.487-1.149z" stroke="#6133B4" stroke-width="1.5"/>
|
||||
<path clip-rule="evenodd" d="M27.66 5.365c.648-1.044 1.737-1.895 2.888-1.782 1.151.112 1.678 1.123 1.228 1.889-.45.765-1.27.802-3.964 1.133-.765.094-.8-.195-.152-1.24z" stroke="#9A62FF" stroke-width="1.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.319 4.294c-2.24-.315-1.259 2.44-.36 2.566.898.126 2.6-2.25.36-2.566z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m26.016 6.454 8.279 1.165-.582 4.145-8.279-1.165.582-4.145z" fill="#F9F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m17.737 5.29 8.279 1.164-.582 4.145-8.279-1.165.582-4.145z" fill="#E1E0E3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m23.256 6.066 5.52.777-.582 4.144-5.52-.776.582-4.145z" fill="#9A62FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m23.256 6.066 2.76.388-.582 4.145-2.76-.388.582-4.145z" fill="#6133B4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m26.016 6.454 2.76.389-.195 1.381-2.76-.388.195-1.382z" fill="#6133B4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m23.256 6.066 2.76.388-.194 1.382-2.76-.388.194-1.382z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m17.349 8.053 5.52.776-.195 1.382-5.519-.777.194-1.381zM28.388 9.606l5.519.776-.194 1.382-5.52-.777.195-1.381z" fill="#BDA8FF" fill-opacity=".3"/>
|
||||
<path clip-rule="evenodd" d="M56.55 9.16c-.624-1.413-1.83-2.662-3.282-2.724-1.452-.062-2.285 1.104-1.858 2.135.426 1.031 1.441 1.22 4.734 2.104.935.25 1.03-.102.406-1.515z" stroke="#6133B4" stroke-width="1.5"/>
|
||||
<path clip-rule="evenodd" d="M60.26 9.16c.624-1.413 1.83-2.662 3.283-2.724 1.451-.062 2.284 1.104 1.857 2.135-.426 1.031-1.44 1.22-4.734 2.104-.935.25-1.03-.102-.406-1.515z" stroke="#9A62FF" stroke-width="1.5"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.405 8.061c-2.842 0-1.14 3.256 0 3.256s2.842-3.256 0-3.256z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.405 10.802H68.91v5.259H58.405v-5.259z" fill="#F9F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.901 10.802h10.504v5.259H47.901v-5.259z" fill="#E1E0E3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.904 10.802h7.002v5.259h-7.002v-5.259z" fill="#9A62FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.904 10.802h3.501v5.259h-3.501v-5.259zM58.405 10.802h3.501v1.753h-3.5v-1.753z" fill="#6133B4"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.904 10.802h3.501v1.753h-3.501v-1.753z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.405 16.06h8.753v12.27h-8.753V16.06z" fill="#F9F9F9"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.652 16.06h8.753v12.27h-8.753V16.06z" fill="#E1E0E3"/>
|
||||
<path fill="#6133B4" d="M56.654 16.061h1.751v12.27h-1.751z"/>
|
||||
<path fill="#9A62FF" d="M58.405 16.061h1.751v12.27h-1.751z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M56.654 26.578h1.751v1.753h-1.75v-1.753zM56.654 16.06h1.751v1.754h-1.75V16.06z" fill="#4F2A93"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.652 16.06h7.002v1.754h-7.002V16.06z" fill="#BDA8FF" fill-opacity=".3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.901 14.308h7.003v1.753H47.9v-1.753zM49.652 26.578h7.002v1.753h-7.002v-1.753zM60.156 16.06h7.002v1.754h-7.002V16.06z" fill="#BDA8FF" fill-opacity=".3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M61.906 14.308h7.003v1.753h-7.003v-1.753zM60.156 26.578h7.002v1.753h-7.002v-1.753z" fill="#BDA8FF" fill-opacity=".3"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M58.405 26.578h1.75v1.753h-1.75v-1.753zM58.405 16.06h1.75v1.754h-1.75V16.06z" fill="#6133B4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path fill="url(#sxizdfpdya)" d="M0 0h68v68H0z"/>
|
||||
<defs>
|
||||
<pattern id="sxizdfpdya" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#pomapjzcdb" transform="scale(.0147)"/>
|
||||
</pattern>
|
||||
<image id="pomapjzcdb" width="68" height="68" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAABECAYAAAA4E5OyAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5gwJFx8lKwafmAAABjRJREFUeNrtm39MVWUYx78IV5BfF3Qg8kNppNIoyhKWZmUBlpRNpC3Uac7EUUGrscxM+AOHia1foKmDHOHItkbhLGnpzGVBimtlozmVTeByCRDhwgXll/THc17de3zvuedernDd3uefc895f5/zuc/3ed57DyBNmjRpzpuH+sLvR8fGAODR0nS+YmAgAMCQNQMAMJKwkys//+tUAMDQDTrPzNgIACj58iAAoLH5LADgx/Kzwols/zyba3+jny9n7WNmJ3LXffzoONUHTrVPyvDg7sEUyQRvXrYKrvX2AgCmK2RUxL9OT35oDzWs38bVf3XlNe78sfindE3gam/TpCw87fwPwuuSEL2EXMg6DgBY/HW6+E4OdQAA4lZ4CYnotVgBAEvShuiIRwAAXxV/y9Xbve+jSSHj2SfIl1RLQpwkRO1LmDEimJV+Qyqy+709wvZx/3wBAFhk3QQAONlYqKoxpGuijLDfvnduodmnTwIAUrM2ataThDhKSEp9FH2o38/5iopCuuO1/drqceAy+YiSAPI5m+rE9UvLcjTnweKfJWk7nVtppb5qkhB7hLAI01Y8wdSjyzLM9aCOJ9qrI+jDZTqkNlwBAByLixZOpMtk4uIeZrVrqkgVhtYL4x9m830iXXJDJCH2CGGqwXKB4gKxevw9eBgA0HDwIgAg94UdAICZL1JSkd5H5w2VVB4ZHg8A6IkkEoKMREKPhVTM/AHFPdNLxHFP6TnKdTIX8vOp7a8AABxWcpnVwZ9pLvjSlRYAwNzoKEmIS1TGlnqc2H1aWO5n8qSIFGsAAKdazVx53pR5pDog1SkMuF8z7rnD1yi+a4bRICy39vUBAPwDAhSVe5uy3Scp283+iUjE8ymSEJcQolaPVxZkkldPJRKiYmZqtl+3iy9vaWwk1agcJTUKJ18S9TI5gSu59ASjP07R9F0PW1YLV1BlzSMfhh1SZSaEkHe25ZMK1LQBANpa2wEAyTELxjUw64dZFOYJs21bxkjxcdAL1qzdCgBYXrlLEuJSlWFPdGlmuOIL6Lz/L4pITeZmIicnnH/SVV5cOfM9rJ9TpUyF5rl0YZXtebpISTr6viTEpXHIgy9NAwD4+voCAA4da+YIamn05OqbzKNc+bqMhwAAAwMDCiHudUMkIc4Swr7zakvf7698omNoaKiKqA6uXG+/kpB7jRCmBsEhscoVyikWrpoFAPAYJZ8wcqvjQQBAeKgRADDmST7n3HdXlRoGpV+KXFcVxWiO391F+zCY7dxC9baXhOglxPIL/UhqNlOEunKfNwAgInKYU5kLJ9Sbqjds9Ej15iYZOJVhVlfM9k3oERqf8ZE+xK0JYZElix96/iQ1iFRtXcYm+3E+5OaUafwdv3md8yEWi0WoMrcjVjKjiyNXSYirCWE5hzqrPVPA5ybZR4xc+d6VlzlfkKbakj20voMrj00f4fZNWI4k4xB3J4R9p5dmQjM3OX7AU3Of4+cy7XI/Ve7Dxk3eEiMJuSciVZbd3jZSjfkgdVi2KUyzvb1ydf+Tnf1KQvQSYisLTcyYodlh8ub7uPjDXnt1xGpr3M7OrnEtVG97SYg9QrYVPQcAWDywXZXdQslF9HXMIlZbpDBrrTOofAhlv7W+bwjr118sI082SL/wfZpfJ33IhBJiDAxWvtx0iFg0zMcR9bwqtGFM1cOAQ0NGLBLXvzUPdRbe2y1Vxi1UpiFsr/LpE+76rIQR7jzI3yD0Hepsl1mPdVilMrbGhSY51zt7JSFuEameKZjOZbfrKnxdOoGqLCuX/U62SUIcJaS8ZQN3fmItX74hqpwj6M1q/h9BJataOQLKW14TD9Sib8JMZT7MrZOEuAUhDhO0AuMiwJb919k0LjIu/UH7MMuXSUIcsjveuYt7IH7MHSdq7FkOAAgJpm3/t/Lpf6v23rlj/7Pt7DYJ28t37hwlJDHh8TEA6LcOCJ8QM3an7e5DKE/mDrUIquHO/fzF8Y2X6WnhdTb+lqJsISHs/R1b47P2R/7NkYQ4pDJhIXPoCXp3c/sOIR7OvW1gi6QRPyJimnegMLtlqhI+JwgAYG7qEZL37uatuubB+rltVqkyExKH3G1TkzHefhixhkBJiDRp0qTddfsfGCIUXNZsU4gAAAAASUVORK5CYII="/>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
1
website/client/src/assets/svg/new-close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" fill="none"/><g id="b"><g id="c"><g id="d"><polygon id="e" points="12.2 2 14 3.8 9.8 8 14 12.2 12.2 14 8 9.8 3.8 14 2 12.2 6.2 8 2 3.8 3.8 2 8 6.2 12.2 2"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 308 B |
5
website/client/src/assets/svg/stripe.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="48" height="20" viewBox="0 0 48 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M48 10.334c0-3.418-1.653-6.115-4.813-6.115-3.174 0-5.094 2.697-5.094 6.088 0 4.019 2.267 6.048 5.52 6.048 1.587 0 2.787-.36 3.694-.868v-2.67c-.907.454-1.947.735-3.267.735-1.293 0-2.44-.454-2.587-2.03h6.52c0-.173.027-.868.027-1.188zm-6.587-1.268c0-1.51.92-2.137 1.76-2.137.813 0 1.68.628 1.68 2.136h-3.44zM32.947 4.22c-1.307 0-2.147.613-2.614 1.04l-.173-.827h-2.933V20l3.333-.707.013-3.779c.48.347 1.187.841 2.36.841 2.387 0 4.56-1.922 4.56-6.155-.013-3.871-2.213-5.98-4.546-5.98zm-.8 9.198c-.787 0-1.254-.28-1.574-.627l-.013-4.954c.347-.387.827-.654 1.587-.654 1.213 0 2.053 1.362 2.053 3.11 0 1.79-.827 3.125-2.053 3.125zM22.64 3.431l3.346-.72V0L22.64.708V3.43z" fill="#635BFF"/>
|
||||
<path fill="#635BFF" d="M22.64 4.446h3.347v11.682H22.64z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m19.053 5.434-.213-.988h-2.88v11.682h3.333V8.211c.787-1.028 2.12-.841 2.534-.694V4.446c-.427-.16-1.987-.454-2.774.988zM12.387 1.549l-3.254.694-.013 10.694c0 1.976 1.48 3.431 3.453 3.431 1.094 0 1.894-.2 2.334-.44v-2.71c-.427.173-2.534.787-2.534-1.189V7.29h2.534V4.447h-2.534l.014-2.897zM3.373 7.837c0-.52.427-.72 1.134-.72 1.013 0 2.293.306 3.306.854V4.833a8.783 8.783 0 0 0-3.306-.614C1.8 4.22 0 5.634 0 7.997c0 3.685 5.067 3.098 5.067 4.687 0 .614-.534.814-1.28.814-1.107 0-2.52-.454-3.64-1.068v3.178a9.233 9.233 0 0 0 3.64.76c2.773 0 4.68-1.375 4.68-3.764-.014-3.98-5.094-3.271-5.094-4.767z" fill="#635BFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -43,42 +43,59 @@
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Months until renewal:
|
||||
Perk offset months:
|
||||
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Next Mystic Hourglass:
|
||||
<strong>{{ nextHourglassDate }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Mystic Hourglasses:
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Gem cap:
|
||||
<strong>{{ hero.purchased.plan.consecutive.gemCapExtra + 25 }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Gems bought this month:
|
||||
<input
|
||||
v-model="hero.purchased.plan.gemsBought"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Perk month count:
|
||||
<strong>{{ hero.purchased.plan.perkMonthCount }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
Next Mystic Hourglass:
|
||||
<strong>{{ nextHourglassDate }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Mystic Hourglasses:
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.trinkets"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Gem cap increase:
|
||||
<input
|
||||
v-model="hero.purchased.plan.consecutive.gemCapExtra"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
max="25"
|
||||
step="5"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
Total Gem cap:
|
||||
<strong>{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 25 }}</strong>
|
||||
</div>
|
||||
<div class="form-inline">
|
||||
<label>
|
||||
Gems bought this month:
|
||||
<input
|
||||
v-model="hero.purchased.plan.gemsBought"
|
||||
class="form-control"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="hero.purchased.plan.consecutive.gemCapExtra + 25"
|
||||
step="1"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="hero.purchased.plan.extraMonths > 0"
|
||||
>
|
||||
@@ -136,14 +153,7 @@ export default {
|
||||
nextHourglassDate () {
|
||||
const currentPlanContext = getPlanContext(this.hero, new Date());
|
||||
|
||||
return currentPlanContext.nextHourglassDate.format('MMMM');
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'hero.purchased.plan.consecutive.count' () { // eslint-disable-line object-shorthand
|
||||
this.hero.purchased.plan.consecutive.gemCapExtra = Math.min(
|
||||
Math.floor(this.hero.purchased.plan.consecutive.count / 3) * 5, 25,
|
||||
);
|
||||
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -86,6 +86,13 @@
|
||||
>{{ $t('companyContribute') }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://translate.habitica.com/"
|
||||
target="_blank"
|
||||
>{{ $t('translateHabitica') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Support -->
|
||||
@@ -101,6 +108,7 @@
|
||||
v-if="user"
|
||||
>
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
@click.prevent="openBugReportModal()"
|
||||
>
|
||||
@@ -224,7 +232,7 @@
|
||||
></div>
|
||||
</a><a
|
||||
class="social-circle"
|
||||
href="https://www.tumblr.com/Habitica"
|
||||
href="http://blog.habitrpg.com/"
|
||||
target="_blank"
|
||||
>
|
||||
<div
|
||||
@@ -472,10 +480,6 @@ footer {
|
||||
color: $purple-300;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:not([href]):not([class]):hover { // needed to make "report a bug"'s hover state correct
|
||||
color: $purple-300;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
column-gap: 1.5rem;
|
||||
display: grid;
|
||||
@@ -578,6 +582,7 @@ h3 {
|
||||
.text{
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
text-overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,11 +679,6 @@ h3 {
|
||||
|
||||
footer {
|
||||
padding: 24px 16px;
|
||||
a:not([href]):not([class]):hover { // needed to make "report a bug"'s hover state correct
|
||||
color: $purple-300;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
column-gap: 1.5rem;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
@@ -718,10 +718,6 @@ h3 {
|
||||
@media (max-width: 1024px) and (min-width: 768px) {
|
||||
footer {
|
||||
padding: 24px 24px;
|
||||
a:not([href]):not([class]):hover { // needed to make "report a bug"'s hover state correct
|
||||
color: $purple-300;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop {
|
||||
@@ -814,7 +810,7 @@ export default {
|
||||
...mapState({ user: 'user.data' }),
|
||||
...mapState(['isUserLoaded']),
|
||||
getDataDisplayToolUrl () {
|
||||
const base = 'https://oldgods.net/habitrpg/habitrpg_user_data_display.html';
|
||||
const base = 'https://tools.habitica.com/';
|
||||
if (!this.user) return null;
|
||||
return `${base}?uuid=${this.user._id}`;
|
||||
},
|
||||
|
||||
@@ -244,7 +244,7 @@ export default {
|
||||
petClass () {
|
||||
if (some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'virtual',
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'teaShop',
|
||||
)) {
|
||||
return this.foolPet(this.member.items.currentPet);
|
||||
}
|
||||
|
||||
@@ -159,7 +159,6 @@ label {
|
||||
}
|
||||
|
||||
.cancel-link {
|
||||
color: $blue-10;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,6 @@ label {
|
||||
}
|
||||
|
||||
.cancel-link {
|
||||
color: $blue-10;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,21 @@ export default {
|
||||
challengeId: this.challengeId,
|
||||
keep,
|
||||
});
|
||||
await this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true });
|
||||
const userTasksByType = (await this.$store.dispatch('tasks:fetchUserTasks', { forceLoad: true })).data;
|
||||
let tagInUse = false;
|
||||
Object.keys(userTasksByType).forEach(taskType => {
|
||||
userTasksByType[taskType].forEach(task => {
|
||||
if (task.tags.indexOf(this.challengeId) > -1) {
|
||||
tagInUse = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (!tagInUse) {
|
||||
await this.$store.dispatch(
|
||||
'tags:deleteTag',
|
||||
{ tagId: this.challengeId },
|
||||
);
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
close () {
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
}
|
||||
|
||||
a.cancel-link {
|
||||
color: $blue-10;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
v-if="editing"
|
||||
class="menu-container col-2"
|
||||
:class="{active: activeTopPage === 'backgrounds'}"
|
||||
@click="changeTopPage('backgrounds', '2022')"
|
||||
@click="changeTopPage('backgrounds', '2023')"
|
||||
>
|
||||
<div class="menu-item">
|
||||
<div
|
||||
@@ -198,52 +198,79 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!filterBackgrounds"
|
||||
class="row text-center title-row"
|
||||
>
|
||||
<strong>{{ backgroundShopSets[1].text }}</strong>
|
||||
</div>
|
||||
<div
|
||||
v-if="!filterBackgrounds"
|
||||
class="row title-row"
|
||||
v-if="!filterBackgrounds && user.purchased.background.birthday_bash"
|
||||
>
|
||||
<div
|
||||
v-for="bg in backgroundShopSets[1].items"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
:popover-title="bg.text"
|
||||
:popover="bg.notes"
|
||||
popover-trigger="mouseenter"
|
||||
@click="!user.purchased.background[bg.key]
|
||||
? backgroundSelected(bg) : unlock('background.' + bg.key)"
|
||||
class="row text-center title-row"
|
||||
>
|
||||
<strong>{{ backgroundShopSets[2].text }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="row title-row"
|
||||
>
|
||||
<div
|
||||
class="background"
|
||||
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
|
||||
></div>
|
||||
<i
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="glyphicon glyphicon-lock"
|
||||
></i>
|
||||
<div
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="purchase-background single d-flex align-items-center justify-content-center"
|
||||
v-for="bg in backgroundShopSets[2].items"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
:popover-title="bg.text"
|
||||
:popover="bg.notes"
|
||||
popover-trigger="mouseenter"
|
||||
@click="unlock('background.' + bg.key)"
|
||||
>
|
||||
<div
|
||||
class="svg-icon hourglass"
|
||||
v-html="icons.hourglass"
|
||||
class="background"
|
||||
:class="`background_${bg.key}`"
|
||||
></div>
|
||||
<span class="price">1</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="badge-top"
|
||||
@click.stop.prevent="togglePinned(bg)"
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!filterBackgrounds">
|
||||
<div
|
||||
class="row text-center title-row"
|
||||
>
|
||||
<strong>{{ backgroundShopSets[1].text }}</strong>
|
||||
</div>
|
||||
<div
|
||||
class="row title-row"
|
||||
>
|
||||
<div
|
||||
v-for="bg in backgroundShopSets[1].items"
|
||||
:key="bg.key"
|
||||
class="col-4 text-center customize-option background-button"
|
||||
:popover-title="bg.text"
|
||||
:popover="bg.notes"
|
||||
popover-trigger="mouseenter"
|
||||
@click="!user.purchased.background[bg.key]
|
||||
? backgroundSelected(bg) : unlock('background.' + bg.key)"
|
||||
>
|
||||
<pin-badge
|
||||
:pinned="isBackgroundPinned(bg)"
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
class="background"
|
||||
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
|
||||
></div>
|
||||
<i
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="glyphicon glyphicon-lock"
|
||||
></i>
|
||||
<div
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="purchase-background single d-flex align-items-center justify-content-center"
|
||||
>
|
||||
<div
|
||||
class="svg-icon hourglass"
|
||||
v-html="icons.hourglass"
|
||||
></div>
|
||||
<span class="price">1</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="!user.purchased.background[bg.key]"
|
||||
class="badge-top"
|
||||
@click.stop.prevent="togglePinned(bg)"
|
||||
>
|
||||
<pin-badge
|
||||
:pinned="isBackgroundPinned(bg)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<sub-menu
|
||||
@@ -1185,7 +1212,7 @@ export default {
|
||||
},
|
||||
],
|
||||
|
||||
bgSubMenuItems: ['2022', '2021', '2020', '2019', '2018', '2017', '2016', '2015', '2014'].map(y => ({
|
||||
bgSubMenuItems: ['2023', '2022', '2021', '2020', '2019', '2018', '2017', '2016', '2015', '2014'].map(y => ({
|
||||
id: y,
|
||||
label: y,
|
||||
})),
|
||||
@@ -1214,6 +1241,7 @@ export default {
|
||||
2020: [],
|
||||
2021: [],
|
||||
2022: [],
|
||||
2023: [],
|
||||
};
|
||||
|
||||
// Hack to force update for now until we restructure the data
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
a:not([href]) {
|
||||
color: $blue-10;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
@@ -245,10 +245,6 @@
|
||||
text-align: center;
|
||||
|
||||
color: $gray-100;
|
||||
|
||||
a {
|
||||
color: $blue-10;
|
||||
}
|
||||
}
|
||||
|
||||
#quest-detail-modal {
|
||||
|
||||
@@ -377,11 +377,9 @@
|
||||
|
||||
.members-invited {
|
||||
min-height: 1rem;
|
||||
color: $blue-10;
|
||||
margin: 0;
|
||||
|
||||
&:hover, &:focus {
|
||||
color: $blue-10;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4">
|
||||
<sidebar-section :title="$t('staffAndModerators')">
|
||||
<sidebar-section :title="$t('staff')">
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="user in staff"
|
||||
@@ -289,19 +289,6 @@
|
||||
class="svg-icon staff-icon"
|
||||
v-html="icons.tierStaff"
|
||||
></div>
|
||||
<div
|
||||
v-if="user.type === 'Moderator' && user.name !== 'It\'s Bailey'"
|
||||
class="svg-icon mod-icon"
|
||||
v-html="icons.tierMod"
|
||||
></div>
|
||||
<div
|
||||
v-if="user.name === 'It\'s Bailey'"
|
||||
class="svg-icon npc-icon"
|
||||
v-html="icons.tierNPC"
|
||||
></div>
|
||||
</div>
|
||||
<div class="type">
|
||||
{{ user.type }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,12 +340,13 @@
|
||||
<li>
|
||||
<a
|
||||
v-once
|
||||
href="https://oldgods.net/habitrpg/habitrpg_user_data_display.html"
|
||||
href="https://tools.habitica.com/"
|
||||
target="_blank"
|
||||
>{{ $t('dataDisplayTool') }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href=""
|
||||
target="_blank"
|
||||
@click.prevent="openBugReportModal()"
|
||||
>
|
||||
@@ -534,21 +522,6 @@
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
// formats the report a bug link to match the others
|
||||
a:not([href]) {
|
||||
&:not([role=button]) {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
a:not([href]):hover {
|
||||
&:not([role=button]) {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.tier1-icon, .tier2-icon {
|
||||
width: 11px;
|
||||
}
|
||||
@@ -772,6 +745,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
import { goToModForm } from '@/libs/modform';
|
||||
|
||||
@@ -848,22 +822,23 @@ export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
questData () {
|
||||
if (!this.group.quest) return {};
|
||||
return quests.quests[this.group.quest.key];
|
||||
},
|
||||
imageURLs () {
|
||||
if (!this.currentEvent || !this.currentEvent.season) {
|
||||
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
|
||||
if (!currentEvent) {
|
||||
return {
|
||||
background: 'url(/static/npc/normal/tavern_background.png)',
|
||||
npc: 'url(/static/npc/normal/tavern_npc.png)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: `url(/static/npc/${this.currentEvent.season}/tavern_background.png)`,
|
||||
npc: `url(/static/npc/${this.currentEvent.season}/tavern_npc.png)`,
|
||||
background: `url(/static/npc/${currentEvent.season}/tavern_background.png)`,
|
||||
npc: `url(/static/npc/${currentEvent.season}/tavern_npc.png)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
119
website/client/src/components/header/banners/birthdayBanner.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<base-banner
|
||||
banner-id="birthday-banner"
|
||||
class="birthday-banner"
|
||||
:show="showBirthdayBanner"
|
||||
height="3rem"
|
||||
:can-close="false"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
:aria-label="$t('celebrateBirthday')"
|
||||
class="content d-flex justify-content-around align-items-center ml-auto mr-auto"
|
||||
@click="showBirthdayModal"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gifts left-gift"
|
||||
v-html="icons.giftsBirthday"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-ten-birthday"
|
||||
v-html="icons.tenBirthday"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="announce-text"
|
||||
v-html="$t('celebrateBirthday')"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-gifts right-gift"
|
||||
v-html="icons.giftsBirthday"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</base-banner>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
.announce-text {
|
||||
color: $purple-50;
|
||||
}
|
||||
|
||||
.birthday-banner {
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 8px;
|
||||
background-image: linear-gradient(90deg,
|
||||
rgba(255,190,93,0) 0%,
|
||||
rgba(255,190,93,1) 25%,
|
||||
rgba(255,190,93,1) 75%,
|
||||
rgba(255,190,93,0) 100%),
|
||||
url('~@/assets/images/glitter.png');
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.left-gift {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.right-gift {
|
||||
margin: auto auto auto 8px;
|
||||
filter: flipH;
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.svg-gifts {
|
||||
width: 85px;
|
||||
}
|
||||
|
||||
.svg-ten-birthday {
|
||||
width: 192.5px;
|
||||
margin-left: 8px;
|
||||
margin-right: 8.5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import { mapState } from '@/libs/store';
|
||||
import BaseBanner from './base';
|
||||
|
||||
import giftsBirthday from '@/assets/svg/gifts-birthday.svg';
|
||||
import tenBirthday from '@/assets/svg/10th-birthday-linear.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseBanner,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
icons: Object.freeze({
|
||||
giftsBirthday,
|
||||
tenBirthday,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
showBirthdayBanner () {
|
||||
return Boolean(find(this.currentEventList, event => Boolean(event.event === 'birthday10')));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showBirthdayModal () {
|
||||
this.$root.$emit('bv::show::modal', 'birthday-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<base-notification
|
||||
:can-remove="canRemove"
|
||||
:has-icon="true"
|
||||
:notification="notification"
|
||||
:read-after-click="true"
|
||||
@click="action"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
>
|
||||
<strong> {{ notification.data.title }} </strong>
|
||||
<span> {{ notification.data.text }} </span>
|
||||
</div>
|
||||
<div
|
||||
slot="icon"
|
||||
class="mt-3"
|
||||
:class="notification.data.icon"
|
||||
></div>
|
||||
</base-notification>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseNotification from './base';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
default (data) {
|
||||
return data;
|
||||
},
|
||||
},
|
||||
canRemove: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
action () {
|
||||
if (!this.notification || !this.notification.data) {
|
||||
return;
|
||||
}
|
||||
if (this.notification.data.destination === 'backgrounds') {
|
||||
this.$store.state.avatarEditorOptions.editingUser = true;
|
||||
this.$store.state.avatarEditorOptions.startingPage = 'backgrounds';
|
||||
this.$store.state.avatarEditorOptions.subpage = '2023';
|
||||
this.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||
} else {
|
||||
this.$router.push({ name: this.notification.data.destination || 'items' });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -5,7 +5,14 @@
|
||||
:notification="notification"
|
||||
>
|
||||
<div slot="content">
|
||||
<div v-html="$t('invitedToParty', {party: notification.data.name})"></div>
|
||||
<div
|
||||
v-html="$t('invitedToPartyBy', {
|
||||
userId: notification.data.inviter,
|
||||
userName: invitingUser.auth ? invitingUser.auth.local.username : null,
|
||||
party: notification.data.name,
|
||||
})"
|
||||
>
|
||||
</div>
|
||||
<div class="notifications-buttons">
|
||||
<div
|
||||
class="btn btn-small btn-success"
|
||||
@@ -32,10 +39,31 @@ export default {
|
||||
components: {
|
||||
BaseNotification,
|
||||
},
|
||||
props: ['notification', 'canRemove'],
|
||||
props: {
|
||||
notification: {
|
||||
type: Object,
|
||||
default (data) {
|
||||
return data;
|
||||
},
|
||||
},
|
||||
canRemove: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
invitingUser: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({ user: 'user.data' }),
|
||||
},
|
||||
async mounted () {
|
||||
this.invitingUser = await this.$store.dispatch('members:fetchMember', {
|
||||
memberId: this.notification.data.inviter,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async accept () {
|
||||
const group = this.notification.data;
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
{{ $t('notifications') }}
|
||||
</h4>
|
||||
<a
|
||||
class="small-link standard-link"
|
||||
class="small-link"
|
||||
:disabled="notificationsCount === 0"
|
||||
@click="dismissAll"
|
||||
>{{ $t('dismissAll') }}</a>
|
||||
@@ -123,23 +123,24 @@ import successImage from '@/assets/svg/success.svg';
|
||||
import starBadge from '@/assets/svg/star-badge.svg';
|
||||
|
||||
// Notifications
|
||||
import NEW_STUFF from './notifications/newStuff';
|
||||
import GROUP_TASK_NEEDS_WORK from './notifications/groupTaskNeedsWork';
|
||||
import GUILD_INVITATION from './notifications/guildInvitation';
|
||||
import PARTY_INVITATION from './notifications/partyInvitation';
|
||||
import CARD_RECEIVED from './notifications/cardReceived';
|
||||
import CHALLENGE_INVITATION from './notifications/challengeInvitation';
|
||||
import QUEST_INVITATION from './notifications/questInvitation';
|
||||
import GIFT_ONE_GET_ONE from './notifications/g1g1';
|
||||
import GROUP_TASK_ASSIGNED from './notifications/groupTaskAssigned';
|
||||
import GROUP_TASK_CLAIMED from './notifications/groupTaskClaimed';
|
||||
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
|
||||
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
|
||||
import CARD_RECEIVED from './notifications/cardReceived';
|
||||
import NEW_INBOX_MESSAGE from './notifications/newPrivateMessage';
|
||||
import GROUP_TASK_NEEDS_WORK from './notifications/groupTaskNeedsWork';
|
||||
import GUILD_INVITATION from './notifications/guildInvitation';
|
||||
import ITEM_RECEIVED from './notifications/itemReceived';
|
||||
import NEW_CHAT_MESSAGE from './notifications/newChatMessage';
|
||||
import WORLD_BOSS from './notifications/worldBoss';
|
||||
import VERIFY_USERNAME from './notifications/verifyUsername';
|
||||
import NEW_INBOX_MESSAGE from './notifications/newPrivateMessage';
|
||||
import NEW_MYSTERY_ITEMS from './notifications/newMysteryItems';
|
||||
import NEW_STUFF from './notifications/newStuff';
|
||||
import ONBOARDING_COMPLETE from './notifications/onboardingComplete';
|
||||
import GIFT_ONE_GET_ONE from './notifications/g1g1';
|
||||
import PARTY_INVITATION from './notifications/partyInvitation';
|
||||
import QUEST_INVITATION from './notifications/questInvitation';
|
||||
import UNALLOCATED_STATS_POINTS from './notifications/unallocatedStatsPoints';
|
||||
import VERIFY_USERNAME from './notifications/verifyUsername';
|
||||
import WORLD_BOSS from './notifications/worldBoss';
|
||||
import OnboardingGuide from './onboardingGuide';
|
||||
|
||||
export default {
|
||||
@@ -147,24 +148,25 @@ export default {
|
||||
MenuDropdown,
|
||||
MessageCount,
|
||||
// One component for each type
|
||||
NEW_STUFF,
|
||||
GROUP_TASK_NEEDS_WORK,
|
||||
GUILD_INVITATION,
|
||||
PARTY_INVITATION,
|
||||
CARD_RECEIVED,
|
||||
CHALLENGE_INVITATION,
|
||||
QUEST_INVITATION,
|
||||
GIFT_ONE_GET_ONE,
|
||||
GROUP_TASK_ASSIGNED,
|
||||
GROUP_TASK_CLAIMED,
|
||||
UNALLOCATED_STATS_POINTS,
|
||||
NEW_MYSTERY_ITEMS,
|
||||
CARD_RECEIVED,
|
||||
NEW_INBOX_MESSAGE,
|
||||
GROUP_TASK_NEEDS_WORK,
|
||||
GUILD_INVITATION,
|
||||
ITEM_RECEIVED,
|
||||
NEW_CHAT_MESSAGE,
|
||||
WorldBoss: WORLD_BOSS,
|
||||
VERIFY_USERNAME,
|
||||
OnboardingGuide,
|
||||
NEW_INBOX_MESSAGE,
|
||||
NEW_MYSTERY_ITEMS,
|
||||
NEW_STUFF,
|
||||
ONBOARDING_COMPLETE,
|
||||
GIFT_ONE_GET_ONE,
|
||||
PARTY_INVITATION,
|
||||
QUEST_INVITATION,
|
||||
UNALLOCATED_STATS_POINTS,
|
||||
VERIFY_USERNAME,
|
||||
WorldBoss: WORLD_BOSS,
|
||||
OnboardingGuide,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -185,6 +187,7 @@ export default {
|
||||
// NOTE: Those not listed here won't be shown in the notification panel!
|
||||
handledNotifications: [
|
||||
'NEW_STUFF',
|
||||
'ITEM_RECEIVED',
|
||||
'GIFT_ONE_GET_ONE',
|
||||
'GROUP_TASK_NEEDS_WORK',
|
||||
'GUILD_INVITATION',
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
>{{ $t('editAvatar') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item dropdown-separated"
|
||||
@click="showAvatar('backgrounds', '2022')"
|
||||
@click="showAvatar('backgrounds', '2023')"
|
||||
>{{ $t('backgrounds') }}</a>
|
||||
<a
|
||||
class="topbar-dropdown-item dropdown-item"
|
||||
|
||||
@@ -879,7 +879,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.user.preferences.suppressModals.raisePet) {
|
||||
if (this.user.preferences.suppressModals.hatchPet) {
|
||||
this.hatchPet(pet);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -171,8 +171,9 @@ export default {
|
||||
getPetItemClass () {
|
||||
if (this.isOwned() && some(
|
||||
this.currentEventList,
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'virtual',
|
||||
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'teaShop',
|
||||
)) {
|
||||
if (this.isSpecial()) return `Pet ${this.foolPet(this.item.key)}`;
|
||||
const petString = `${this.item.eggKey}-${this.item.key}`;
|
||||
return `Pet ${this.foolPet(petString)}`;
|
||||
}
|
||||
|
||||
877
website/client/src/components/news/birthdayModal.vue
Normal file
@@ -0,0 +1,877 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="birthday-modal"
|
||||
:hide-header="true"
|
||||
:hide-footer="true"
|
||||
>
|
||||
<div class="modal-content">
|
||||
<div
|
||||
class="modal-close"
|
||||
@click="close()"
|
||||
>
|
||||
<div
|
||||
class="svg-icon svg-close"
|
||||
v-html="icons.close"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="svg-confetti svg-icon"
|
||||
v-html="icons.confetti"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="~@/assets/images/10-birthday.png"
|
||||
class="ten-birthday"
|
||||
>
|
||||
</div>
|
||||
<div class="limited-wrapper">
|
||||
<div
|
||||
class="svg-gifts svg-icon"
|
||||
v-html="icons.gifts"
|
||||
>
|
||||
</div>
|
||||
<div class="limited-event">
|
||||
{{ $t('limitedEvent') }}
|
||||
</div>
|
||||
<div class="dates">
|
||||
{{ $t('anniversaryLimitedDates') }}
|
||||
</div>
|
||||
<div
|
||||
class="svg-gifts-flip svg-icon"
|
||||
v-html="icons.gifts"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="celebrate d-flex justify-content-center">
|
||||
{{ $t('celebrateAnniversary') }}
|
||||
</div>
|
||||
<h2 class="d-flex justify-content-center">
|
||||
<span
|
||||
class="left-divider"
|
||||
v-html="icons.divider"
|
||||
></span>
|
||||
<span
|
||||
class="svg-cross"
|
||||
v-html="icons.cross"
|
||||
>
|
||||
</span>
|
||||
{{ $t('jubilantGryphatricePromo') }}
|
||||
<span
|
||||
class="svg-cross"
|
||||
v-html="icons.cross"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="right-divider"
|
||||
></span>
|
||||
</h2>
|
||||
<!-- gryphatrice info -->
|
||||
<div class="d-flex">
|
||||
<div class="jubilant-gryphatrice d-flex mr-auto">
|
||||
<img
|
||||
src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant-Large.gif"
|
||||
width="156px"
|
||||
height="144px"
|
||||
alt="a pink, purple, and green gryphatrice pet winks at you adorably"
|
||||
>
|
||||
</div>
|
||||
<div class="align-items-center">
|
||||
<div class="limited-edition mr-auto">
|
||||
{{ $t('limitedEdition') }}
|
||||
</div>
|
||||
<div class="gryphatrice-text">
|
||||
{{ $t('anniversaryGryphatriceText') }}
|
||||
</div>
|
||||
<div
|
||||
class="gryphatrice-price"
|
||||
v-html="$t('anniversaryGryphatricePrice')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- beginning of payments -->
|
||||
<!-- buy with money OR gems -->
|
||||
<div
|
||||
v-if="!ownGryphatrice && !gryphBought"
|
||||
>
|
||||
<div
|
||||
v-if="selectedPage !== 'payment-buttons'"
|
||||
id="initial-buttons"
|
||||
class="d-flex justify-content-center"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary buy-now-left"
|
||||
:class="{active: selectedPage === 'payment-buttons'}"
|
||||
@click="selectedPage = 'payment-buttons'"
|
||||
>
|
||||
{{ $t('buyNowMoneyButton') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary buy-now-right"
|
||||
@click="buyGryphatriceGems()"
|
||||
>
|
||||
{{ $t('buyNowGemsButton') }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- buy with money -->
|
||||
<div
|
||||
v-else-if="selectedPage === 'payment-buttons'"
|
||||
id="payment-buttons"
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<button
|
||||
class="btn btn-secondary d-flex stripe"
|
||||
@click="redirectToStripe({ sku: 'price_0MPZ6iZCD0RifGXlLah2furv' })"
|
||||
>
|
||||
<span
|
||||
class="svg-stripe"
|
||||
v-html="icons.stripe"
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary d-flex paypal"
|
||||
@click="openPaypal({
|
||||
url: paypalCheckoutLink, type: 'sku', sku: 'Pet-Gryphatrice-Jubilant'
|
||||
})"
|
||||
>
|
||||
<span
|
||||
class="svg-paypal"
|
||||
v-html="icons.paypal"
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
<amazon-button
|
||||
:disabled="disabled"
|
||||
:amazon-data="amazonData"
|
||||
class="btn btn-secondary d-flex amazon"
|
||||
v-html="icons.amazon"
|
||||
/>
|
||||
<div
|
||||
class="pay-with-gems"
|
||||
@click="selectedPage = 'initial-buttons'"
|
||||
>
|
||||
{{ $t('wantToPayWithGemsText') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Own the gryphatrice -->
|
||||
<div
|
||||
v-else
|
||||
class="d-flex"
|
||||
>
|
||||
<button
|
||||
class="own-gryphatrice-button"
|
||||
@click="closeAndRedirect('/inventory/stable')"
|
||||
v-html="$t('ownJubilantGryphatrice')"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<!-- end of payments -->
|
||||
<h2 class="d-flex justify-content-center">
|
||||
<span
|
||||
class="left-divider"
|
||||
v-html="icons.divider"
|
||||
></span>
|
||||
<span
|
||||
class="svg-cross"
|
||||
v-html="icons.cross"
|
||||
>
|
||||
</span>
|
||||
{{ $t('plentyOfPotions') }}
|
||||
<span
|
||||
class="svg-cross"
|
||||
v-html="icons.cross"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="right-divider"
|
||||
></span>
|
||||
</h2>
|
||||
<div class="plenty-of-potions d-flex">
|
||||
{{ $t('plentyOfPotionsText') }}
|
||||
</div>
|
||||
<div class="potions">
|
||||
<div class="pot-1">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Porcelain.png">
|
||||
</div>
|
||||
<div class="pot-2">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Vampire.png">
|
||||
</div>
|
||||
<div class="pot-3">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Aquatic.png">
|
||||
</div>
|
||||
<div class="pot-4">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_StainedGlass.png">
|
||||
</div>
|
||||
<div class="pot-5">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Celestial.png">
|
||||
</div>
|
||||
<div class="pot-6">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Glow.png">
|
||||
</div>
|
||||
<div class="pot-7">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_AutumnLeaf.png">
|
||||
</div>
|
||||
<div class="pot-8">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_SandSculpture.png">
|
||||
</div>
|
||||
<div class="pot-9">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Peppermint.png">
|
||||
</div>
|
||||
<div class="pot-10">
|
||||
<img src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Shimmer.png">
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-secondary d-flex justify-content-center visit-the-market"
|
||||
@click="closeAndRedirect('/shops/market')"
|
||||
>
|
||||
{{ $t('visitTheMarketButton') }}
|
||||
</button>
|
||||
<h2 class="d-flex justify-content-center">
|
||||
<span
|
||||
class="left-divider"
|
||||
v-html="icons.divider"
|
||||
></span>
|
||||
<span
|
||||
class="svg-cross"
|
||||
v-html="icons.cross"
|
||||
>
|
||||
</span>
|
||||
{{ $t('fourForFree') }}
|
||||
<span
|
||||
class="svg-cross"
|
||||
v-html="icons.cross"
|
||||
>
|
||||
</span>
|
||||
<span
|
||||
class="right-divider"
|
||||
></span>
|
||||
</h2>
|
||||
<div class="four-for-free">
|
||||
{{ $t('fourForFreeText') }}
|
||||
</div>
|
||||
<div class="four-grid d-flex justify-content-center">
|
||||
<div class="day-one-a">
|
||||
<div class="day-text">
|
||||
{{ $t('dayOne') }}
|
||||
</div>
|
||||
<div class="gift d-flex justify-content-center align-items-middle">
|
||||
<img
|
||||
src="~@/assets/images/robes.webp"
|
||||
class="m-auto"
|
||||
width="40px"
|
||||
height="66px"
|
||||
>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ $t('partyRobes') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="day-one-b">
|
||||
<div class="day-text">
|
||||
{{ $t('dayOne') }}
|
||||
</div>
|
||||
<div class="gift d-flex justify-content-center align-items-middle">
|
||||
<div
|
||||
class="svg-gem svg-icon m-auto"
|
||||
v-html="icons.birthdayGems"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ $t('twentyGems') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="day-five">
|
||||
<div class="day-text">
|
||||
{{ $t('dayFive') }}
|
||||
</div>
|
||||
<div class="gift d-flex justify-content-center align-items-middle">
|
||||
<img
|
||||
src="~@/assets/images/habitica-hero-goober.webp"
|
||||
class="m-auto"
|
||||
><!-- Birthday Set -->
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ $t('birthdaySet') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="day-ten">
|
||||
<div class="day-text">
|
||||
{{ $t('dayTen') }}
|
||||
</div>
|
||||
<div class="gift d-flex justify-content-center align-items-middle">
|
||||
<div
|
||||
class="svg-background svg-icon m-auto"
|
||||
v-html="icons.birthdayBackground"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
{{ $t('background') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-bottom">
|
||||
<div class="limitations d-flex justify-content-center">
|
||||
{{ $t('limitations') }}
|
||||
</div>
|
||||
<div class="fine-print">
|
||||
{{ $t('anniversaryLimitations') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
#birthday-modal {
|
||||
.modal-body {
|
||||
padding: 0px;
|
||||
border: 0px;
|
||||
}
|
||||
.modal-content {
|
||||
border-radius: 14px;
|
||||
border: 0px;
|
||||
}
|
||||
.modal-footer {
|
||||
border-radius: 14px;
|
||||
border: 0px;
|
||||
}
|
||||
.amazon {
|
||||
margin-bottom: 16px;
|
||||
|
||||
svg {
|
||||
width: 84px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.amazonpay-button-inner-image {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
@import '~@/assets/scss/mixins.scss';
|
||||
|
||||
#birthday-modal {
|
||||
h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
color: $white;
|
||||
column-gap: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.modal-body{
|
||||
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 566px;
|
||||
padding: 32px 24px 24px;
|
||||
background: linear-gradient(158deg,#6133b4,#4f2a93);
|
||||
border-top-left-radius: 12px;
|
||||
border-top-right-radius: 12px;
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
|
||||
.modal-bottom {
|
||||
width: 566px;
|
||||
background-color: $purple-50;
|
||||
color: $purple-500;
|
||||
line-height: 1.33;
|
||||
border-top: 0px;
|
||||
padding: 16px 40px 28px 40px;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
.limitations {
|
||||
color: $white;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
margin-top: 8px;
|
||||
justify-content: center;
|
||||
}
|
||||
.fine-print {
|
||||
font-size: 0.75rem;
|
||||
color: $purple-500;
|
||||
line-height: 1.33;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ten-birthday {
|
||||
position: relative;
|
||||
width: 268px;
|
||||
height: 244px;
|
||||
margin: 0 125px 16px;
|
||||
}
|
||||
|
||||
.limited-event {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 2.4px;
|
||||
margin-top: -8px;
|
||||
color: $yellow-50;
|
||||
}
|
||||
|
||||
.dates {
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.71;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.celebrate {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
margin: 16px 16px 24px 16px;
|
||||
text-align: center;
|
||||
color: $yellow-50;
|
||||
}
|
||||
|
||||
.jubilant-gryphatrice {
|
||||
height: 176px;
|
||||
width: 204px;
|
||||
border-radius: 12px;
|
||||
background-color: $purple-50;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24px;
|
||||
margin-left: 4px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.limited-wrapper {
|
||||
margin-top: -36px;
|
||||
margin-bottom: -36px;
|
||||
}
|
||||
|
||||
.limited-edition, .gryphatrice-text, .gryphatrice-price {
|
||||
max-width: 274px;
|
||||
}
|
||||
|
||||
.limited-edition {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
line-height:1.33;
|
||||
letter-spacing:2.4px;
|
||||
padding-top: 18px;
|
||||
margin-left: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: $yellow-50;
|
||||
}
|
||||
|
||||
.gryphatrice-text, .gryphatrice-price {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
margin-left: 24px;
|
||||
margin-right: 4px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.gryphatrice-price {
|
||||
padding-top: 16px;
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.buy-now-left {
|
||||
width: 243px;
|
||||
margin: 24px 8px 24px 0px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.buy-now-right {
|
||||
width: 243px;
|
||||
margin: 24px 0px 24px 8px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
}
|
||||
|
||||
.stripe {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.paypal {
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.stripe, .paypal, .amazon {
|
||||
width: 506px;
|
||||
height: 32px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pay-with-gems {
|
||||
color: $white;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pay-with-gems:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.own-gryphatrice-button {
|
||||
width: 506px;
|
||||
height: 32px;
|
||||
margin: 24px 4px;
|
||||
border-radius: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: $green-100;
|
||||
background-color: $green-100;
|
||||
color: $green-1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.plenty-of-potions {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
margin: 0 8px 24px;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.potions {
|
||||
display: grid;
|
||||
grid-template-columns: 5;
|
||||
grid-template-rows: 2;
|
||||
gap: 24px 24px;
|
||||
justify-content: center;
|
||||
|
||||
.pot-1, .pot-2, .pot-3, .pot-4, .pot-5,
|
||||
.pot-6, .pot-7, .pot-8, .pot-9, .pot-10 {
|
||||
height: 68px;
|
||||
width: 68px;
|
||||
border-radius: 8px;
|
||||
background-color: $purple-50;
|
||||
}
|
||||
|
||||
.pot-1 {
|
||||
grid-column: 1 / 1;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
.pot-2 {
|
||||
grid-column: 2 / 2;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
.pot-3 {
|
||||
grid-column: 3 / 3;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
.pot-4 {
|
||||
grid-column: 4 / 4;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
.pot-5 {
|
||||
grid-column: 5 / 5;
|
||||
grid-row: 1 / 2;
|
||||
}
|
||||
.pot-6 {
|
||||
grid-column: 1 / 5;
|
||||
grid-row: 2 / 2;
|
||||
}
|
||||
.pot-7 {
|
||||
grid-column: 2 / 5;
|
||||
grid-row: 2 / 2;
|
||||
}
|
||||
.pot-8 {
|
||||
grid-column: 3 / 5;
|
||||
grid-row: 2 / 2;
|
||||
}
|
||||
.pot-9 {
|
||||
grid-column: 4 / 5;
|
||||
grid-row: 2 / 2;
|
||||
}
|
||||
.pot-10 {
|
||||
grid-column: 5 / 5;
|
||||
grid-row: 2 / 2;
|
||||
}
|
||||
|
||||
}
|
||||
.visit-the-market {
|
||||
height: 32px;
|
||||
margin: 24px 4px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.four-for-free {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
margin: 0 36px 24px;
|
||||
text-align: center;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.four-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 4;
|
||||
grid-template-rows: 1;
|
||||
gap: 24px;
|
||||
}
|
||||
.day-one-a, .day-one-b, .day-five, .day-ten {
|
||||
height: 140px;
|
||||
width: 100px;
|
||||
border-radius: 8px;
|
||||
background-color: $purple-50;
|
||||
}
|
||||
|
||||
.day-one-a {
|
||||
grid-column: 1 / 1;
|
||||
grid-row: 1 / 1;
|
||||
}
|
||||
.day-one-b {
|
||||
grid-column: 2 / 2;
|
||||
grid-row: 1 / 1;
|
||||
}
|
||||
.day-five {
|
||||
grid-column: 3 / 3;
|
||||
grid-row: 1 / 1;
|
||||
}
|
||||
.day-ten {
|
||||
grid-column: 4 / 4;
|
||||
grid-row: 1 / 1;
|
||||
}
|
||||
|
||||
.day-text {
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.33;
|
||||
letter-spacing: 2.4px;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
padding: 4px 0px;
|
||||
color: $yellow-50;
|
||||
}
|
||||
|
||||
.gift {
|
||||
height: 80px;
|
||||
width: 84px;
|
||||
margin: 0 8px 32px;
|
||||
background-color: $purple-100;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.33;
|
||||
text-align: center;
|
||||
padding: 8px 0px;
|
||||
margin-top: -32px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// SVG CSS
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 16px;
|
||||
cursor: pointer;
|
||||
|
||||
.svg-close {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
vertical-align: middle;
|
||||
fill: $purple-50;
|
||||
|
||||
& svg path {
|
||||
fill: $purple-50 !important;;
|
||||
}
|
||||
& :hover {
|
||||
fill: $purple-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.svg-confetti {
|
||||
position: absolute;
|
||||
height: 152px;
|
||||
width: 518px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.svg-gifts, .svg-gifts-flip {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
width: 85px;
|
||||
}
|
||||
|
||||
.svg-gifts {
|
||||
margin-left: 70px;
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
.svg-gifts-flip {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
left: 366px;
|
||||
bottom: 34px;
|
||||
}
|
||||
|
||||
.left-divider, .right-divider {
|
||||
background-image: url('~@/assets/images/fancy-divider.png');
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
display: inline-flex;
|
||||
flex-grow: 2;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.right-divider {
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.svg-cross {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
color: $yellow-50;
|
||||
}
|
||||
|
||||
.svg-gem {
|
||||
height: 48px;
|
||||
width: 58px;
|
||||
}
|
||||
|
||||
.svg-background {
|
||||
height: 68px;
|
||||
width: 68px;
|
||||
}
|
||||
|
||||
.svg-stripe {
|
||||
height: 20px;
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.svg-paypal {
|
||||
height: 16px;
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<script>
|
||||
// to check if user owns JG or not
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
// Purchase functionality
|
||||
import buy from '@/mixins/buy';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import payments from '@/mixins/payments';
|
||||
import content from '@/../../common/script/content/index';
|
||||
import amazonButton from '@/components/payments/buttons/amazon';
|
||||
|
||||
// import images
|
||||
import close from '@/assets/svg/new-close.svg';
|
||||
import confetti from '@/assets/svg/confetti.svg';
|
||||
import gifts from '@/assets/svg/gifts-birthday.svg';
|
||||
import cross from '@/assets/svg/cross.svg';
|
||||
import stripe from '@/assets/svg/stripe.svg';
|
||||
import paypal from '@/assets/svg/paypal-logo.svg';
|
||||
import amazon from '@/assets/svg/amazonpay.svg';
|
||||
import birthdayGems from '@/assets/svg/birthday-gems.svg';
|
||||
import birthdayBackground from '@/assets/svg/icon-background-birthday.svg';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
amazonButton,
|
||||
},
|
||||
mixins: [buy, notifications, payments],
|
||||
data () {
|
||||
return {
|
||||
amazonData: {
|
||||
type: 'single',
|
||||
sku: 'Pet-Gryphatrice-Jubilant',
|
||||
},
|
||||
icons: Object.freeze({
|
||||
close,
|
||||
confetti,
|
||||
gifts,
|
||||
cross,
|
||||
stripe,
|
||||
paypal,
|
||||
amazon,
|
||||
birthdayGems,
|
||||
birthdayBackground,
|
||||
}),
|
||||
selectedPage: 'initial-buttons',
|
||||
gryphBought: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
user: 'user.data',
|
||||
}),
|
||||
ownGryphatrice () {
|
||||
return Boolean(this.user && this.user.items.pets['Gryphatrice-Jubilant']);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hide () {
|
||||
this.$root.$emit('bv::hide::modal', 'birthday-modal');
|
||||
},
|
||||
buyGryphatriceGems () {
|
||||
const gryphatrice = content.petInfo['Gryphatrice-Jubilant'];
|
||||
if (this.user.balance * 4 < gryphatrice.value) {
|
||||
this.$root.$emit('bv::show::modal', 'buy-gems');
|
||||
return this.hide();
|
||||
}
|
||||
if (!this.confirmPurchase(gryphatrice.currency, gryphatrice.value)) {
|
||||
return null;
|
||||
}
|
||||
this.makeGenericPurchase(gryphatrice);
|
||||
this.gryphBought = true;
|
||||
return this.purchased(gryphatrice.text());
|
||||
},
|
||||
closeAndRedirect (route) {
|
||||
const routeTerminator = route.split('/')[route.split('/').length - 1];
|
||||
if (this.$router.history.current.name !== routeTerminator) {
|
||||
this.$router.push(route);
|
||||
}
|
||||
this.hide();
|
||||
},
|
||||
close () {
|
||||
this.$root.$emit('bv::hide::modal', 'birthday-modal');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -78,6 +78,7 @@ export default {
|
||||
orderReferenceId: null,
|
||||
subscription: null,
|
||||
coupon: null,
|
||||
sku: null,
|
||||
},
|
||||
isAmazonSetup: false,
|
||||
amazonButtonEnabled: false,
|
||||
@@ -174,7 +175,10 @@ export default {
|
||||
storePaymentStatusAndReload (url) {
|
||||
let paymentType;
|
||||
|
||||
if (this.amazonPayments.type === 'single' && !this.amazonPayments.gift) paymentType = 'gems';
|
||||
if (this.amazonPayments.type === 'single') {
|
||||
if (this.amazonPayments.sku) paymentType = 'sku';
|
||||
else if (!this.amazonPayments.gift) paymentType = 'gems';
|
||||
}
|
||||
if (this.amazonPayments.type === 'subscription') paymentType = 'subscription';
|
||||
if (this.amazonPayments.groupId || this.amazonPayments.groupToCreate) paymentType = 'groupPlan';
|
||||
if (this.amazonPayments.type === 'single' && this.amazonPayments.gift && this.amazonPayments.giftReceiver) {
|
||||
@@ -223,6 +227,7 @@ export default {
|
||||
const data = {
|
||||
orderReferenceId: this.amazonPayments.orderReferenceId,
|
||||
gift: this.amazonPayments.gift,
|
||||
sku: this.amazonPayments.sku,
|
||||
};
|
||||
|
||||
if (this.amazonPayments.gemsBlock) {
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $gray-10;
|
||||
font-size: 0.875rem;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
</button>
|
||||
<a
|
||||
v-once
|
||||
class="standard-link"
|
||||
@click="close()"
|
||||
>{{ $t('neverMind') }}</a>
|
||||
</div>
|
||||
|
||||
@@ -180,7 +180,6 @@
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
a:not([href]) {
|
||||
color: $blue-10;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<b-modal
|
||||
id="payments-success-modal"
|
||||
:hide-footer="isNewGroup || isGems || isSubscription"
|
||||
:modal-class="isNewGroup || isGems || isSubscription
|
||||
:hide-footer="isNewGroup || isGems || isSubscription || ownsJubilantGryphatrice"
|
||||
:modal-class="isNewGroup || isGems || isSubscription || ownsJubilantGryphatrice
|
||||
? ['modal-hidden-footer'] : []"
|
||||
>
|
||||
<!-- HEADER -->
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="check-container d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon check"
|
||||
class="svg-icon svg-check"
|
||||
v-html="icons.check"
|
||||
></div>
|
||||
</div>
|
||||
@@ -107,6 +107,35 @@
|
||||
class="small-text auto-renew"
|
||||
>{{ $t('paymentAutoRenew') }}</span>
|
||||
</template>
|
||||
<!-- if you buy the Jubilant Gryphatrice during 10th birthday -->
|
||||
<template
|
||||
v-if="ownsJubilantGryphatrice"
|
||||
>
|
||||
<div class="words">
|
||||
<p class="jub-success">
|
||||
<span
|
||||
v-once
|
||||
v-html="$t('jubilantSuccess')"
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
<p class="jub-success">
|
||||
<span
|
||||
v-once
|
||||
v-html="$t('stableVisit')"
|
||||
>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="gryph-bg">
|
||||
<img
|
||||
src="https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant-Large.gif"
|
||||
alt="a pink, purple, and green gryphatrice pet winks at you adorably"
|
||||
width="78px"
|
||||
height="72px"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<!-- buttons for subscriptions / new Group / buy Gems for self -->
|
||||
<button
|
||||
v-if="isNewGroup || isGems || isSubscription"
|
||||
@@ -116,6 +145,14 @@
|
||||
>
|
||||
{{ $t('onwards') }}
|
||||
</button>
|
||||
<!-- buttons for Jubilant Gryphatrice purchase during 10th birthday -->
|
||||
<button
|
||||
v-if="ownsJubilantGryphatrice"
|
||||
class="btn btn-primary mx-auto btn-jub"
|
||||
@click="closeAndRedirect()"
|
||||
>
|
||||
{{ $t('takeMeToStable') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- FOOTER -->
|
||||
@@ -232,9 +269,8 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.check {
|
||||
width: 35.1px;
|
||||
height: 28px;
|
||||
.svg-check {
|
||||
width: 45px;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
@@ -293,6 +329,34 @@
|
||||
.group-billing-date {
|
||||
width: 269px;
|
||||
}
|
||||
|
||||
.words {
|
||||
margin-bottom: 16px;
|
||||
justify-content: center;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-50;
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.jub-success {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.gryph-bg {
|
||||
width: 110px;
|
||||
height: 104px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
background-color: $gray-700;
|
||||
}
|
||||
.btn-jub {
|
||||
margin-bottom: 8px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
}
|
||||
.modal-footer {
|
||||
background: $gray-700;
|
||||
@@ -430,6 +494,9 @@ export default {
|
||||
isNewGroup () {
|
||||
return this.paymentData.paymentType === 'groupPlan' && this.paymentData.newGroup;
|
||||
},
|
||||
ownsJubilantGryphatrice () {
|
||||
return this.paymentData.paymentType === 'sku'; // will need to be revised when there are other discrete skus in system
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.$root.$on('habitica:payment-success', data => {
|
||||
@@ -458,6 +525,12 @@ export default {
|
||||
this.sendingInProgress = false;
|
||||
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
|
||||
},
|
||||
closeAndRedirect () {
|
||||
if (this.$router.history.current.name !== 'stable') {
|
||||
this.$router.push('/inventory/stable');
|
||||
}
|
||||
this.close();
|
||||
},
|
||||
submit () {
|
||||
if (this.paymentData.group && !this.paymentData.newGroup) {
|
||||
Analytics.track({
|
||||
|
||||
@@ -23,33 +23,7 @@
|
||||
</div>
|
||||
<div class="section">
|
||||
<h3>{{ $t('thirdPartyApps') }}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://www.beeminder.com/habitica"
|
||||
>{{ $t('beeminder') }}</a>
|
||||
<br>
|
||||
{{ $t('beeminderDesc') }}
|
||||
</li>
|
||||
<li>
|
||||
<div v-html="$t('chatExtension')">
|
||||
</div>
|
||||
<span>{{ $t('chatExtensionDesc') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://oldgods.net/habitica/habitrpg_user_data_display.html?uuid=` + user._id"
|
||||
>{{ $t('dataDisplayTool') }}</a>
|
||||
<br>
|
||||
{{ $t('dataToolDesc') }}
|
||||
</li>
|
||||
<li>
|
||||
<div v-html="$t('otherExtensions')"></div>
|
||||
<span>{{ $t('otherDesc') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-html="$t('thirdPartyTools')"></p>
|
||||
<hr>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,38 @@
|
||||
{{ $t('settings') }}
|
||||
</h1>
|
||||
<div class="col-sm-6">
|
||||
<div class="sleep">
|
||||
<h5>{{ $t('pauseDailies') }}</h5>
|
||||
<h4>{{ $t('sleepDescription') }}</h4>
|
||||
<ul>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet1') }}
|
||||
</li>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet2') }}
|
||||
</li>
|
||||
<li v-once>
|
||||
{{ $t('sleepBullet3') }}
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
v-if="!user.preferences.sleep"
|
||||
v-once
|
||||
class="sleep btn btn-primary btn-block pause-button"
|
||||
@click="toggleSleep()"
|
||||
>
|
||||
{{ $t('pauseDailies') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="user.preferences.sleep"
|
||||
v-once
|
||||
class="btn btn-secondary btn-block pause-button"
|
||||
@click="toggleSleep()"
|
||||
>
|
||||
{{ $t('unpauseDailies') }}
|
||||
</button>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-horizontal">
|
||||
<h5>{{ $t('language') }}</h5>
|
||||
<select
|
||||
@@ -517,6 +549,10 @@
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.sleep {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -651,6 +687,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSleep () {
|
||||
this.$store.dispatch('user:sleep');
|
||||
},
|
||||
validateDisplayName: debounce(function checkName (displayName) {
|
||||
if (displayName.length <= 1 || displayName === this.user.profile.name) {
|
||||
this.displayNameIssues = [];
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="subscribe-card mx-auto">
|
||||
<div
|
||||
v-if="hasSubscription && !hasCanceledSubscription"
|
||||
class="d-flex flex-column align-items-center"
|
||||
class="d-flex flex-column align-items-center pt-4"
|
||||
>
|
||||
<div class="round-container bg-green-10 d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
@@ -107,7 +107,7 @@
|
||||
</h2>
|
||||
<div
|
||||
v-if="hasGroupPlan"
|
||||
class="mx-5 text-center"
|
||||
class="mx-5 mb-4 text-center"
|
||||
>
|
||||
{{ $t('youHaveGroupPlan') }}
|
||||
</div>
|
||||
@@ -130,7 +130,7 @@
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-primary btn-update-card
|
||||
d-flex justify-content-center align-items-center"
|
||||
d-flex justify-content-center align-items-center mb-4"
|
||||
@click="redirectToStripeEdit()"
|
||||
>
|
||||
<div
|
||||
@@ -143,21 +143,61 @@
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="svg-icon"
|
||||
class="svg-icon mb-4"
|
||||
:class="paymentMethodLogo.class"
|
||||
v-html="paymentMethodLogo.icon"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="purchasedPlanExtraMonthsDetails.months > 0"
|
||||
class="extra-months green-10 py-2 px-3 mt-4"
|
||||
class="extra-months green-10 py-2 px-3 mb-4"
|
||||
v-html="$t('purchasedPlanExtraMonths',
|
||||
{months: purchasedPlanExtraMonthsDetails.months})"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="hasCanceledSubscription"
|
||||
v-if="hasGiftSubscription"
|
||||
class="d-flex flex-column align-items-center mt-4"
|
||||
>
|
||||
<div class="round-container bg-green-10 d-flex align-items-center justify-content-center">
|
||||
<div
|
||||
v-once
|
||||
class="svg-icon svg-check"
|
||||
v-html="icons.checkmarkIcon"
|
||||
></div>
|
||||
</div>
|
||||
<h2 class="green-10 mx-auto mb-75">
|
||||
{{ $t('youAreSubscribed') }}
|
||||
</h2>
|
||||
<div
|
||||
class="mx-4 text-center mb-4 lh-71"
|
||||
>
|
||||
<span v-once>
|
||||
{{ $t('haveNonRecurringSub') }}
|
||||
</span>
|
||||
<span
|
||||
v-once
|
||||
v-html="$t('subscriptionInactiveDate', {date: subscriptionEndDate})"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<h2 v-once>
|
||||
{{ $t('switchToRecurring') }}
|
||||
</h2>
|
||||
<small
|
||||
v-once
|
||||
class="mx-4 mb-3 text-center"
|
||||
>
|
||||
{{ $t('continueGiftSubBenefits') }}
|
||||
</small>
|
||||
<subscription-options
|
||||
:note="'subscriptionCreditConversion'"
|
||||
class="w-100 mb-2"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasCanceledSubscription"
|
||||
class="d-flex flex-column align-items-center mt-4"
|
||||
>
|
||||
<div class="round-container bg-gray-300 d-flex align-items-center justify-content-center">
|
||||
@@ -180,7 +220,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="hasSubscription"
|
||||
class="bg-gray-700 py-3 mt-4 mb-3 text-center"
|
||||
class="bg-gray-700 py-3 mb-3 text-center"
|
||||
>
|
||||
<div class="header-mini mb-3">
|
||||
{{ $t('subscriptionStats') }}
|
||||
@@ -322,6 +362,12 @@
|
||||
max-width: 21rem;
|
||||
}
|
||||
|
||||
small {
|
||||
color: $gray-100;
|
||||
font-size: 12px ;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -399,6 +445,10 @@
|
||||
height: 49px;
|
||||
}
|
||||
|
||||
.lh-71 {
|
||||
line-height: 1.71;
|
||||
}
|
||||
|
||||
.maroon-50 {
|
||||
color: $maroon-50;
|
||||
}
|
||||
@@ -443,7 +493,6 @@
|
||||
}
|
||||
|
||||
.subscribe-card {
|
||||
padding-top: 2rem;
|
||||
width: 28rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
|
||||
@@ -472,8 +521,7 @@
|
||||
}
|
||||
|
||||
.svg-check {
|
||||
width: 35.1px;
|
||||
height: 28px;
|
||||
width: 36px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
@@ -670,6 +718,9 @@ export default {
|
||||
hasSubscription () {
|
||||
return Boolean(this.user.purchased.plan.customerId);
|
||||
},
|
||||
hasGiftSubscription () {
|
||||
return this.user.purchased.plan.customerId === 'Gift';
|
||||
},
|
||||
hasCanceledSubscription () {
|
||||
return (
|
||||
this.hasSubscription
|
||||
@@ -753,7 +804,7 @@ export default {
|
||||
return currentPlanContext.nextHourglassDate;
|
||||
},
|
||||
nextHourGlass () {
|
||||
const nextHourglassMonth = this.nextHourGlassDate.format('MMM');
|
||||
const nextHourglassMonth = this.nextHourGlassDate.format('MMM YYYY');
|
||||
|
||||
return nextHourglassMonth;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div id="subscription-form">
|
||||
<b-form-group class="mb-4 w-100 h-100">
|
||||
<b-form-group class="mb-3 w-100 h-100">
|
||||
<!-- eslint-disable vue/no-use-v-if-with-v-for -->
|
||||
<b-form-radio
|
||||
v-for="block in subscriptionBlocksOrdered"
|
||||
@@ -32,6 +32,15 @@
|
||||
</div>
|
||||
</b-form-radio>
|
||||
</b-form-group>
|
||||
<div class="mx-4 mb-4 text-center">
|
||||
<small
|
||||
v-if="note"
|
||||
v-once
|
||||
class="font-italic"
|
||||
>
|
||||
{{ $t(note) }}
|
||||
</small>
|
||||
</div>
|
||||
<!-- payment buttons first is for gift subs and the second is for renewing subs -->
|
||||
<payments-buttons
|
||||
v-if="userReceivingGift && userReceivingGift._id"
|
||||
@@ -82,7 +91,10 @@
|
||||
|
||||
.subscription-bubble, .discount-bubble {
|
||||
border-radius: 100px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.subscription-bubble {
|
||||
@@ -100,8 +112,20 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~@/assets/scss/colors.scss';
|
||||
|
||||
small {
|
||||
color: $gray-100;
|
||||
display: inline-block;
|
||||
font-size: 12px ;
|
||||
font-weight: normal;
|
||||
line-height: 1.33;
|
||||
}
|
||||
|
||||
.subscribe-option {
|
||||
border-bottom: 1px solid $gray-600;
|
||||
background-color: $gray-700;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border-bottom: 1px solid $gray-600;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -121,6 +145,10 @@ export default {
|
||||
paymentsMixin,
|
||||
],
|
||||
props: {
|
||||
note: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
userReceivingGift: {
|
||||
type: Object,
|
||||
default () {},
|
||||
@@ -154,13 +182,13 @@ export default {
|
||||
subscriptionBubbles (subscription) {
|
||||
switch (subscription) {
|
||||
case 'basic_3mo':
|
||||
return '<span class="subscription-bubble px-2 py-1 mr-1">Gem cap raised to 30</span><span class="subscription-bubble px-2 py-1">+1 Mystic Hourglass</span>';
|
||||
return '<span class="subscription-bubble py-1 mr-1">Gem cap raised to 30</span><span class="subscription-bubble py-1">+1 Mystic Hourglass</span>';
|
||||
case 'basic_6mo':
|
||||
return '<span class="subscription-bubble px-2 py-1 mr-1">Gem cap raised to 35</span><span class="subscription-bubble px-2 py-1">+2 Mystic Hourglass</span>';
|
||||
return '<span class="subscription-bubble py-1 mr-1">Gem cap raised to 35</span><span class="subscription-bubble py-1">+2 Mystic Hourglass</span>';
|
||||
case 'basic_12mo':
|
||||
return '<span class="discount-bubble px-2 py-1 mr-1">Save 20%</span><span class="subscription-bubble px-2 py-1 mr-1">Gem cap raised to 45</span><span class="subscription-bubble px-2 py-1">+4 Mystic Hourglass</span>';
|
||||
return '<span class="discount-bubble py-1 mr-1">Save 20%</span><span class="subscription-bubble py-1 mr-1">Gem cap raised to 45</span><span class="subscription-bubble py-1">+4 Mystic Hourglass</span>';
|
||||
default:
|
||||
return '<span class="subscription-bubble px-2 py-1">Gem cap at 25</span>';
|
||||
return '<span class="subscription-bubble py-1">Gem cap at 25</span>';
|
||||
}
|
||||
},
|
||||
updateSubscriptionData (key) {
|
||||
|
||||
@@ -385,7 +385,6 @@ import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
|
||||
import Item from '@/components/inventory/item';
|
||||
import Avatar from '@/components/avatar';
|
||||
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
import { drops as dropEggs } from '@/../../common/script/content/eggs';
|
||||
import { drops as dropPotions } from '@/../../common/script/content/hatching-potions';
|
||||
|
||||
@@ -438,7 +437,6 @@ export default {
|
||||
|
||||
selectedAmountToBuy: 1,
|
||||
isPinned: false,
|
||||
endDate: seasonalShopConfig.dateRange.end,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -489,6 +487,9 @@ export default {
|
||||
nonSubscriberHourglasses () {
|
||||
return (!this.user.purchased.plan.customerId && !this.user.purchased.plan.consecutive.trinkets && this.getPriceClass() === 'hourglasses');
|
||||
},
|
||||
endDate () {
|
||||
return moment(this.item.event.end);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function itemChanged () {
|
||||
|
||||
@@ -8,16 +8,6 @@
|
||||
:popover-position="'top'"
|
||||
@click="itemSelected(item)"
|
||||
>
|
||||
<span slot="popoverContent">
|
||||
<strong v-if="item.key === 'gem' && gemsLeft === 0">{{ $t('maxBuyGems') }}</strong>
|
||||
<h4 class="popover-content-title">{{ item.text }}</h4>
|
||||
<div
|
||||
v-if="item.event"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ limitedString }}
|
||||
</div>
|
||||
</span>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
slot-scope="ctx"
|
||||
@@ -32,11 +22,9 @@
|
||||
import _filter from 'lodash/filter';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _map from 'lodash/map';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import pinUtils from '@/mixins/pinUtils';
|
||||
import planGemLimits from '@/../../common/script/libs/planGemLimits';
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
import ShopItem from '../shopItem';
|
||||
import CategoryItem from './categoryItem';
|
||||
@@ -48,12 +36,6 @@ export default {
|
||||
},
|
||||
mixins: [pinUtils],
|
||||
props: ['hideLocked', 'hidePinned', 'searchBy', 'sortBy', 'category'],
|
||||
data () {
|
||||
return {
|
||||
timer: '',
|
||||
limitedString: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
content: 'content',
|
||||
@@ -106,43 +88,10 @@ export default {
|
||||
return result;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.countdownString();
|
||||
this.timer = setInterval(this.countdownString, 1000);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.cancelAutoUpdate();
|
||||
},
|
||||
methods: {
|
||||
itemSelected (item) {
|
||||
this.$root.$emit('buyModal::showItem', item);
|
||||
},
|
||||
countdownString () {
|
||||
const diffDuration = moment.duration(moment(seasonalShopConfig.dateRange.end).diff(moment()));
|
||||
|
||||
if (diffDuration.asSeconds() <= 0) {
|
||||
this.limitedString = this.$t('noLongerAvailable');
|
||||
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
||||
this.limitedString = this.$t('limitedAvailabilityDays', {
|
||||
days: moment(seasonalShopConfig.dateRange.end).diff(moment(), 'days'),
|
||||
hours: diffDuration.hours(),
|
||||
minutes: diffDuration.minutes(),
|
||||
});
|
||||
} else if (diffDuration.asMinutes() > 2) {
|
||||
this.limitedString = this.$t('limitedAvailabilityHours', {
|
||||
hours: diffDuration.hours(),
|
||||
minutes: diffDuration.minutes(),
|
||||
});
|
||||
} else {
|
||||
this.limitedString = this.$t('limitedAvailabilityMinutes', {
|
||||
minutes: diffDuration.minutes(),
|
||||
seconds: diffDuration.seconds(),
|
||||
});
|
||||
}
|
||||
},
|
||||
cancelAutoUpdate () {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import _filter from 'lodash/filter';
|
||||
import _map from 'lodash/map';
|
||||
import _throttle from 'lodash/throttle';
|
||||
@@ -225,7 +226,7 @@ export default {
|
||||
user: 'user.data',
|
||||
userStats: 'user.data.stats',
|
||||
userItems: 'user.data.items',
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
market () {
|
||||
return shops.getMarketShop(this.user);
|
||||
@@ -292,15 +293,16 @@ export default {
|
||||
return Object.values(this.viewOptions).some(g => g.selected);
|
||||
},
|
||||
imageURLs () {
|
||||
if (!this.currentEvent || !this.currentEvent.season) {
|
||||
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
|
||||
if (!currentEvent) {
|
||||
return {
|
||||
background: 'url(/static/npc/normal/market_background.png)',
|
||||
npc: 'url(/static/npc/normal/market_banner_npc.png)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: `url(/static/npc/${this.currentEvent.season}/market_background.png)`,
|
||||
npc: `url(/static/npc/${this.currentEvent.season}/market_banner_npc.png)`,
|
||||
background: `url(/static/npc/${currentEvent.season}/market_background.png)`,
|
||||
npc: `url(/static/npc/${currentEvent.season}/market_banner_npc.png)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -263,8 +263,8 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
import svgClock from '@/assets/svg/clock.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
@@ -319,7 +319,6 @@ export default {
|
||||
|
||||
isPinned: false,
|
||||
selectedAmountToBuy: 1,
|
||||
endDate: seasonalShopConfig.dateRange.end,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -343,6 +342,9 @@ export default {
|
||||
if (this.priceType === 'hourglasses') return this.icons.hourglass;
|
||||
return this.icons.gem;
|
||||
},
|
||||
endDate () {
|
||||
return moment(this.item.event.end);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function itemChanged () {
|
||||
|
||||
@@ -397,6 +397,7 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import find from 'lodash/find';
|
||||
import _filter from 'lodash/filter';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _throttle from 'lodash/throttle';
|
||||
@@ -512,7 +513,7 @@ export default {
|
||||
user: 'user.data',
|
||||
userStats: 'user.data.stats',
|
||||
userItems: 'user.data.items',
|
||||
currentEvent: 'worldState.data.currentEvent',
|
||||
currentEventList: 'worldState.data.currentEventList',
|
||||
}),
|
||||
shop () {
|
||||
return shops.getQuestShop(this.user);
|
||||
@@ -536,15 +537,16 @@ export default {
|
||||
return Object.values(this.viewOptions).some(g => g.selected);
|
||||
},
|
||||
imageURLs () {
|
||||
if (!this.currentEvent || !this.currentEvent.season) {
|
||||
const currentEvent = find(this.currentEventList, event => Boolean(event.season));
|
||||
if (!currentEvent) {
|
||||
return {
|
||||
background: 'url(/static/npc/normal/quest_shop_background.png)',
|
||||
npc: 'url(/static/npc/normal/quest_shop_npc.png)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
background: `url(/static/npc/${this.currentEvent.season}/quest_shop_background.png)`,
|
||||
npc: `url(/static/npc/${this.currentEvent.season}/quest_shop_npc.png)`,
|
||||
background: `url(/static/npc/${currentEvent.season}/quest_shop_background.png)`,
|
||||
npc: `url(/static/npc/${currentEvent.season}/quest_shop_npc.png)`,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="container-fluid">
|
||||
<h1>{{ $t('communityGuidelines') }}</h1>
|
||||
<hr>
|
||||
<p>{{ $t('lastUpdated') }} July 28, 2021</p>
|
||||
<p>{{ $t('lastUpdated') }} February 8, 2023</p>
|
||||
<h2 id="welcome">
|
||||
{{ $t('commGuideHeadingWelcome') }}
|
||||
</h2>
|
||||
@@ -21,6 +21,7 @@
|
||||
<p v-html="$t('commGuidePara016')"></p>
|
||||
<p v-html="$t('commGuidePara017')"></p>
|
||||
<ul>
|
||||
<li v-html="$t('commGuideList01F')"></li>
|
||||
<li v-html="$t('commGuideList01A')"></li>
|
||||
<li v-html="$t('commGuideList01B')"></li>
|
||||
<li v-html="$t('commGuideList01C')"></li>
|
||||
@@ -32,6 +33,7 @@
|
||||
<img src="~@/assets/images/community-guidelines/publicSpaces.png">
|
||||
</div>
|
||||
<ul>
|
||||
<li v-html="$t('commGuideList02N')"></li>
|
||||
<li v-html="$t('commGuideList02A')"></li>
|
||||
<li v-html="$t('commGuideList02B')"></li>
|
||||
<li v-html="$t('commGuideList02G')"></li>
|
||||
@@ -147,10 +149,9 @@
|
||||
<li>
|
||||
{{ $t('commGuideList10A') }}
|
||||
<ul>
|
||||
<li>{{ $t('commGuideList10A1') }}</li>
|
||||
<li v-html="$t('commGuideList10A1')"></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li v-html="$t('commGuideList10C')"></li>
|
||||
<li v-html="$t('commGuideList10D')"></li>
|
||||
<li v-html="$t('commGuideList10F')"></li>
|
||||
</ul>
|
||||
@@ -176,50 +177,53 @@
|
||||
<h2 id="meet-the-mods">
|
||||
{{ $t('commGuideHeadingMeet') }}
|
||||
</h2>
|
||||
<p v-html="$t('commGuidePara006')"></p>
|
||||
<p v-html="$t('commGuidePara007')"></p>
|
||||
<p v-html="$t('commGuidePara008')"></p>
|
||||
<p v-html="$t('commGuidePara009')"></p>
|
||||
<div class="media align-items-center">
|
||||
<img src="~@/assets/images/community-guidelines/staff.png">
|
||||
<div class="media-body">
|
||||
<ul>
|
||||
<li>{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'heyeilatan', realName: 'Natalie'}) }}
|
||||
({{ $t('commGuideOnGitHub', {gitHubName: 'CuriousMagpie'}) }})
|
||||
- Web Developer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'Viirus', realName: 'Phillip'}) }}
|
||||
- Mobile Developer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'redphoenix', realName: 'Vicky'}) }}
|
||||
({{ $t('commGuideOnGitHub', {gitHubName: 'veeeeeee'}) }})
|
||||
- Co-Founder
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}
|
||||
- Art, Community Management, Many Hats
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}
|
||||
- Web Developer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}
|
||||
- Designer
|
||||
</li>
|
||||
<li>
|
||||
{{ $t('commGuideAKA', {habitName: 'Piyo', realName: 'Sara'}) }}
|
||||
- Mobile Designer
|
||||
</li>
|
||||
<li>{{ $t('commGuideAKA', {habitName: 'Beffymaroo', realName: 'Beth'}) }}</li>
|
||||
<li>{{ $t('commGuideAKA', {habitName: 'SabreCat', realName: 'Sabe'}) }}</li>
|
||||
<li>{{ $t('commGuideAKA', {habitName: 'Apollo', realName: 'Tressley'}) }}</li>
|
||||
<li>{{ $t('commGuideAKA', {habitName: 'Piyo', realName: 'Sara'}) }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p v-html="$t('commGuidePara010')"></p>
|
||||
<div class="media align-items-center">
|
||||
<img src="~@/assets/images/community-guidelines/moderators.png">
|
||||
<div class="media-body">
|
||||
<p v-html="$t('commGuidePara011')"></p>
|
||||
<ul>
|
||||
<li>Dewines</li>
|
||||
<li>Nakonana</li>
|
||||
<li>Cantras</li>
|
||||
<li>Alys (LadyAlys {{ $t('commGuidePara011c') }})</li>
|
||||
<li>Fox_town</li>
|
||||
<li>MaybeSteveRogers</li>
|
||||
<li>shanaqui</li>
|
||||
<li>deilann (not yet pictured)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p v-html="$t('commGuidePara012')"></p>
|
||||
<p v-html="$t('commGuidePara013')"></p>
|
||||
<p>
|
||||
{{ $t('commGuidePara014') }}<br>
|
||||
<em>
|
||||
Lemoness, lefnire, Slappybag, litenull, Shaner, Bobbyroberts99, wc8,
|
||||
Breadstrings, Megan, Blade, and Daniel the Bard
|
||||
Breadstrings, Megan, Blade, Daniel the Bard, deilann, shanaqui, Nakonana,
|
||||
Dewines, Alys, Fox_town, MaybeSteveRogers, and Cantras.
|
||||
|
||||
</em>
|
||||
</p>
|
||||
<h2 id="final">
|
||||
@@ -240,6 +244,7 @@
|
||||
</ul>
|
||||
<p v-html="$t('commGuidePara069')"></p>
|
||||
<ul class="list-2col list-unstyled">
|
||||
<li>Beffymaroo</li>
|
||||
<li>Breadstrings</li>
|
||||
<li>Draayder</li>
|
||||
<li>Kiwibot</li>
|
||||
|
||||
@@ -5,40 +5,44 @@
|
||||
>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6 offset-md-3">
|
||||
<h1 id="faq-heading">
|
||||
<h1
|
||||
v-once
|
||||
id="faq-heading"
|
||||
>
|
||||
{{ $t('frequentlyAskedQuestions') }}
|
||||
</h1>
|
||||
<div
|
||||
v-for="(heading, index) in headings"
|
||||
v-for="(entry, index) in faq.questions"
|
||||
:key="index"
|
||||
class="faq-question"
|
||||
>
|
||||
<div
|
||||
v-if="heading !== 'world-boss'"
|
||||
<h2
|
||||
v-once
|
||||
v-b-toggle="entry.heading"
|
||||
role="tab"
|
||||
variant="info"
|
||||
@click="handleClick($event)"
|
||||
>
|
||||
<h2
|
||||
v-b-toggle="heading"
|
||||
role="tab"
|
||||
variant="info"
|
||||
@click="handleClick($event)"
|
||||
>
|
||||
{{ $t(`faqQuestion${index}`) }}
|
||||
</h2>
|
||||
<b-collapse
|
||||
:id="heading"
|
||||
:visible="isVisible(heading)"
|
||||
accordion="faq"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
v-markdown="$t(`webFaqAnswer${index}`, replacements)"
|
||||
class="card-body"
|
||||
></div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
{{ entry.question }}
|
||||
</h2>
|
||||
<b-collapse
|
||||
:id="entry.heading"
|
||||
:visible="isVisible(entry.heading)"
|
||||
accordion="faq"
|
||||
role="tabpanel"
|
||||
>
|
||||
<div
|
||||
v-once
|
||||
v-markdown="entry.web"
|
||||
class="card-body"
|
||||
></div>
|
||||
</b-collapse>
|
||||
</div>
|
||||
<hr>
|
||||
<p v-markdown="$t('webFaqStillNeedHelp')"></p>
|
||||
<p
|
||||
v-once
|
||||
v-markdown="stillNeedHelp"
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,7 +50,7 @@
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.card-body {
|
||||
margin-bottom: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.faq-question h2 {
|
||||
@@ -74,53 +78,34 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// @TODO: env.EMAILS.TECH_ASSISTANCE_EMAIL
|
||||
import markdownDirective from '@/directives/markdown';
|
||||
|
||||
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
data () {
|
||||
const headings = [
|
||||
'overview',
|
||||
'set-up-tasks',
|
||||
'sample-tasks',
|
||||
'task-color',
|
||||
'health',
|
||||
'party-with-friends',
|
||||
'pets-mounts',
|
||||
'character-classes',
|
||||
'blue-mana-bar',
|
||||
'monsters-quests',
|
||||
'gems',
|
||||
'bugs-features',
|
||||
'world-boss',
|
||||
'group-plans',
|
||||
];
|
||||
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
|
||||
return {
|
||||
headings,
|
||||
replacements: {
|
||||
techAssistanceEmail: TECH_ASSISTANCE_EMAIL,
|
||||
wikiTechAssistanceEmail: `mailto:${TECH_ASSISTANCE_EMAIL}`,
|
||||
},
|
||||
visible: hash && headings.includes(hash) ? hash : null,
|
||||
faq: {},
|
||||
headings: [],
|
||||
stillNeedHelp: '',
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
section: this.$t('help'),
|
||||
subSection: this.$t('faq'),
|
||||
});
|
||||
this.faq = await this.$store.dispatch('faq:getFAQ');
|
||||
for (const entry of this.faq.questions) {
|
||||
this.headings.push(entry.heading);
|
||||
}
|
||||
this.stillNeedHelp = this.faq.stillNeedHelp.web;
|
||||
},
|
||||
methods: {
|
||||
isVisible (heading) {
|
||||
return this.visible && this.visible === heading;
|
||||
const hash = window.location.hash.replace('#', '');
|
||||
return hash && this.headings.includes(hash) && hash === heading;
|
||||
},
|
||||
handleClick (e) {
|
||||
if (!e) return;
|
||||
|
||||
@@ -354,6 +354,9 @@
|
||||
|
||||
<style lang='scss'>
|
||||
@import '~@/assets/scss/static.scss';
|
||||
#front .form-text a {
|
||||
color: $white !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -362,10 +365,6 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Varela+Round');
|
||||
|
||||
#front {
|
||||
.form-text a {
|
||||
color: $white !important;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -165,10 +165,6 @@ export default {
|
||||
question: 'pkQuestion7',
|
||||
answer: 'pkAnswer7',
|
||||
},
|
||||
{
|
||||
question: 'pkQuestion8',
|
||||
answer: 'pkAnswer8',
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
</span>
|
||||
<a
|
||||
v-if="assignedUsersCount > 1 && !showStatus"
|
||||
class="blue-10"
|
||||
@click="showStatus = !showStatus"
|
||||
>
|
||||
{{ $t('viewStatus') }}
|
||||
@@ -128,10 +127,6 @@
|
||||
padding-top: 0.25rem;
|
||||
z-index: 9;
|
||||
height: 24px;
|
||||
|
||||
.blue-10 {
|
||||
color: $blue-10;
|
||||
}
|
||||
}
|
||||
|
||||
.completion-row {
|
||||
|
||||
@@ -593,7 +593,6 @@
|
||||
a:not(.dropdown-item) {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $blue-10;
|
||||
}
|
||||
|
||||
.modal-dialog.modal-sm {
|
||||
|
||||
@@ -276,7 +276,6 @@
|
||||
a {
|
||||
font-size: 12px;
|
||||
line-height: 1.33;
|
||||
color: $blue-10;
|
||||
margin-top: 4px;
|
||||
|
||||
&:focus, &:hover, &:active {
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
color: $gray-400;
|
||||
color: $white !important;
|
||||
text-decoration: none !important;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 0.5rem;
|
||||
|
||||
@@ -182,7 +182,6 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -296,10 +295,6 @@
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.challenge-link, .user-link {
|
||||
color: $blue-10 !important;
|
||||
}
|
||||
|
||||
.entry-action {
|
||||
b {
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -863,16 +863,13 @@ export default {
|
||||
this.loadUser();
|
||||
this.oldTitle = this.$store.state.title;
|
||||
this.selectPage(this.startingPage);
|
||||
this.$root.$on('habitica:restoreTitle', () => {
|
||||
if (this.oldTitle) {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
fullTitle: this.oldTitle,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$root.$off('habitica:restoreTitle');
|
||||
if (this.oldTitle) {
|
||||
this.$store.dispatch('common:setTitle', {
|
||||
fullTitle: this.oldTitle,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadUser () {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
:hide-footer="true"
|
||||
:hide-header="true"
|
||||
@hide="beforeHide"
|
||||
@hidden="onHidden"
|
||||
@shown="onShown()"
|
||||
>
|
||||
<profile
|
||||
@@ -55,14 +54,11 @@ export default {
|
||||
},
|
||||
beforeHide () {
|
||||
if (this.$route.path !== window.location.pathname) {
|
||||
this.$root.$emit('habitica:restoreTitle');
|
||||
}
|
||||
},
|
||||
onHidden () {
|
||||
if (this.$route.path !== window.location.pathname) {
|
||||
this.$router.go(-1);
|
||||
this.$router.back();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
@@ -34,44 +34,4 @@ export default [
|
||||
type: 'Staff',
|
||||
uuid: 'f4e5c6da-0617-48bf-b3bd-9f97636774a8',
|
||||
},
|
||||
{
|
||||
name: 'Alys',
|
||||
type: 'Moderator',
|
||||
uuid: 'd904bd62-da08-416b-a816-ba797c9ee265',
|
||||
},
|
||||
{
|
||||
name: 'Cantras',
|
||||
type: 'Moderator',
|
||||
uuid: '28771972-ca6d-4c03-8261-e1734aa7d21d',
|
||||
},
|
||||
{
|
||||
name: 'deilann',
|
||||
type: 'Moderator',
|
||||
uuid: 'e7b5d1e2-3b6e-4192-b867-8bafdb03eeec',
|
||||
},
|
||||
{
|
||||
name: 'Dewines',
|
||||
type: 'Moderator',
|
||||
uuid: '262a7afb-6b57-4d81-88e0-80d2e9f6cbdc',
|
||||
},
|
||||
{
|
||||
name: 'Fox_town',
|
||||
type: 'Moderator',
|
||||
uuid: 'a05f0152-d66b-4ef1-93ac-4adb195d0031',
|
||||
},
|
||||
{
|
||||
name: 'MaybeSteveRogers',
|
||||
type: 'Moderator',
|
||||
uuid: '767e5d92-0e13-4e30-acb1-d8bba62824fc',
|
||||
},
|
||||
{
|
||||
name: 'Nakonana',
|
||||
type: 'Moderator',
|
||||
uuid: '33bb14bd-814d-40cb-98a4-7b76a752761c',
|
||||
},
|
||||
{
|
||||
name: 'shanaqui',
|
||||
type: 'Moderator',
|
||||
uuid: 'bb089388-28ae-4e42-a8fa-f0c2bfb6f779',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -26,6 +26,7 @@ export default {
|
||||
'Fox-Veteran',
|
||||
'JackOLantern-Glow',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphatrice-Jubilant',
|
||||
'JackOLantern-RoyalPurple',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
@@ -39,15 +40,15 @@ export default {
|
||||
'Dragon',
|
||||
'Cactus',
|
||||
];
|
||||
if (!pet) return 'Pet-Cactus-Virtual';
|
||||
if (!pet) return 'Pet-TigerCub-TeaShop';
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return 'Pet-Wolf-Virtual';
|
||||
return 'Pet-Dragon-TeaShop';
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-Virtual`;
|
||||
return `Pet-${species}-TeaShop`;
|
||||
}
|
||||
return 'Pet-Fox-Virtual';
|
||||
return 'Pet-BearCub-TeaShop';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
|
||||
const { STRIPE_PUB_KEY } = process.env;
|
||||
|
||||
// const habiticaUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
let stripeInstance = null;
|
||||
|
||||
export default {
|
||||
@@ -70,6 +69,7 @@ export default {
|
||||
type,
|
||||
giftData,
|
||||
gemsBlock,
|
||||
sku,
|
||||
} = data;
|
||||
let { url } = data;
|
||||
|
||||
@@ -93,6 +93,11 @@ export default {
|
||||
url += `?gemsBlock=${gemsBlock.key}`;
|
||||
}
|
||||
|
||||
if (type === 'sku') {
|
||||
appState.sku = sku;
|
||||
url += `?sku=${sku}`;
|
||||
}
|
||||
|
||||
setLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE, JSON.stringify(appState));
|
||||
window.open(url, '_blank');
|
||||
|
||||
@@ -129,6 +134,7 @@ export default {
|
||||
if (data.group || data.groupToCreate) paymentType = 'groupPlan';
|
||||
if (data.gift && data.gift.type === 'gems') paymentType = 'gift-gems';
|
||||
if (data.gift && data.gift.type === 'subscription') paymentType = 'gift-subscription';
|
||||
if (data.sku) paymentType = 'sku';
|
||||
|
||||
let url = '/stripe/checkout-session';
|
||||
const postData = {};
|
||||
@@ -148,6 +154,7 @@ export default {
|
||||
if (data.coupon) postData.coupon = data.coupon;
|
||||
if (data.groupId) postData.groupId = data.groupId;
|
||||
if (data.demographics) postData.demographics = data.demographics;
|
||||
if (data.sku) postData.sku = data.sku;
|
||||
|
||||
const response = await axios.post(url, postData);
|
||||
|
||||
@@ -250,6 +257,7 @@ export default {
|
||||
|
||||
if (data.type === 'single') {
|
||||
this.amazonPayments.gemsBlock = data.gemsBlock;
|
||||
this.amazonPayments.sku = data.sku;
|
||||
}
|
||||
|
||||
if (data.gift) {
|
||||
|
||||
@@ -272,6 +272,11 @@ const router = new VueRouter({
|
||||
name: 'transactions',
|
||||
path: 'transactions',
|
||||
component: Transactions,
|
||||
meta: {
|
||||
privilegeNeeded: [
|
||||
'userSupport',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
@@ -316,11 +321,6 @@ const router = new VueRouter({
|
||||
{
|
||||
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
|
||||
},
|
||||
// Commenting out merch page see
|
||||
// https://github.com/HabitRPG/habitica/issues/12039
|
||||
// {
|
||||
// name: 'merch', path: 'merch', component: MerchPage, meta: { requiresLogin: false },
|
||||
// },
|
||||
{
|
||||
name: 'news', path: 'new-stuff', component: NewsPage, meta: { requiresLogin: false },
|
||||
},
|
||||
|
||||