diff --git a/habitica-images b/habitica-images index dac6a71d1f..3cda1c1a27 160000 --- a/habitica-images +++ b/habitica-images @@ -1 +1 @@ -Subproject commit dac6a71d1fa2500bf8f3ec18bf917cbe91d82d64 +Subproject commit 3cda1c1a27ef51e735ea0f6edd848f001321ec0f diff --git a/migrations/archive/2023/20230522_pet_group_achievements.js b/migrations/archive/2023/20230522_pet_group_achievements.js new file mode 100644 index 0000000000..59c6fe7d1d --- /dev/null +++ b/migrations/archive/2023/20230522_pet_group_achievements.js @@ -0,0 +1,158 @@ +/* eslint-disable no-console */ +const MIGRATION_NAME = '20230522_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['Parrot-Base'] + && pets['Parrot-CottonCandyBlue'] + && pets['Parrot-CottonCandyPink'] + && pets['Parrot-Desert'] + && pets['Parrot-Golden'] + && pets['Parrot-Red'] + && pets['Parrot-Shade'] + && pets['Parrot-Skeleton'] + && pets['Parrot-White'] + && pets['Parrot-Zombie'] + && pets['Rooster-Base'] + && pets['Rooster-CottonCandyBlue'] + && pets['Rooster-CottonCandyPink'] + && pets['Rooster-Desert'] + && pets['Rooster-Golden'] + && pets['Rooster-Red'] + && pets['Rooster-Shade'] + && pets['Rooster-Skeleton'] + && pets['Rooster-White'] + && pets['Rooster-Zombie'] + && pets['Triceratops-Base'] + && pets['Triceratops-CottonCandyBlue'] + && pets['Triceratops-CottonCandyPink'] + && pets['Triceratops-Desert'] + && pets['Triceratops-Golden'] + && pets['Triceratops-Red'] + && pets['Triceratops-Shade'] + && pets['Triceratops-Skeleton'] + && pets['Triceratops-White'] + && pets['Triceratops-Zombie'] + && pets['TRex-Base'] + && pets['TRex-CottonCandyBlue'] + && pets['TRex-CottonCandyPink'] + && pets['TRex-Desert'] + && pets['TRex-Golden'] + && pets['TRex-Red'] + && pets['TRex-Shade'] + && pets['TRex-Skeleton'] + && pets['TRex-White'] + && pets['TRex-Zombie'] + && pets['Pterodactyl-Base'] + && pets['Pterodactyl-CottonCandyBlue'] + && pets['Pterodactyl-CottonCandyPink'] + && pets['Pterodactyl-Desert'] + && pets['Pterodactyl-Golden'] + && pets['Pterodactyl-Red'] + && pets['Pterodactyl-Shade'] + && pets['Pterodactyl-Skeleton'] + && pets['Pterodactyl-White'] + && pets['Pterodactyl-Zombie'] + && pets['Owl-Base'] + && pets['Owl-CottonCandyBlue'] + && pets['Owl-CottonCandyPink'] + && pets['Owl-Desert'] + && pets['Owl-Golden'] + && pets['Owl-Red'] + && pets['Owl-Shade'] + && pets['Owl-Skeleton'] + && pets['Owl-White'] + && pets['Owl-Zombie'] + && pets['Velociraptor-Base'] + && pets['Velociraptor-CottonCandyBlue'] + && pets['Velociraptor-CottonCandyPink'] + && pets['Velociraptor-Desert'] + && pets['Velociraptor-Golden'] + && pets['Velociraptor-Red'] + && pets['Velociraptor-Shade'] + && pets['Velociraptor-Skeleton'] + && pets['Velociraptor-White'] + && pets['Velociraptor-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['Falcon-Base'] + && pets['Falcon-CottonCandyBlue'] + && pets['Falcon-CottonCandyPink'] + && pets['Falcon-Desert'] + && pets['Falcon-Golden'] + && pets['Falcon-Red'] + && pets['Falcon-Shade'] + && pets['Falcon-Skeleton'] + && pets['Falcon-White'] + && pets['Falcon-Zombie'] + && pets['Peacock-Base'] + && pets['Peacock-CottonCandyBlue'] + && pets['Peacock-CottonCandyPink'] + && pets['Peacock-Desert'] + && pets['Peacock-Golden'] + && pets['Peacock-Red'] + && pets['Peacock-Shade'] + && pets['Peacock-Skeleton'] + && pets['Peacock-White'] + && pets['Peacock-Zombie']) { + set['achievements.dinosaurDynasty'] = 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('2023-04-15') }, + }; + + 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 + } +}; diff --git a/package-lock.json b/package-lock.json index f0cb6492d5..90e371ee80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "habitica", - "version": "4.270.2", + "version": "4.271.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2281,23 +2281,12 @@ } }, "@sinonjs/fake-timers": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz", - "integrity": "sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz", + "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==", "dev": true, "requires": { - "@sinonjs/commons": "^2.0.0" - }, - "dependencies": { - "@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - } + "@sinonjs/commons": "^3.0.0" } }, "@sinonjs/samsam": { @@ -13776,13 +13765,13 @@ } }, "sinon": { - "version": "15.0.4", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.0.4.tgz", - "integrity": "sha512-uzmfN6zx3GQaria1kwgWGeKiXSSbShBbue6Dcj0SI8fiCNFbiUDqKl57WFlY5lyhxZVUKmXvzgG2pilRQCBwWg==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.0.tgz", + "integrity": "sha512-cS5FgpDdE9/zx7no8bxROHymSlPLZzq0ChbbLk1DrxBfc+eTeBK3y8nIL+nu/0QeYydhhbLIr7ecHJpywjQaoQ==", "dev": true, "requires": { "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/fake-timers": "^10.2.0", "@sinonjs/samsam": "^8.0.0", "diff": "^5.1.0", "nise": "^5.1.4", @@ -14435,18 +14424,18 @@ } }, "stripe": { - "version": "12.5.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.5.0.tgz", - "integrity": "sha512-eDBh4bv+Uo+GdhjnQ246lM5KaOReoBzxFltgW0HJqco/QEAgSYxZPOpFbd5+gJnZlRqHSB5B+Zqw273SlbdPag==", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.6.0.tgz", + "integrity": "sha512-aJat4pGA6fr5DvuojRVXMaOCAw8ceS3UQM4ux1BMUViG686NmOAf1psEh6Wu+NFUifoy6kV7+4APq66OlTWtTw==", "requires": { "@types/node": ">=8.1.0", "qs": "^6.11.0" }, "dependencies": { "qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "requires": { "side-channel": "^1.0.4" } diff --git a/package.json b/package.json index 33ab1cfbcc..d1cbce5350 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "4.270.2", + "version": "4.271.1", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.21.8", @@ -67,7 +67,7 @@ "remove-markdown": "^0.5.0", "rimraf": "^3.0.2", "short-uuid": "^4.2.2", - "stripe": "^12.5.0", + "stripe": "^12.6.0", "superagent": "^8.0.9", "universal-analytics": "^0.5.3", "useragent": "^2.1.9", @@ -122,7 +122,7 @@ "monk": "^7.3.4", "require-again": "^2.0.0", "run-rs": "^0.7.7", - "sinon": "^15.0.4", + "sinon": "^15.1.0", "sinon-chai": "^3.7.0", "sinon-stub-promise": "^4.0.0" }, diff --git a/test/api/unit/libs/payments/payments.test.js b/test/api/unit/libs/payments/payments.test.js index 697a13fe1b..438b8def23 100644 --- a/test/api/unit/libs/payments/payments.test.js +++ b/test/api/unit/libs/payments/payments.test.js @@ -748,9 +748,19 @@ describe('payments/index', () => { }); it('does not add to plans.consecutive.offset if 1 month subscription', async () => { + data.sub.key = 'basic_earned'; await api.createSubscription(data); - expect(user.purchased.plan.extraMonths).to.eql(0); + expect(user.purchased.plan.consecutive.offset).to.eql(0); + }); + + it('resets plans.consecutive.offset if 1 month subscription', async () => { + user.purchased.plan.consecutive.offset = 1; + await user.save(); + data.sub.key = 'basic_earned'; + await api.createSubscription(data); + + expect(user.purchased.plan.consecutive.offset).to.eql(0); }); it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => { diff --git a/website/client/src/assets/css/sprites/spritesmith-main.css b/website/client/src/assets/css/sprites/spritesmith-main.css index b3c0c74c78..271abcba15 100644 --- a/website/client/src/assets/css/sprites/spritesmith-main.css +++ b/website/client/src/assets/css/sprites/spritesmith-main.css @@ -138,6 +138,11 @@ width: 48px; height: 52px; } +.achievement-dinosaurDynasty2x { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-dinosaurDynasty2x.png'); + width: 68px; + height: 68px; +} .achievement-domesticated2x { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/achievement-domesticated2x.png'); width: 60px; diff --git a/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue b/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue index 9e7ffff117..5c5e3e8a43 100644 --- a/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue +++ b/website/client/src/components/admin-panel/user-support/subscriptionAndPerks.vue @@ -17,10 +17,18 @@ Payment schedule ("basic-earned" is monthly): {{ hero.purchased.plan.planId }} +
+ Group plan ID: + {{ hero.purchased.plan.owner }} +
Creation date: {{ dateFormat(hero.purchased.plan.dateCreated) }}
+
+ Start date for current subscription type: + {{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }} +
Termination date: {{ hero.purchased.plan.consecutive.offset }}
-
+
Perk month count: - {{ hero.purchased.plan.perkMonthCount }} +
Next Mystic Hourglass: diff --git a/website/common/locales/en/achievements.json b/website/common/locales/en/achievements.json index 7650882af5..7e4aba8355 100644 --- a/website/common/locales/en/achievements.json +++ b/website/common/locales/en/achievements.json @@ -147,5 +147,8 @@ "achievementPolarProModalText": "You collected all the Polar Pets!", "achievementPlantParent": "Plant Parent", "achievementPlantParentText": "Has hatched all standard colors of Plant pets: Cactus and Treeling!", - "achievementPlantParentModalText": "You collected all the Plant Pets!" + "achievementPlantParentModalText": "You collected all the Plant Pets!", + "achievementDinosaurDynasty": "Dinosaur Dynasty", + "achievementDinosaurDynastyText": "Has hatched all standard colors of bird and dinosaur pets: Falcon, Owl, Parrot, Peacock, Penguin, Rooster, Pterodactyl, T-Rex, Triceratops, and Velociraptor!", + "achievementDinosaurDynastyModalText": "You collected all the bird and dinosaur pets!" } diff --git a/website/common/script/content/achievements.js b/website/common/script/content/achievements.js index dfc0e46df4..a7e843d23d 100644 --- a/website/common/script/content/achievements.js +++ b/website/common/script/content/achievements.js @@ -178,6 +178,11 @@ const animalSetAchievs = { titleKey: 'achievementBirdsOfAFeather', textKey: 'achievementBirdsOfAFeatherText', }, + dinosaurDynasty: { + icon: 'achievement-dinosaurDynasty', + titleKey: 'achievementDinosaurDynasty', + textKey: 'achievementDinosaurDynastyText', + }, domesticated: { icon: 'achievement-domesticated', titleKey: 'achievementDomesticated', diff --git a/website/common/script/content/bundles.js b/website/common/script/content/bundles.js index 4576c5e727..f9b3666fab 100644 --- a/website/common/script/content/bundles.js +++ b/website/common/script/content/bundles.js @@ -178,9 +178,9 @@ const bundles = { 'penguin', 'rooster', ], - event: EVENTS.bundle202109, + event: EVENTS.bundle202305, canBuy () { - return moment().isBefore(EVENTS.bundle202109.end); + return moment().isBefore(EVENTS.bundle202305.end); }, type: 'quests', value: 7, diff --git a/website/common/script/content/constants/animalSetAchievements.js b/website/common/script/content/constants/animalSetAchievements.js index 6f6569ff5b..dc49609d12 100644 --- a/website/common/script/content/constants/animalSetAchievements.js +++ b/website/common/script/content/constants/animalSetAchievements.js @@ -26,6 +26,23 @@ const ANIMAL_SET_ACHIEVEMENTS = { achievementKey: 'birdsOfAFeather', notificationType: 'ACHIEVEMENT_ANIMAL_SET', }, + dinosaurDynasty: { + type: 'pet', + species: [ + 'Falcon', + 'Owl', + 'Parrot', + 'Peacock', + 'Penguin', + 'Rooster', + 'Pterodactyl', + 'TRex', + 'Triceratops', + 'Velociraptor', + ], + achievementKey: 'dinosaurDynasty', + notificationType: 'ACHIEVEMENT_ANIMAL_SET', + }, domesticated: { type: 'pet', species: [ diff --git a/website/common/script/content/constants/events.js b/website/common/script/content/constants/events.js index 2349bcbffc..d68c78ee21 100644 --- a/website/common/script/content/constants/events.js +++ b/website/common/script/content/constants/events.js @@ -15,6 +15,10 @@ export const EVENTS = { season: 'normal', npcImageSuffix: '', }, + bundle202305: { + start:'2023-05-23T08:00-04:00', + end:'2023-05-31T23:59-04:00', + }, potions202305: { start:'2023-05-16T08:00-04:00', end:'2023-05-31T23:59-04:00', diff --git a/website/common/script/content/shop-featuredItems.js b/website/common/script/content/shop-featuredItems.js index 57ca7da7ff..13eb8ddff9 100644 --- a/website/common/script/content/shop-featuredItems.js +++ b/website/common/script/content/shop-featuredItems.js @@ -45,19 +45,19 @@ const featuredItems = { ]; }, quests () { // start date is 3/28 - if (moment().isBetween(EVENTS.bundle202303.start, EVENTS.bundle202303.end)) { + if (moment().isBetween(EVENTS.bundle202305.start, EVENTS.bundle202305.end)) { return [ { type: 'bundles', - path: 'bundles.jungleBuddies', + path: 'bundles.birdBuddies', }, { type: 'quests', - path: 'quests.kraken', + path: 'quests.harpy', }, { type: 'quests', - path: 'quests.nudibranch', + path: 'quests.owl', }, ]; } diff --git a/website/common/script/libs/achievements.js b/website/common/script/libs/achievements.js index 380e2b481b..d54469bead 100644 --- a/website/common/script/libs/achievements.js +++ b/website/common/script/libs/achievements.js @@ -221,6 +221,7 @@ function _getBasicAchievements (user, language) { _addSimple(result, user, { path: 'boneToPick', language }); _addSimple(result, user, { path: 'polarPro', language }); _addSimple(result, user, { path: 'plantParent', language }); + _addSimple(result, user, { path: 'dinosaurDynasty', language }); _addSimpleWithMasterCount(result, user, { path: 'beastMaster', language }); _addSimpleWithMasterCount(result, user, { path: 'mountMaster', language }); diff --git a/website/server/libs/payments/groupPayments.js b/website/server/libs/payments/groupPayments.js index b59566c2fb..dfd7f7eea5 100644 --- a/website/server/libs/payments/groupPayments.js +++ b/website/server/libs/payments/groupPayments.js @@ -180,6 +180,7 @@ async function addSubToGroupUser (member, group) { } // save unused hourglass and mystery items + plan.perkMonthCount = memberPlan.perkMonthCount; plan.consecutive.trinkets = memberPlan.consecutive.trinkets; plan.mysteryItems = memberPlan.mysteryItems; diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index ce609e022d..41e2d1ff26 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -253,13 +253,18 @@ async function createSubscription (data) { } = await prepareSubscriptionValues(data); // Block sub perks - if (months > 0 && (!data.gift || !isNewSubscription)) { + if (months > 1 && (!data.gift || !isNewSubscription)) { if (!data.gift && !groupId) { plan.consecutive.offset = block.months; } + } else if (months === 1) { + plan.consecutive.offset = 0; } if (months > 1 || data.gift) { await plan.incrementPerkCounterAndReward(recipient._id, months); + } else { + // Make sure the perkMonthCount field is initialized. + await plan.incrementPerkCounterAndReward(recipient._id, 0); } if (recipient !== group) { diff --git a/website/server/libs/user/index.js b/website/server/libs/user/index.js index 20f1616310..e3f9e8d604 100644 --- a/website/server/libs/user/index.js +++ b/website/server/libs/user/index.js @@ -130,6 +130,7 @@ export async function update (req, res, { isV3 = false }) { uuid: user._id, hitType: 'event', category: 'behavior', + headers: req.headers, }); } @@ -187,6 +188,7 @@ export async function update (req, res, { isV3 = false }) { uuid: user._id, hitType: 'event', category: 'behavior', + headers: req.headers, }); } else if (key === 'tags') { if (!Array.isArray(val)) throw new BadRequest('Tag list must be an array.'); @@ -274,6 +276,7 @@ export async function reset (req, res, { isV3 = false }) { uuid: user._id, hitType: 'event', category: 'behavior', + headers: req.headers, }); res.respond(200, ...resetRes); diff --git a/website/server/models/subscriptionPlan.js b/website/server/models/subscriptionPlan.js index 31849ceb94..8bc7daa587 100644 --- a/website/server/models/subscriptionPlan.js +++ b/website/server/models/subscriptionPlan.js @@ -56,13 +56,20 @@ schema.methods.incrementPerkCounterAndReward = async function incrementPerkCount if (typeof adding === 'string' || adding instanceof String) { addingNumber = parseInt(adding, 10); } + const isSingleMonthPlan = this.planId === 'basic_earned' || this.planId === 'group_plan_auto' || this.planId === 'group_monthly'; // if perkMonthCount wasn't used before, initialize it. if (this.perkMonthCount === undefined || this.perkMonthCount === -1) { - if (this.planId === 'basic_earned') { + if (isSingleMonthPlan && this.consecutive.count > 0) { this.perkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH; } else { this.perkMonthCount = 0; } + } else if (isSingleMonthPlan) { + const expectedPerkMonthCount = (this.consecutive.count - 1) % SUBSCRIPTION_BASIC_BLOCK_LENGTH; + if (this.perkMonthCount === (expectedPerkMonthCount - 1)) { + // User was affected by a bug that makes their perkMonthCount off by one + this.perkMonthCount += 1; + } } this.perkMonthCount += addingNumber; diff --git a/website/server/models/user/schema.js b/website/server/models/user/schema.js index a0148bc7dc..8a98ee17ce 100644 --- a/website/server/models/user/schema.js +++ b/website/server/models/user/schema.js @@ -154,6 +154,7 @@ export default new Schema({ boneToPick: Boolean, polarPro: Boolean, plantParent: Boolean, + dinosaurDynasty: Boolean, // Onboarding Guide createdTask: Boolean, completedTask: Boolean,