Compare commits
	
		
			316 Commits
		
	
	
		
			phillip/re
			...
			v4.267.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7b4dd36827 | ||
|  | be65042463 | ||
|  | cccb6a9c02 | ||
|  | 916c7c49e7 | ||
|  | 164121d9e4 | ||
|  | a2d209a34b | ||
|  | c8adf20804 | ||
|  | de132c59ea | ||
|  | e5f6c4ba0f | ||
|  | 360c17c56e | ||
|  | c8b98678d0 | ||
|  | ea7e5d2a8d | ||
|  | afee09e7cb | ||
|  | 01fea6b968 | ||
|  | 2df6b6461b | ||
|  | 8c96ac241a | ||
|  | c0362c614e | ||
|  | 229ed46425 | ||
|  | 7363f08a86 | ||
|  | 2322f7e342 | ||
|  | 7ede3acd01 | ||
|  | 57f17a08e8 | ||
|  | 63453ce01b | ||
|  | 888f6f2486 | ||
|  | e8501f5cf8 | ||
|  | fc49015ff0 | ||
|  | eb4e930e63 | ||
|  | 53c536b525 | ||
|  | e2defc675e | ||
|  | f0fa2508a9 | ||
|  | 232a62ffc7 | ||
|  | d2d4af227b | ||
|  | ca1200b689 | ||
|  | 008579363c | ||
|  | 786b1ec670 | ||
|  | 573de80a91 | ||
|  | 115340e62d | ||
|  | 5de2573521 | ||
|  | b472af532c | ||
|  | b264e539f4 | ||
|  | c726208d6e | ||
|  | b1d2fff13f | ||
|  | 027e61a93e | ||
|  | c35afb7cfe | ||
|  | 8cd706fd95 | ||
|  | 3a4620976e | ||
|  | a16098ccda | ||
|  | 118c8421fe | ||
|  | a363e68080 | ||
|  | 1940062200 | ||
|  | aea075d0bf | ||
|  | 7388707a43 | ||
|  | 3e63d74b2c | ||
|  | 199ce3e6f7 | ||
|  | 597f74c84b | ||
|  | c83a8c9766 | ||
|  | c481354f78 | ||
|  | ec9973f9d2 | ||
|  | 971b124b05 | ||
|  | de3f1b3f5e | ||
|  | 7098d2a72e | ||
|  | bbea789700 | ||
|  | 5359a2bf3d | ||
|  | 93e922e774 | ||
|  | 377b152ffd | ||
|  | 8ff8213954 | ||
|  | 00bdf81902 | ||
|  | 4ae50e4d44 | ||
|  | 383cd84016 | ||
|  | f6f1202baf | ||
|  | 0c65ff6fec | ||
|  | 83f5c92ff1 | ||
|  | 8dcacdc92e | ||
|  | 7874ae5092 | ||
|  | 481719e513 | ||
|  | 9657112bca | ||
|  | f7026b2478 | ||
|  | e39b3bdd35 | ||
|  | a210ab57b0 | ||
|  | 3f3e0e2ae8 | ||
|  | 65f12ac9ea | ||
|  | d0941810a7 | ||
|  | b77deb28b4 | ||
|  | 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 | ||
|  | df25e0574d | ||
|  | 8b2af1ef56 | ||
|  | 21652c2670 | ||
|  | d1ee679810 | ||
|  | 67988da33c | ||
|  | fae26a517d | ||
|  | e3c86349b4 | ||
|  | 6604f38144 | ||
|  | 037882b50a | ||
|  | 15deb778fd | ||
|  | 7d2529f5e1 | ||
|  | 8d732c59c4 | ||
|  | 3a34aa4cc5 | ||
|  | e7fc7feddd | ||
|  | 7fd899b642 | ||
|  | 36d2ad6b9b | ||
|  | 164dbdcf10 | ||
|  | b65fa941b9 | ||
|  | ab953440e3 | ||
|  | 1143f690d1 | ||
|  | 08469c556b | ||
|  | 13a25ad89e | ||
|  | 8e2e170930 | ||
|  | e6a7d15644 | ||
|  | 6a4b08203f | ||
|  | c9016c8d42 | ||
|  | 31685c3e94 | ||
|  | c25b09c7ed | 
| @@ -86,5 +86,6 @@ | ||||
|   "RATE_LIMITER_ENABLED": "false", | ||||
|   "REDIS_HOST": "aaabbbcccdddeeefff", | ||||
|   "REDIS_PORT": "1234", | ||||
|   "REDIS_PASSWORD": "12345678" | ||||
|   "REDIS_PASSWORD": "12345678", | ||||
|   "TRUSTED_DOMAINS": "https://localhost,https://habitica.com" | ||||
| } | ||||
|   | ||||
							
								
								
									
										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 = { | ||||
|   | ||||
							
								
								
									
										808
									
								
								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.251.0", | ||||
|   "version": "4.267.1", | ||||
|   "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", | ||||
| @@ -54,7 +54,7 @@ | ||||
|     "nconf": "^0.12.0", | ||||
|     "node-gcm": "^1.0.5", | ||||
|     "on-headers": "^1.0.2", | ||||
|     "passport": "^0.6.0", | ||||
|     "passport": "^0.5.0", | ||||
|     "passport-facebook": "^3.0.0", | ||||
|     "passport-google-oauth2": "^0.2.0", | ||||
|     "passport-google-oauth20": "2.0.0", | ||||
| @@ -67,12 +67,12 @@ | ||||
|     "remove-markdown": "^0.5.0", | ||||
|     "rimraf": "^3.0.2", | ||||
|     "short-uuid": "^4.2.2", | ||||
|     "stripe": "^10.13.0", | ||||
|     "superagent": "^8.0.5", | ||||
|     "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; | ||||
|   | ||||
							
								
								
									
										12490
									
								
								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,8 @@ | ||||
|       <sub-canceled-modal v-if="isUserLoaded" /> | ||||
|       <bug-report-modal v-if="isUserLoaded" /> | ||||
|       <bug-report-success-modal v-if="isUserLoaded" /> | ||||
|       <external-link-modal /> | ||||
|       <birthday-modal /> | ||||
|       <snackbars /> | ||||
|       <router-view v-if="!isUserLoggedIn || isStaticPage" /> | ||||
|       <template v-else> | ||||
| @@ -42,6 +44,7 @@ | ||||
|           <damage-paused-banner /> | ||||
|           <gems-promo-banner /> | ||||
|           <gift-promo-banner /> | ||||
|           <birthday-banner /> | ||||
|           <notifications-display /> | ||||
|           <app-menu /> | ||||
|           <div | ||||
| @@ -153,11 +156,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'; | ||||
| @@ -171,6 +176,7 @@ import amazonPaymentsModal from '@/components/payments/amazonModal'; | ||||
| import paymentsSuccessModal from '@/components/payments/successModal'; | ||||
| import subCancelModalConfirm from '@/components/payments/cancelModalConfirm'; | ||||
| import subCanceledModal from '@/components/payments/canceledModal'; | ||||
| import externalLinkModal from '@/components/externalLinkModal.vue'; | ||||
|  | ||||
| import spellsMixin from '@/mixins/spells'; | ||||
| import { | ||||
| @@ -191,9 +197,11 @@ export default { | ||||
|     AppMenu, | ||||
|     AppHeader, | ||||
|     AppFooter, | ||||
|     birthdayModal, | ||||
|     DamagePausedBanner, | ||||
|     GemsPromoBanner, | ||||
|     GiftPromoBanner, | ||||
|     BirthdayBanner, | ||||
|     notificationsDisplay, | ||||
|     snackbars, | ||||
|     BuyModal, | ||||
| @@ -204,6 +212,7 @@ export default { | ||||
|     subCanceledModal, | ||||
|     bugReportModal, | ||||
|     bugReportSuccessModal, | ||||
|     externalLinkModal, | ||||
|   }, | ||||
|   mixins: [notifications, spellsMixin], | ||||
|   data () { | ||||
|   | ||||
| @@ -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()" | ||||
|             > | ||||
| @@ -205,7 +213,7 @@ | ||||
|             </a> | ||||
|             <a | ||||
|               class="social-circle" | ||||
|               href="https://twitter.com/habitica" | ||||
|               href="https://twitter.com/habitica/" | ||||
|               target="_blank" | ||||
|             > | ||||
|               <div | ||||
| @@ -215,7 +223,7 @@ | ||||
|             </a> | ||||
|             <a | ||||
|               class="social-circle" | ||||
|               href="https://www.facebook.com/Habitica" | ||||
|               href="https://www.facebook.com/Habitica/" | ||||
|               target="_blank" | ||||
|             > | ||||
|               <div | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -322,6 +322,7 @@ import omit from 'lodash/omit'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| import { userStateMixin } from '../../mixins/userState'; | ||||
| import externalLinks from '../../mixins/externalLinks'; | ||||
| import memberSearchDropdown from '@/components/members/memberSearchDropdown'; | ||||
| import closeChallengeModal from './closeChallengeModal'; | ||||
| import Column from '../tasks/column'; | ||||
| @@ -358,7 +359,7 @@ export default { | ||||
|     userLink, | ||||
|     groupLink, | ||||
|   }, | ||||
|   mixins: [challengeMemberSearchMixin, userStateMixin], | ||||
|   mixins: [challengeMemberSearchMixin, externalLinks, userStateMixin], | ||||
|   props: ['challengeId'], | ||||
|   data () { | ||||
|     return { | ||||
| @@ -414,6 +415,10 @@ export default { | ||||
|   mounted () { | ||||
|     if (!this.searchId) this.searchId = this.challengeId; | ||||
|     if (!this.challenge._id) this.loadChallenge(); | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   async beforeRouteUpdate (to, from, next) { | ||||
|     this.searchId = to.params.challengeId; | ||||
|   | ||||
| @@ -120,6 +120,7 @@ import { mapState } from '@/libs/store'; | ||||
| import Sidebar from './sidebar'; | ||||
| import ChallengeItem from './challengeItem'; | ||||
| import challengeModal from './challengeModal'; | ||||
| import externalLinks from '@/mixins/externalLinks'; | ||||
| import challengeUtilities from '@/mixins/challengeUtilities'; | ||||
|  | ||||
| import positiveIcon from '@/assets/svg/positive.svg'; | ||||
| @@ -131,7 +132,7 @@ export default { | ||||
|     challengeModal, | ||||
|     MugenScroll, | ||||
|   }, | ||||
|   mixins: [challengeUtilities], | ||||
|   mixins: [challengeUtilities, externalLinks], | ||||
|   data () { | ||||
|     return { | ||||
|       loading: true, | ||||
| @@ -177,6 +178,10 @@ export default { | ||||
|       section: this.$t('challenges'), | ||||
|     }); | ||||
|     this.loadChallenges(); | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   methods: { | ||||
|     updateSearch (eventData) { | ||||
|   | ||||
| @@ -81,6 +81,8 @@ import challengeModal from './challengeModal'; | ||||
| import { mapState } from '@/libs/store'; | ||||
| import markdownDirective from '@/directives/markdown'; | ||||
|  | ||||
| import externalLinks from '../../mixins/externalLinks'; | ||||
|  | ||||
| import challengeItem from './challengeItem'; | ||||
| import challengeIcon from '@/assets/svg/challenge.svg'; | ||||
|  | ||||
| @@ -92,6 +94,7 @@ export default { | ||||
|   directives: { | ||||
|     markdown: markdownDirective, | ||||
|   }, | ||||
|   mixins: [externalLinks], | ||||
|   props: ['group'], | ||||
|   data () { | ||||
|     return { | ||||
| @@ -118,6 +121,10 @@ export default { | ||||
|   }, | ||||
|   mounted () { | ||||
|     this.loadChallenges(); | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async loadChallenges () { | ||||
|   | ||||
| @@ -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 () { | ||||
|   | ||||
| @@ -145,6 +145,7 @@ import Sidebar from './sidebar'; | ||||
| import ChallengeItem from './challengeItem'; | ||||
| import challengeModal from './challengeModal'; | ||||
| import challengeUtilities from '@/mixins/challengeUtilities'; | ||||
| import externalLinks from '@/mixins/externalLinks'; | ||||
|  | ||||
| import challengeIcon from '@/assets/svg/challenge.svg'; | ||||
| import positiveIcon from '@/assets/svg/positive.svg'; | ||||
| @@ -156,7 +157,7 @@ export default { | ||||
|     challengeModal, | ||||
|     MugenScroll, | ||||
|   }, | ||||
|   mixins: [challengeUtilities], | ||||
|   mixins: [challengeUtilities, externalLinks], | ||||
|   data () { | ||||
|     return { | ||||
|       icons: Object.freeze({ | ||||
| @@ -203,6 +204,10 @@ export default { | ||||
|       section: this.$t('challenges'), | ||||
|     }); | ||||
|     this.loadChallenges(); | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   methods: { | ||||
|     updateSearch (eventData) { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										209
									
								
								website/client/src/components/externalLinkModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,209 @@ | ||||
| <template> | ||||
|   <b-modal | ||||
|     id="external-link-modal" | ||||
|     size="md" | ||||
|   > | ||||
|     <!-- HEADER --> | ||||
|     <div slot="modal-header"> | ||||
|       <div | ||||
|         class="modal-close" | ||||
|         @click="close()" | ||||
|       > | ||||
|         <div | ||||
|           class="icon-close" | ||||
|           v-html="icons.close" | ||||
|         > | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="exclamation-container d-flex align-items-center justify-content-center"> | ||||
|         <div | ||||
|           v-once | ||||
|           class="svg-icon svg-exclamation" | ||||
|           v-html="icons.exclamation" | ||||
|         ></div> | ||||
|       </div> | ||||
|       <h2> | ||||
|         {{ $t('leaveHabitica') }} | ||||
|       </h2> | ||||
|     </div> | ||||
|  | ||||
|     <!-- BODY --> | ||||
|     <div | ||||
|       class="row leave-warning-text" | ||||
|       v-html="$t('leaveHabiticaText')" | ||||
|     > | ||||
|     </div> | ||||
|     <div | ||||
|       class="skip-modal" | ||||
|     > | ||||
|       {{ $t('skipExternalLinkModal') }} | ||||
|     </div> | ||||
|  | ||||
|     <!-- FOOTER --> | ||||
|     <div slot="modal-footer"> | ||||
|       <button | ||||
|         v-once | ||||
|         class="btn btn-primary" | ||||
|         @click="proceed()" | ||||
|       > | ||||
|         {{ $t('continue') }} | ||||
|       </button> | ||||
|       <div | ||||
|         v-once | ||||
|         class="close-link justify-content-center" | ||||
|         @click="close()" | ||||
|       > | ||||
|         {{ $t('cancel') }} | ||||
|       </div> | ||||
|     </div> | ||||
|   </b-modal> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss"> | ||||
| @import '~@/assets/scss/colors.scss'; | ||||
|  | ||||
| #external-link-modal { | ||||
|   &.modal { | ||||
|     display: flex !important; | ||||
|   } | ||||
|  | ||||
|   .modal-md { | ||||
|     max-width: 448px; | ||||
|     min-width: 330px; | ||||
|     margin: auto; | ||||
|  | ||||
|   .modal-close { | ||||
|     position: absolute; | ||||
|     right: 12px; | ||||
|     top: 12px; | ||||
|     cursor: pointer; | ||||
|  | ||||
|     .icon-close { | ||||
|       width: 16px; | ||||
|       height: 16px; | ||||
|       vertical-align: middle; | ||||
|  | ||||
|       & svg { | ||||
|         fill: $yellow-1; | ||||
|         opacity: 0.75; | ||||
|       } | ||||
|        & :hover { | ||||
|         fill: $yellow-1; | ||||
|         opacity: 1; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .modal-content { | ||||
|     background: transparent; | ||||
|   } | ||||
|  | ||||
|   .modal-header { | ||||
|     justify-content: center; | ||||
|     padding-top: 32px; | ||||
|     padding-bottom: 0px; | ||||
|     background: $yellow-100; | ||||
|     border-top-right-radius: 8px; | ||||
|     border-top-left-radius: 8px; | ||||
|     border-bottom: none; | ||||
|  | ||||
|     .exclamation-container { | ||||
|       width: 64px; | ||||
|       height: 64px; | ||||
|       border-radius: 50%; | ||||
|       background: $yellow-1; | ||||
|       margin: 0 auto; | ||||
|       margin-bottom: 16px; | ||||
|     } | ||||
|  | ||||
|     .svg-exclamation { | ||||
|       width: 8px; | ||||
|       color: $white; | ||||
|     } | ||||
|  | ||||
|     h2 { | ||||
|       color: $yellow-1; | ||||
|       margin-bottom: 16px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .modal-body { | ||||
|     padding: 16px 44px 20px 44px; | ||||
|     background: $white; | ||||
|  | ||||
|     .leave-warning-text { | ||||
|       font-size: 0.875rem; | ||||
|       line-height: 1.71; | ||||
|       text-align: center; | ||||
|       margin-top:24px; | ||||
|     } | ||||
|  | ||||
|     .skip-modal { | ||||
|       color: $gray-100; | ||||
|       font-size: 0.75rem; | ||||
|       text-align: center; | ||||
|       line-height: 1.33; | ||||
|       margin-top: 16px; | ||||
|       // padding-bottom: 24px; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|     .modal-footer { | ||||
|       background: $white; | ||||
|       border-bottom-right-radius: 8px; | ||||
|       border-bottom-left-radius: 8px; | ||||
|       justify-content: center; | ||||
|       border-top: none; | ||||
|       padding-top: 0; | ||||
|     } | ||||
|     .close-link { | ||||
|       color: $purple-300; | ||||
|       line-height: 1.71; | ||||
|       font-size: 0.875rem; | ||||
|       cursor: pointer; | ||||
|       margin-top:16px; | ||||
|       margin-bottom: 8px; | ||||
|       text-align: center; | ||||
|  | ||||
|       &:hover { | ||||
|         text-decoration: underline; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| import exclamationIcon from '@/assets/svg/exclamation.svg'; | ||||
| import closeIcon from '@/assets/svg/new-close.svg'; | ||||
|  | ||||
| export default { | ||||
|   data () { | ||||
|     return { | ||||
|       icons: Object.freeze({ | ||||
|         close: closeIcon, | ||||
|         exclamation: exclamationIcon, | ||||
|       }), | ||||
|       url: '', | ||||
|     }; | ||||
|   }, | ||||
|   mounted () { | ||||
|     this.$root.$on('habitica:external-link', url => { | ||||
|       this.url = url; | ||||
|       this.$root.$emit('bv::show::modal', 'external-link-modal'); | ||||
|     }); | ||||
|   }, | ||||
|   beforeDestroy () { | ||||
|     this.$root.$off('habitica:external-link'); | ||||
|   }, | ||||
|   methods: { | ||||
|     close () { | ||||
|       this.$root.$emit('bv::hide::modal', 'external-link-modal'); | ||||
|     }, | ||||
|     proceed () { | ||||
|       window.open(this.url, '_blank').focus(); | ||||
|       this.close(); | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| @@ -87,6 +87,8 @@ | ||||
| <script> | ||||
| import debounce from 'lodash/debounce'; | ||||
|  | ||||
| import externalLinks from '../../mixins/externalLinks'; | ||||
|  | ||||
| import autocomplete from '../chat/autoComplete'; | ||||
| import communityGuidelines from './communityGuidelines'; | ||||
| import chatMessage from '../chat/chatMessages'; | ||||
| @@ -103,6 +105,7 @@ export default { | ||||
|     communityGuidelines, | ||||
|     chatMessage, | ||||
|   }, | ||||
|   mixins: [externalLinks], | ||||
|   props: ['label', 'group', 'placeholder'], | ||||
|   data () { | ||||
|     return { | ||||
| @@ -132,6 +135,10 @@ export default { | ||||
|   }, | ||||
|   mounted () { | ||||
|     this.textbox = this.$refs['user-entry']; | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   methods: { | ||||
|     // https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -340,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()" | ||||
|               > | ||||
| @@ -521,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; | ||||
|   } | ||||
| @@ -759,6 +745,7 @@ | ||||
| </style> | ||||
|  | ||||
| <script> | ||||
| import find from 'lodash/find'; | ||||
| import { mapState } from '@/libs/store'; | ||||
| import { goToModForm } from '@/libs/modform'; | ||||
|  | ||||
| @@ -835,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)}`; | ||||
|       } | ||||
|   | ||||
| @@ -139,6 +139,8 @@ | ||||
| import axios from 'axios'; | ||||
| import moment from 'moment'; | ||||
|  | ||||
| import externalLinks from '../../mixins/externalLinks'; | ||||
|  | ||||
| import renderWithMentions from '@/libs/renderWithMentions'; | ||||
| import { mapState } from '@/libs/store'; | ||||
| import userLink from '../userLink'; | ||||
| @@ -150,6 +152,7 @@ export default { | ||||
|   components: { | ||||
|     userLink, | ||||
|   }, | ||||
|   mixins: [externalLinks], | ||||
|   filters: { | ||||
|     timeAgo (value) { | ||||
|       return moment(value).fromNow(); | ||||
| @@ -179,6 +182,10 @@ export default { | ||||
|   }, | ||||
|   mounted () { | ||||
|     this.$emit('message-card-mounted'); | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   methods: { | ||||
|     report () { | ||||
|   | ||||
							
								
								
									
										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) { | ||||
|   | ||||
| @@ -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)`, | ||||
|       }; | ||||
|     }, | ||||
|   }, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -355,6 +355,7 @@ import Task from './task'; | ||||
| import ClearCompletedTodos from './clearCompletedTodos'; | ||||
| import buyMixin from '@/mixins/buy'; | ||||
| import sync from '@/mixins/sync'; | ||||
| import externalLinks from '@/mixins/externalLinks'; | ||||
| import { mapState, mapActions, mapGetters } from '@/libs/store'; | ||||
| import shopItem from '../shops/shopItem'; | ||||
| import BuyQuestModal from '@/components/shops/quests/buyQuestModal.vue'; | ||||
| @@ -384,7 +385,7 @@ export default { | ||||
|     shopItem, | ||||
|     draggable, | ||||
|   }, | ||||
|   mixins: [buyMixin, notifications, sync], | ||||
|   mixins: [buyMixin, notifications, sync, externalLinks], | ||||
|   // @TODO Set default values for props | ||||
|   // allows for better control of props values | ||||
|   // allows for better control of where this component is called | ||||
| @@ -534,6 +535,10 @@ export default { | ||||
|       if (this.activeFilter.label !== 'complete2') return; | ||||
|       this.loadCompletedTodos(); | ||||
|     }); | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   beforeDestroy () { | ||||
|     this.$root.$off('buyModal::boughtItem'); | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -83,6 +83,7 @@ | ||||
| <script> | ||||
| import moment from 'moment'; | ||||
| import { mapState } from '@/libs/store'; | ||||
| import externalLinks from '@/mixins/externalLinks'; | ||||
| import scoreTask from '@/mixins/scoreTask'; | ||||
| import sync from '@/mixins/sync'; | ||||
| import Task from './task'; | ||||
| @@ -93,7 +94,7 @@ export default { | ||||
|     Task, | ||||
|     LoadingSpinner, | ||||
|   }, | ||||
|   mixins: [scoreTask, sync], | ||||
|   mixins: [externalLinks, scoreTask, sync], | ||||
|   props: { | ||||
|     yesterDailies: { | ||||
|       type: Array, | ||||
| @@ -108,6 +109,9 @@ export default { | ||||
|       dueDate: moment().subtract(1, 'days'), | ||||
|     }; | ||||
|   }, | ||||
|   updated () { | ||||
|     this.handleExternalLinks(); | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState({ user: 'user.data' }), | ||||
|     tasksByType () { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||