Merge branch 'develop' into increment-component
This commit is contained in:
+1
-1
Submodule habitica-images updated: 941bf731d6...fadca5c096
Generated
+78
-46
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "4.249.3",
|
||||
"version": "4.250.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -2038,9 +2038,9 @@
|
||||
"integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw=="
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
|
||||
"integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
|
||||
"integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
@@ -2053,15 +2053,26 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.5.tgz",
|
||||
"integrity": "sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@sinonjs/samsam": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz",
|
||||
"integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-7.0.1.tgz",
|
||||
"integrity": "sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.6.0",
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"type-detect": "^4.0.8"
|
||||
}
|
||||
@@ -3067,7 +3078,7 @@
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
@@ -4277,14 +4288,14 @@
|
||||
"integrity": "sha512-z0R4cT5357OEAVkP1CEFTHz1egpu2gYiWm2WJOY/sQDhojEXUYL4m3v2kYi5wER3PkMRL+GgfDhed2kGzrHSZA=="
|
||||
},
|
||||
"chai": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
|
||||
"integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==",
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz",
|
||||
"integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"assertion-error": "^1.1.0",
|
||||
"check-error": "^1.0.2",
|
||||
"deep-eql": "^3.0.1",
|
||||
"deep-eql": "^4.1.2",
|
||||
"get-func-name": "^2.0.0",
|
||||
"loupe": "^2.3.1",
|
||||
"pathval": "^1.1.1",
|
||||
@@ -5329,9 +5340,9 @@
|
||||
}
|
||||
},
|
||||
"deep-eql": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
|
||||
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.2.tgz",
|
||||
"integrity": "sha512-gT18+YW4CcW/DBNTwAmqTtkJh7f9qqScu2qFVlx7kCoeY9tlBu9cUcr7+I+Z/noG8INehS3xQgLpTtd/QUTn4w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "^4.0.0"
|
||||
@@ -5485,7 +5496,7 @@
|
||||
"dezalgo": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
|
||||
"integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=",
|
||||
"integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==",
|
||||
"requires": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
@@ -7570,7 +7581,7 @@
|
||||
"get-func-name": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
|
||||
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||
"integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
@@ -10130,7 +10141,7 @@
|
||||
"lodash.get": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
|
||||
"integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.includes": {
|
||||
@@ -10233,9 +10244,9 @@
|
||||
}
|
||||
},
|
||||
"loupe": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.1.tgz",
|
||||
"integrity": "sha512-EN1D3jyVmaX4tnajVlfbREU4axL647hLec1h/PXAb8CPDMJiYitcWF2UeLVNttRqaIqQs4x+mRvXf+d+TlDrCA==",
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
|
||||
"integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-func-name": "^2.0.0"
|
||||
@@ -11397,22 +11408,42 @@
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"nise": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz",
|
||||
"integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==",
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/nise/-/nise-5.1.2.tgz",
|
||||
"integrity": "sha512-+gQjFi8v+tkfCuSCxfURHLhRhniE/+IaYbIphxAN2JRR9SHKhY8hgXpaXiYfHdw+gcGe4buxgbprBQFab9FkhA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.8.3",
|
||||
"@sinonjs/fake-timers": ">=5",
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"@sinonjs/fake-timers": "^7.0.4",
|
||||
"@sinonjs/text-encoding": "^0.7.1",
|
||||
"just-extend": "^4.0.2",
|
||||
"path-to-regexp": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/fake-timers": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
|
||||
"integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.5.tgz",
|
||||
"integrity": "sha512-rTpCA0wG1wUxglBSFdMMY0oTrKYvgf4fNgv/sXbfCVAdf+FnPBdKJR/7XbpTCwbCrvCbdPYnlWaUUYz4V2fPDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-detect": "4.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||
"dev": true
|
||||
},
|
||||
"path-to-regexp": {
|
||||
@@ -12063,12 +12094,13 @@
|
||||
"integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ="
|
||||
},
|
||||
"passport": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.5.0.tgz",
|
||||
"integrity": "sha512-ln+ue5YaNDS+fes6O5PCzXKSseY5u8MYhX9H5Co4s+HfYI5oqvnHKoOORLYDUPh+8tHvrxugF2GFcUA1Q1Gqfg==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz",
|
||||
"integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==",
|
||||
"requires": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1"
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"passport-facebook": {
|
||||
@@ -12179,7 +12211,7 @@
|
||||
"pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"pause-stream": {
|
||||
"version": "0.0.11",
|
||||
@@ -12780,9 +12812,9 @@
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.9",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
|
||||
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.15.0",
|
||||
@@ -13595,16 +13627,16 @@
|
||||
}
|
||||
},
|
||||
"sinon": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.1.tgz",
|
||||
"integrity": "sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==",
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sinon/-/sinon-14.0.2.tgz",
|
||||
"integrity": "sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinonjs/commons": "^1.8.3",
|
||||
"@sinonjs/commons": "^2.0.0",
|
||||
"@sinonjs/fake-timers": "^9.1.2",
|
||||
"@sinonjs/samsam": "^6.1.1",
|
||||
"@sinonjs/samsam": "^7.0.1",
|
||||
"diff": "^5.0.0",
|
||||
"nise": "^5.1.1",
|
||||
"nise": "^5.1.2",
|
||||
"supports-color": "^7.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -14283,9 +14315,9 @@
|
||||
"integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ=="
|
||||
},
|
||||
"superagent": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.2.tgz",
|
||||
"integrity": "sha512-QtYZ9uaNAMexI7XWl2vAXAh0j4q9H7T0WVEI/y5qaUB3QLwxo+voUgCQ217AokJzUTIVOp0RTo7fhZrwhD7A2Q==",
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.0.4.tgz",
|
||||
"integrity": "sha512-yaQB7YqAHhutt8yU7XIRKXGXtkxKidVuiDJ8FJPBG7JPd7Ckv4X9cOdxVGH5DFsPXodsCrOSzdUi2xL+o9z1qw==",
|
||||
"requires": {
|
||||
"component-emitter": "^1.3.0",
|
||||
"cookiejar": "^2.1.3",
|
||||
@@ -14296,7 +14328,7 @@
|
||||
"methods": "^1.1.2",
|
||||
"mime": "2.6.0",
|
||||
"qs": "^6.11.0",
|
||||
"semver": "^7.3.7"
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.249.3",
|
||||
"version": "4.250.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
@@ -54,7 +54,7 @@
|
||||
"nconf": "^0.12.0",
|
||||
"node-gcm": "^1.0.5",
|
||||
"on-headers": "^1.0.2",
|
||||
"passport": "^0.5.0",
|
||||
"passport": "^0.6.0",
|
||||
"passport-facebook": "^3.0.0",
|
||||
"passport-google-oauth2": "^0.2.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
@@ -63,12 +63,12 @@
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.0",
|
||||
"redis": "^3.1.2",
|
||||
"regenerator-runtime": "^0.13.9",
|
||||
"regenerator-runtime": "^0.13.11",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
"stripe": "^10.13.0",
|
||||
"superagent": "^8.0.2",
|
||||
"superagent": "^8.0.4",
|
||||
"universal-analytics": "^0.5.3",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^8.3.2",
|
||||
@@ -111,7 +111,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"chai": "^4.3.6",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chai-moment": "^0.1.0",
|
||||
"chalk": "^5.1.2",
|
||||
@@ -122,7 +122,7 @@
|
||||
"monk": "^7.3.4",
|
||||
"require-again": "^2.0.0",
|
||||
"run-rs": "^0.7.7",
|
||||
"sinon": "^14.0.1",
|
||||
"sinon": "^14.0.2",
|
||||
"sinon-chai": "^3.7.0",
|
||||
"sinon-stub-promise": "^4.0.0"
|
||||
},
|
||||
|
||||
@@ -218,7 +218,6 @@ describe('Apple Payments', () => {
|
||||
headers = {};
|
||||
receipt = `{"token": "${token}"}`;
|
||||
nextPaymentProcessing = moment.utc().add({ days: 2 });
|
||||
user = new User();
|
||||
|
||||
iapSetupStub = sinon.stub(iap, 'setup')
|
||||
.resolves();
|
||||
@@ -299,7 +298,6 @@ describe('Apple Payments', () => {
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
productId: option.sku,
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[option.subKey];
|
||||
|
||||
@@ -323,116 +321,14 @@ 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];
|
||||
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(),
|
||||
productId: newOption.sku,
|
||||
transactionId: `${token}new`,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[newOption.subKey];
|
||||
|
||||
await applePayments.subscribe(newOption.sku,
|
||||
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(),
|
||||
productId: newOption.sku,
|
||||
transactionId: `${token}new`,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
sub = common.content.subscriptionBlocks[newOption.subKey];
|
||||
|
||||
await applePayments.subscribe(newOption.sku,
|
||||
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 using the same subscription', async () => {
|
||||
user = new User();
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
await user.save();
|
||||
it('errors when a user is already subscribed', async () => {
|
||||
payments.createSubscription.restore();
|
||||
|
||||
iap.getPurchaseData.restore();
|
||||
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
|
||||
.returns([{
|
||||
expirationDate: moment.utc().add({ day: 1 }).toDate(),
|
||||
productId: sku,
|
||||
transactionId: token,
|
||||
originalTransactionId: token,
|
||||
}]);
|
||||
user = new User();
|
||||
await user.save();
|
||||
|
||||
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
|
||||
user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
await user.save();
|
||||
|
||||
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
|
||||
@@ -370,10 +370,6 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Purchasing a subscription for self', () => {
|
||||
beforeEach(() => {
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
});
|
||||
|
||||
it('creates a subscription', async () => {
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
@@ -400,7 +396,6 @@ describe('payments/index', () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add(2, 'months');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -411,7 +406,6 @@ describe('payments/index', () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).subtract(2, 'months');
|
||||
expect(user.purchased.plan.extraMonths).to.eql(0);
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -421,7 +415,6 @@ describe('payments/index', () => {
|
||||
it('does not reset Gold-to-Gems cap on additional subscription', async () => {
|
||||
user.purchased.plan = plan;
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -472,96 +465,9 @@ 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', () => {
|
||||
beforeEach(() => {
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
});
|
||||
|
||||
it('adds block months to plan.consecutive.offset', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
@@ -582,6 +488,7 @@ 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);
|
||||
@@ -589,6 +496,7 @@ 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);
|
||||
@@ -598,7 +506,6 @@ describe('payments/index', () => {
|
||||
data.sub.key = 'basic_12mo';
|
||||
|
||||
await api.createSubscription(data);
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
|
||||
@@ -625,134 +532,6 @@ describe('payments/index', () => {
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
|
||||
});
|
||||
|
||||
context('Upgrades 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' };
|
||||
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 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('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', () => {
|
||||
@@ -765,7 +544,6 @@ describe('payments/index', () => {
|
||||
now: mayMysteryItemTimeframe,
|
||||
toFake: ['Date'],
|
||||
});
|
||||
data.user.purchased.plan.dateUpdated = moment().subtract(1, 'hours').toDate();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -344,6 +344,24 @@ describe('POST /user/auth/local/register', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('enforces maximum length for the password', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = '12345678910111213141516171819202122232425262728293031323334353637383940';
|
||||
const confirmPassword = '12345678910111213141516171819202122232425262728293031323334353637383940';
|
||||
|
||||
await expect(api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword,
|
||||
})).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('invalidReqParams'),
|
||||
});
|
||||
});
|
||||
|
||||
it('requires a username', async () => {
|
||||
const email = `${generateRandomUserName()}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
Generated
+9
-18
@@ -17072,9 +17072,9 @@
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.0.tgz",
|
||||
"integrity": "sha512-Be9tbQMZds4a3C6xTmz68NlMfeONA//4dOavl/1rNw50E+/QO0KVpbcU0PcaW0nsQxurXls9ZocqFxk8R2mWEA=="
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
|
||||
"integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "1.7.0",
|
||||
@@ -28824,7 +28824,6 @@
|
||||
"version": "npm:vue-loader@16.8.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
|
||||
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"hash-sum": "^2.0.0",
|
||||
@@ -28835,7 +28834,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
@@ -28844,7 +28842,6 @@
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
@@ -28854,7 +28851,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
@@ -28862,26 +28858,22 @@
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"optional": true
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"optional": true
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"optional": true
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"loader-utils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.3.tgz",
|
||||
"integrity": "sha512-THWqIsn8QRnvLl0shHYVBN9syumU8pYWEHPTmkiVGd+7K5eFNVSY6AJhRvgGF70gg1Dz+l/k8WicvFCxdEs60A==",
|
||||
"optional": true,
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
|
||||
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
@@ -28892,7 +28884,6 @@
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"bootstrap-vue": "^2.22.0",
|
||||
"chai": "^4.3.6",
|
||||
"core-js": "^3.26.0",
|
||||
"dompurify": "^2.4.0",
|
||||
"dompurify": "^2.4.1",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-habitrpg": "^6.2.0",
|
||||
"eslint-plugin-mocha": "^5.3.0",
|
||||
@@ -49,6 +49,7 @@
|
||||
"sass": "^1.34.0",
|
||||
"sass-loader": "^8.0.2",
|
||||
"smartbanner.js": "^1.19.1",
|
||||
"stopword": "^2.0.5",
|
||||
"svg-inline-loader": "^0.8.2",
|
||||
"svg-url-loader": "^7.1.1",
|
||||
"svgo": "^1.3.2",
|
||||
|
||||
@@ -27415,6 +27415,46 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202212.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.headAccessory_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/headAccessory_mystery_202212.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shop_armor_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_armor_mystery_202212.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_headAccessory_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_headAccessory_mystery_202212.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_set_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_set_mystery_202212.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.shop_weapon_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shop_weapon_mystery_202212.png');
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
.slim_armor_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202212.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202212 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202212.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_mystery_301404 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png');
|
||||
width: 90px;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
|
||||
<path fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<path d="M6.54,13c-.3,0-.59-.13-.81-.35l-3.73-3.9,1.62-1.69,2.86,2.98L12.26,3l1.74,1.56L7.41,12.58c-.21,.25-.51,.4-.83,.42-.01,0-.03,0-.04,0Z" fill-rule="evenodd"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 268 B After Width: | Height: | Size: 236 B |
@@ -787,18 +787,15 @@ export default {
|
||||
if (sortBy === 'sortByColor') {
|
||||
groupKey = 'potionKey';
|
||||
} else if (sortBy === 'AZ') {
|
||||
groupKey = '';
|
||||
groupKey = i => i.eggName[0];
|
||||
} else if (sortBy === 'sortByHatchable') {
|
||||
groupKey = i => (i.isHatchable() ? 0 : 1);
|
||||
}
|
||||
const groupedPets = groupBy(pets, groupKey);
|
||||
|
||||
// Pets are rendered as grouped "rows". Count helps decide if show more button is necessary.
|
||||
if (sortBy === 'AZ') {
|
||||
this.petRowCount[animalGroup.key] = 1;
|
||||
} else {
|
||||
this.petRowCount[animalGroup.key] = Object.keys(groupedPets).length;
|
||||
}
|
||||
this.petRowCount[animalGroup.key] = Object.keys(groupedPets).length;
|
||||
|
||||
return groupedPets;
|
||||
},
|
||||
mounts (animalGroup, hideMissing, sortBy, searchText) {
|
||||
@@ -814,14 +811,12 @@ export default {
|
||||
if (sortBy === 'sortByColor') {
|
||||
groupKey = 'potionKey';
|
||||
} else if (sortBy === 'AZ') {
|
||||
groupKey = '';
|
||||
groupKey = i => i.eggName[0];
|
||||
}
|
||||
const groupedMounts = groupBy(mounts, groupKey);
|
||||
if (sortBy === 'AZ') {
|
||||
this.mountRowCount[animalGroup.key] = 1;
|
||||
} else {
|
||||
this.mountRowCount[animalGroup.key] = Object.keys(groupedMounts).length;
|
||||
}
|
||||
|
||||
this.mountRowCount[animalGroup.key] = Object.keys(groupedMounts).length;
|
||||
|
||||
return groupedMounts;
|
||||
},
|
||||
// Actions
|
||||
|
||||
@@ -359,8 +359,8 @@
|
||||
|
||||
.svg-icon.check {
|
||||
color: $purple-400;
|
||||
width: 0.77rem;
|
||||
height: 0.615rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.text-leadin {
|
||||
|
||||
@@ -380,7 +380,6 @@ import EquipmentAttributesGrid from '../inventory/equipment/attributesGrid.vue';
|
||||
import Item from '@/components/inventory/item';
|
||||
import Avatar from '@/components/avatar';
|
||||
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
import { drops as dropEggs } from '@/../../common/script/content/eggs';
|
||||
import { drops as dropPotions } from '@/../../common/script/content/hatching-potions';
|
||||
|
||||
@@ -437,7 +436,6 @@ export default {
|
||||
|
||||
selectedAmountToBuy: 1,
|
||||
isPinned: false,
|
||||
endDate: seasonalShopConfig.dateRange.end,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -488,6 +486,9 @@ export default {
|
||||
nonSubscriberHourglasses () {
|
||||
return (!this.user.purchased.plan.customerId && !this.user.purchased.plan.consecutive.trinkets && this.getPriceClass() === 'hourglasses');
|
||||
},
|
||||
endDate () {
|
||||
return moment(this.item.event.end);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function itemChanged () {
|
||||
|
||||
@@ -8,16 +8,6 @@
|
||||
:popover-position="'top'"
|
||||
@click="itemSelected(item)"
|
||||
>
|
||||
<span slot="popoverContent">
|
||||
<strong v-if="item.key === 'gem' && gemsLeft === 0">{{ $t('maxBuyGems') }}</strong>
|
||||
<h4 class="popover-content-title">{{ item.text }}</h4>
|
||||
<div
|
||||
v-if="item.event"
|
||||
class="mt-2"
|
||||
>
|
||||
{{ limitedString }}
|
||||
</div>
|
||||
</span>
|
||||
<template
|
||||
slot="itemBadge"
|
||||
slot-scope="ctx"
|
||||
@@ -32,11 +22,9 @@
|
||||
import _filter from 'lodash/filter';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import _map from 'lodash/map';
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import pinUtils from '@/mixins/pinUtils';
|
||||
import planGemLimits from '@/../../common/script/libs/planGemLimits';
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
import ShopItem from '../shopItem';
|
||||
import CategoryItem from './categoryItem';
|
||||
@@ -48,12 +36,6 @@ export default {
|
||||
},
|
||||
mixins: [pinUtils],
|
||||
props: ['hideLocked', 'hidePinned', 'searchBy', 'sortBy', 'category'],
|
||||
data () {
|
||||
return {
|
||||
timer: '',
|
||||
limitedString: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
content: 'content',
|
||||
@@ -106,43 +88,10 @@ export default {
|
||||
return result;
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
this.countdownString();
|
||||
this.timer = setInterval(this.countdownString, 1000);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.cancelAutoUpdate();
|
||||
},
|
||||
methods: {
|
||||
itemSelected (item) {
|
||||
this.$root.$emit('buyModal::showItem', item);
|
||||
},
|
||||
countdownString () {
|
||||
const diffDuration = moment.duration(moment(seasonalShopConfig.dateRange.end).diff(moment()));
|
||||
|
||||
if (diffDuration.asSeconds() <= 0) {
|
||||
this.limitedString = this.$t('noLongerAvailable');
|
||||
} else if (diffDuration.days() > 0 || diffDuration.months() > 0) {
|
||||
this.limitedString = this.$t('limitedAvailabilityDays', {
|
||||
days: moment(seasonalShopConfig.dateRange.end).diff(moment(), 'days'),
|
||||
hours: diffDuration.hours(),
|
||||
minutes: diffDuration.minutes(),
|
||||
});
|
||||
} else if (diffDuration.asMinutes() > 2) {
|
||||
this.limitedString = this.$t('limitedAvailabilityHours', {
|
||||
hours: diffDuration.hours(),
|
||||
minutes: diffDuration.minutes(),
|
||||
});
|
||||
} else {
|
||||
this.limitedString = this.$t('limitedAvailabilityMinutes', {
|
||||
minutes: diffDuration.minutes(),
|
||||
seconds: diffDuration.seconds(),
|
||||
});
|
||||
}
|
||||
},
|
||||
cancelAutoUpdate () {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -264,8 +264,8 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mapState } from '@/libs/store';
|
||||
import seasonalShopConfig from '@/../../common/script/libs/shops-seasonal.config';
|
||||
|
||||
import svgClock from '@/assets/svg/clock.svg';
|
||||
import svgClose from '@/assets/svg/close.svg';
|
||||
@@ -325,7 +325,6 @@ export default {
|
||||
|
||||
isPinned: false,
|
||||
selectedAmountToBuy: 1,
|
||||
endDate: seasonalShopConfig.dateRange.end,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -349,6 +348,9 @@ export default {
|
||||
if (this.priceType === 'hourglasses') return this.icons.hourglass;
|
||||
return this.icons.gem;
|
||||
},
|
||||
endDate () {
|
||||
return moment(this.item.event.end);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
item: function itemChanged () {
|
||||
|
||||
@@ -402,6 +402,8 @@ import _sortBy from 'lodash/sortBy';
|
||||
import _throttle from 'lodash/throttle';
|
||||
import _groupBy from 'lodash/groupBy';
|
||||
import _map from 'lodash/map';
|
||||
import _each from 'lodash/each';
|
||||
import * as stopword from 'stopword/dist/stopword.esm.mjs';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
import ShopItem from '../shopItem';
|
||||
@@ -426,6 +428,51 @@ import SelectTranslatedArray from '@/components/tasks/modal-controls/selectTrans
|
||||
import QuestPopover from './questPopover';
|
||||
import { worldStateMixin } from '@/mixins/worldState';
|
||||
|
||||
function splitMultipleDelims (text, delims) {
|
||||
const omniDelim = 'θνι';
|
||||
let workingText = text;
|
||||
for (const delim of delims) {
|
||||
workingText = workingText.replace(new RegExp(delim, 'g'), omniDelim);
|
||||
}
|
||||
return workingText.split(omniDelim);
|
||||
}
|
||||
|
||||
function removeStopwordsFromText (text, language) {
|
||||
// list of supported languages https://www.npmjs.com/package/stopword
|
||||
const langs = {
|
||||
bg: stopword.bul,
|
||||
cs: stopword.ces,
|
||||
da: stopword.dan,
|
||||
de: stopword.deu,
|
||||
en: stopword.eng,
|
||||
en_GB: stopword.eng,
|
||||
'en@pirate': stopword.eng.concat(["th'"]),
|
||||
es: stopword.spa,
|
||||
es_419: stopword.spa,
|
||||
fr: stopword.fra,
|
||||
he: stopword.heb,
|
||||
hu: stopword.hun,
|
||||
id: stopword.ind,
|
||||
it: stopword.ita,
|
||||
ja: stopword.jpn,
|
||||
nl: stopword.nld,
|
||||
pl: stopword.pol,
|
||||
pt: stopword.por,
|
||||
pt_BR: stopword.porBr,
|
||||
ro: stopword.ron,
|
||||
ru: stopword.rus,
|
||||
sk: stopword.slv,
|
||||
// sr: stopword.,
|
||||
sv: stopword.swe,
|
||||
tr: stopword.tur,
|
||||
uk: stopword.ukr,
|
||||
zh: stopword.zho,
|
||||
zh_TW: stopword.zho,
|
||||
};
|
||||
const splitText = splitMultipleDelims(text, [' ', "'"]);
|
||||
return stopword.removeStopwords(splitText, langs[language] || stopword.eng).join(' ').toLowerCase();
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
QuestPopover,
|
||||
@@ -539,7 +586,14 @@ export default {
|
||||
|
||||
switch (sortBy) { // eslint-disable-line default-case
|
||||
case 'AZ': {
|
||||
result = _sortBy(result, ['text']);
|
||||
if (category.identifier === 'pet' || category.identifier === 'hatchingPotion') {
|
||||
_each(result, item => {
|
||||
item.sortText = removeStopwordsFromText(item.text, this.user.preferences.language);
|
||||
});
|
||||
result = _sortBy(result, ['sortText']);
|
||||
} else {
|
||||
result = _sortBy(result, ['text']);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -206,12 +206,11 @@
|
||||
}
|
||||
|
||||
span.svg-icon.inline.check {
|
||||
height: 12px;
|
||||
width: 10px;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
margin-top: 0;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
color: $gray-200;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,25 +13,29 @@
|
||||
:key="index"
|
||||
class="faq-question"
|
||||
>
|
||||
<h2
|
||||
v-b-toggle="heading"
|
||||
role="tab"
|
||||
variant="info"
|
||||
@click="handleClick($event)"
|
||||
<div
|
||||
v-if="heading !== 'world-boss'"
|
||||
>
|
||||
{{ $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>
|
||||
<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>
|
||||
</div>
|
||||
<hr>
|
||||
<p v-markdown="$t('webFaqStillNeedHelp')"></p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="task-wrapper">
|
||||
<div class="task-wrapper" draggable>
|
||||
<div
|
||||
class="task transition"
|
||||
:class="[{
|
||||
@@ -773,9 +773,9 @@
|
||||
}
|
||||
|
||||
.check.svg-icon {
|
||||
width: 12.3px;
|
||||
height: 9.8px;
|
||||
margin: 9px 8px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.challenge.broken {
|
||||
|
||||
@@ -66,10 +66,10 @@
|
||||
"androidFaqAnswer12": "World Bosses are special monsters that appear in the Tavern. All active users are automatically battling the Boss, and their tasks and Skills will damage the Boss as usual.\n\n You can also be in a normal Quest at the same time. Your tasks and Skills will count towards both the World Boss and the Boss/Collection Quest in your party.\n\n A World Boss will never hurt you or your account in any way. Instead, it has a Rage Bar that fills when users skip Dailies. If its Rage bar fills, it will attack one of the Non-Player Characters around the site and their image will change.\n\n You can read more about [past World Bosses](https://habitica.fandom.com/wiki/World_Bosses) on the wiki.",
|
||||
"webFaqAnswer12": "World Bosses are special monsters that appear in the Tavern. All active users are automatically battling the Boss, and their tasks and Skills will damage the Boss as usual. You can also be in a normal Quest at the same time. Your tasks and Skills will count towards both the World Boss and the Boss/Collection Quest in your party. A World Boss will never hurt you or your account in any way. Instead, it has a Rage Bar that fills when users skip Dailies. If its Rage bar fills, it will attack one of the Non-Player Characters around the site and their image will change. You can read more about [past World Bosses](https://habitica.fandom.com/wiki/World_Bosses) on the wiki.",
|
||||
|
||||
"faqQuestion13": "What is a Group Plan?",
|
||||
"webFaqAnswer13": "## How do Group Plans work?\n\nA [Group Plan](/group-plans) gives your Party or Guild access to a shared task board that’s similar to your personal task board! It’s a shared Habitica experience where tasks can be created and checked off by anyone in the group.\n\nThere are also features available like member roles, status view, and task assigning that give you a more controlled experience. [Visit our wiki](https://habitica.fandom.com/wiki/Group_Plans) to learn more about our Group Plans’ features!\n\n## Who benefits from a Group Plan?\n\nGroup Plans work best when you have a small team of people who want to collaborate together. We recommend 2-5 members.\n\nGroup Plans are great for families, whether it’s a parent and child or you and a partner. Shared goals, chores, or responsibilities are easy to keep track of on one board.\n\nGroup Plans can also be useful for teams of colleagues that have shared goals, or managers that want to introduce their employees to gamification.\n\n## Quick tips for using Groups\n\nHere are some quick tips to get you started with your new Group. We’ll provide more details in the following sections:\n\n* Make a member a manager to give them the ability to create and edit tasks\n* Leave tasks unassigned if anyone can complete it and it only needs done once\n* Assign a task to one person to make sure no one else can complete their task\n* Assign a task to multiple people if they all need to complete it\n* Toggle the ability to display shared tasks on your personal board to not miss anything\n* You get rewarded for the tasks you complete, even multi-assigned\n* Task completion rewards aren’t shared or split between Team members\n* Use task color on the team board to judge the average completion rate of tasks\n* Regularly review the tasks on your Team Board to make sure they are still relevant\n* Missing a Daily won’t damage you or your team, but the task will degrade in color\n\n## How can others in the group create tasks?\n\nOnly the group leader and managers can create tasks. If you’d like a group member to be able to create tasks, then you should promote them to be a manager by going to the Group Information tab, viewing the member list, and clicking the dot icon by their name.\n\n## How does assigning a task work?\n\nGroup Plans give you the unique ability to assign tasks to other group members. Assigning a task is great for delegating. If you assign a task to someone, then other members are prevented from completing it.\n\nYou can also assign a task to multiple people if it needs to be completed by more than one member. For example, if everyone has to brush their teeth, create a task and assign it to each group member. They will all be able to check it off and get their individual rewards for doing so. The main task will show as complete once everyone checks it off.\n\n## How do unassigned tasks work?\n\nUnassigned tasks can be completed by anyone in the group, so leave a task unassigned to allow any member to complete it. For example, taking out the trash. Whoever takes out the trash can check off the unassigned task and it will show as completed for everyone.\n\n## How does the synchronized day reset work?\n\nShared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the group leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in color to help visualize progress. We don’t want the shared experience to be a negative one!\n\n## How do I use my Group on the mobile apps?\n\nWhile the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android app by copying the tasks onto your personal task board. You can switch this preference on from Settings in the mobile apps or from the group task board on the browser version. Now open and assigned shared tasks will display on your personal task board across all platforms.\n\n## What’s the difference between a Group’s shared tasks and Challenges?\n\nGroup Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction.",
|
||||
|
||||
"iosFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
|
||||
"androidFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
|
||||
"webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help.",
|
||||
|
||||
"faqQuestion13": "What is a Group Plan?",
|
||||
"webFaqAnswer13": "## How do Group Plans work?\n\nA [Group Plan](/group-plans) gives your Party or Guild access to a shared task board that’s similar to your personal task board! It’s a shared Habitica experience where tasks can be created and checked off by anyone in the group.\n\nThere are also features available like member roles, status view, and task assigning that give you a more controlled experience. [Visit our wiki](https://habitica.fandom.com/wiki/Group_Plans) to learn more about our Group Plans’ features!\n\n## Who benefits from a Group Plan?\n\nGroup Plans work best when you have a small team of people who want to collaborate together. We recommend 2-5 members.\n\nGroup Plans are great for families, whether it’s a parent and child or you and a partner. Shared goals, chores, or responsibilities are easy to keep track of on one board.\n\nGroup Plans can also be useful for teams of colleagues that have shared goals, or managers that want to introduce their employees to gamification.\n\n## Quick tips for using Groups\n\nHere are some quick tips to get you started with your new Group. We’ll provide more details in the following sections:\n\n* Make a member a manager to give them the ability to create and edit tasks\n* Leave tasks unassigned if anyone can complete it and it only needs done once\n* Assign a task to one person to make sure no one else can complete their task\n* Assign a task to multiple people if they all need to complete it\n* Toggle the ability to display shared tasks on your personal board to not miss anything\n* You get rewarded for the tasks you complete, even multi-assigned\n* Task completion rewards aren’t shared or split between Team members\n* Use task color on the team board to judge the average completion rate of tasks\n* Regularly review the tasks on your Team Board to make sure they are still relevant\n* Missing a Daily won’t damage you or your team, but the task will degrade in color\n\n## How can others in the group create tasks?\n\nOnly the group leader and managers can create tasks. If you’d like a group member to be able to create tasks, then you should promote them to be a manager by going to the Group Information tab, viewing the member list, and clicking the dot icon by their name.\n\n## How does assigning a task work?\n\nGroup Plans give you the unique ability to assign tasks to other group members. Assigning a task is great for delegating. If you assign a task to someone, then other members are prevented from completing it.\n\nYou can also assign a task to multiple people if it needs to be completed by more than one member. For example, if everyone has to brush their teeth, create a task and assign it to each group member. They will all be able to check it off and get their individual rewards for doing so. The main task will show as complete once everyone checks it off.\n\n## How do unassigned tasks work?\n\nUnassigned tasks can be completed by anyone in the group, so leave a task unassigned to allow any member to complete it. For example, taking out the trash. Whoever takes out the trash can check off the unassigned task and it will show as completed for everyone.\n\n## How does the synchronized day reset work?\n\nShared tasks will reset at the same time for everyone to keep the shared task board in sync. This time is visible on the shared task board and is determined by the group leader’s day start time. Because shared tasks reset automatically, you will not get a chance to complete yesterday’s uncompleted shared Dailies when you check in the next morning.\n\nShared Dailies will not do damage if they are missed, however they will degrade in color to help visualize progress. We don’t want the shared experience to be a negative one!\n\n## How do I use my Group on the mobile apps?\n\nWhile the mobile apps don’t fully support all Group Plans functionality yet, you can still complete shared tasks from the iOS and Android app by copying the tasks onto your personal task board. You can switch this preference on from Settings in the mobile apps or from the group task board on the browser version. Now open and assigned shared tasks will display on your personal task board across all platforms.\n\n## What’s the difference between a Group’s shared tasks and Challenges?\n\nGroup Plan shared task boards are more dynamic than Challenges, in that they can constantly be updated and interacted with. Challenges are great if you have one set of tasks to send out to many people.\n\nGroup Plans are also a paid feature, while Challenges are available free to everyone.\n\nYou cannot assign specific tasks in Challenges, and Challenges do not have a shared day reset. In general, Challenges offer less control and direct interaction."
|
||||
"webFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), come ask in the [Habitica Help guild](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! We're happy to help."
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"mobileAndroid": "Android App",
|
||||
"mobileIOS": "iOS App",
|
||||
"oldNews": "News",
|
||||
"newsArchive": "News archive on Wikia (multilingual)",
|
||||
"newsArchive": "News archive on Fandom (multilingual)",
|
||||
"setNewPass": "Set New Password",
|
||||
"password": "Password",
|
||||
"playButton": "Play",
|
||||
|
||||
@@ -484,6 +484,8 @@
|
||||
"weaponMystery202209Notes": "This book will guide you through your journey into magic-making. Confers no benefit. September 2022 Subscriber Item.",
|
||||
"weaponMystery202211Text": "Electromancer Staff",
|
||||
"weaponMystery202211Notes": "Harness the awesome power of a lightning storm with this staff. Confers no benefit. November 2022 Subscriber Item.",
|
||||
"weaponMystery202212Text": "Glacial Wand",
|
||||
"weaponMystery202212Notes": "The glowing snowflake in this wand holds the power to warm hearts on even the coldest winter night! Confers no benefit. December 2022 Subscriber Item.",
|
||||
"weaponMystery301404Text": "Steampunk Cane",
|
||||
"weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.",
|
||||
|
||||
@@ -1218,6 +1220,8 @@
|
||||
"armorMystery202207Notes": "This armor will have you looking glamorous and gelatinous. Confers no benefit. July 2022 Subscriber Item.",
|
||||
"armorMystery202210Text": "Ominous Ophidian Armor",
|
||||
"armorMystery202210Notes": "Try slithering for a change, you may find it's quite an efficient mode of transportation! Confers no benefit. October 2022 Subscriber Item.",
|
||||
"armorMystery202212Text": "Glacial Dress",
|
||||
"armorMystery202212Notes": "The universe can be cold, but this charming dress will keep you cozy as you fly. Confers no benefit. December 2022 Subscriber Item.",
|
||||
"armorMystery301404Text": "Steampunk Suit",
|
||||
"armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.",
|
||||
"armorMystery301703Text": "Steampunk Peacock Gown",
|
||||
@@ -1972,7 +1976,6 @@
|
||||
"headMystery202210Notes": "This scaly hood will surely terrify your To-Do list into submission! Confers no benefit. October 2022 Subscriber Item.",
|
||||
"headMystery202211Text": "Electromancer Hat",
|
||||
"headMystery202211Notes": "Be careful with this powerful hat, its effect on admirers can be quite shocking! Confers no benefit. November 2022 Subscriber Item.",
|
||||
|
||||
"headMystery301404Text": "Fancy Top Hat",
|
||||
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
|
||||
"headMystery301405Text": "Basic Top Hat",
|
||||
@@ -2816,6 +2819,8 @@
|
||||
"headAccessoryMystery202203Notes": "Need an extra boost of speed? The tiny decorative wings on this circlet are more powerful than they look! Confers no benefit. March 2022 Subscriber Item.",
|
||||
"headAccessoryMystery202205Text": "Dusk-Winged Dragon Horns",
|
||||
"headAccessoryMystery202205Notes": "These dazzling horns are as bright as a desert sunset. Confers no benefit. May 2022 Subscriber Item.",
|
||||
"headAccessoryMystery202212Text": "Glacial Tiara",
|
||||
"headAccessoryMystery202212Notes": "Magnify your warmth and friendship to new heights with this ornate golden tiara. Confers no benefit. December 2022 Subscriber Item.",
|
||||
"headAccessoryMystery301405Text": "Headwear Goggles",
|
||||
"headAccessoryMystery301405Notes": "\"Goggles are for your eyes,\" they said. \"Nobody wants goggles that you can only wear on your head,\" they said. Hah! You sure showed them! Confers no benefit. August 3015 Subscriber Item.",
|
||||
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
"usernameIssueForbidden": "Usernames may not contain restricted words.",
|
||||
"usernameIssueLength": "Usernames must be between 1 and 20 characters.",
|
||||
"usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.",
|
||||
"passwordIssueLength": "Passwords must be between 8 and 64 characters.",
|
||||
"currentUsername": "Current username:",
|
||||
"displaynameIssueLength": "Display Names must be between 1 and 30 characters.",
|
||||
"bannedWordUsedInProfile": "Your Display Name or About text contained inappropriate language.",
|
||||
|
||||
@@ -144,6 +144,7 @@
|
||||
"mysterySet202209": "Magical Scholar Set",
|
||||
"mysterySet202210": "Ominous Ophidian Set",
|
||||
"mysterySet202211": "Electromancer Set",
|
||||
"mysterySet202212": "Glacial Guardian Set",
|
||||
"mysterySet301404": "Steampunk Standard Set",
|
||||
"mysterySet301405": "Steampunk Accessories Set",
|
||||
"mysterySet301703": "Peacock Steampunk Set",
|
||||
|
||||
@@ -30,6 +30,7 @@ export const GUILDS_PER_PAGE = 30; // number of guilds to return per page when u
|
||||
export const PARTY_LIMIT_MEMBERS = 29;
|
||||
|
||||
export const MINIMUM_PASSWORD_LENGTH = 8;
|
||||
export const MAXIMUM_PASSWORD_LENGTH = 64;
|
||||
|
||||
export const TRANSFORMATION_DEBUFFS_LIST = {
|
||||
snowball: 'salt',
|
||||
|
||||
@@ -10,22 +10,26 @@ const gemsPromo = {
|
||||
|
||||
export const EVENTS = {
|
||||
noEvent: {
|
||||
start: '2022-11-30T20:00-05:00',
|
||||
start: '2022-11-27T20:00-05:00',
|
||||
end: '2022-12-20T08:00-05:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
harvestFeast2022: {
|
||||
start: '2022-11-22T08:00-05:00',
|
||||
end: '2022-11-27T20:00-05:00',
|
||||
season: 'thanksgiving',
|
||||
npcImageSuffix: '_thanksgiving',
|
||||
},
|
||||
afterGala: {
|
||||
start: '2022-10-31T20:00-04:00',
|
||||
end: '2022-11-22T08:00-05:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
bundle202211: {
|
||||
start: '2022-11-15T08:00-05:00',
|
||||
end: '2022-11-30T20:00-05:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
afterGala: {
|
||||
start: '2022-10-31T20:00-04:00',
|
||||
end: '2022-11-15T08:00-05:00',
|
||||
season: 'normal',
|
||||
npcImageSuffix: '',
|
||||
},
|
||||
fall2022: {
|
||||
start: '2022-09-20T08:00-04:00',
|
||||
|
||||
@@ -59,6 +59,7 @@ const armor = {
|
||||
202204: { },
|
||||
202207: { },
|
||||
202210: { },
|
||||
202212: { },
|
||||
301404: { },
|
||||
301703: { },
|
||||
301704: { },
|
||||
@@ -222,6 +223,7 @@ const headAccessory = {
|
||||
202105: { },
|
||||
202109: { },
|
||||
202203: { },
|
||||
202212: { },
|
||||
202205: { },
|
||||
301405: { },
|
||||
};
|
||||
@@ -253,6 +255,7 @@ const weapon = {
|
||||
202111: { twoHanded: true },
|
||||
202211: { twoHanded: true },
|
||||
202201: { },
|
||||
202212: { },
|
||||
202209: { },
|
||||
301404: { },
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
MIN_SHORTNAME_SIZE_FOR_CHALLENGES,
|
||||
PARTY_LIMIT_MEMBERS,
|
||||
MINIMUM_PASSWORD_LENGTH,
|
||||
MAXIMUM_PASSWORD_LENGTH,
|
||||
SUPPORTED_SOCIAL_NETWORKS,
|
||||
TAVERN_ID,
|
||||
MAX_MESSAGE_LENGTH,
|
||||
@@ -119,6 +120,7 @@ api.constants = {
|
||||
CHAT_FLAG_FROM_MOD,
|
||||
CHAT_FLAG_FROM_SHADOW_MUTE,
|
||||
MINIMUM_PASSWORD_LENGTH,
|
||||
MAXIMUM_PASSWORD_LENGTH,
|
||||
MAX_MESSAGE_LENGTH,
|
||||
MAX_GIFT_MESSAGE_LENGTH,
|
||||
MAX_LEVEL_HARD_CAP,
|
||||
|
||||
@@ -419,6 +419,11 @@ shops.getSeasonalGearBySet = function getSeasonalGearBySet (
|
||||
const itemInfo = getItemInfo(null, currentSet ? 'marketGear' : 'gear', gear, officialPinnedItems, language);
|
||||
itemInfo.locked = currentSet && user.stats.class !== gear.specialClass;
|
||||
|
||||
// gear that has previously been owned should be repurchaseable with gold
|
||||
if (user.items.gear.owned[gear.key] !== undefined) {
|
||||
itemInfo.currency = 'gold';
|
||||
}
|
||||
|
||||
return itemInfo;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -276,7 +276,7 @@ api.exportUserAvatarPng = {
|
||||
/**
|
||||
* @api {get} /export/inbox.html Export user private messages as HTML document
|
||||
* @apiName ExportUserPrivateMessages
|
||||
* @apiDescription This HTML export feature is not currently working (https://github.com/HabitRPG/habitica/issues/9489).
|
||||
*
|
||||
* @apiGroup DataExport
|
||||
*
|
||||
* @apiSuccess {HTML} File An html page of the user's private messages.
|
||||
|
||||
@@ -100,8 +100,11 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
errorMessage: res.t('missingPassword'),
|
||||
equals: { options: [req.body.confirmPassword], errorMessage: res.t('passwordConfirmationMatch') },
|
||||
isLength: {
|
||||
options: { min: common.constants.MINIMUM_PASSWORD_LENGTH },
|
||||
errorMessage: res.t('minPasswordLength'),
|
||||
options: {
|
||||
min: common.constants.MINIMUM_PASSWORD_LENGTH,
|
||||
max: common.constants.MAXIMUM_PASSWORD_LENGTH,
|
||||
},
|
||||
errorMessage: res.t('passwordIssueLength'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -106,6 +106,10 @@ api.verifyGemPurchase = async function verifyGemPurchase (options) {
|
||||
};
|
||||
|
||||
api.subscribe = async function subscribe (sku, user, receipt, headers, nextPaymentProcessing) {
|
||||
if (user && user.isSubscribed()) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
|
||||
if (!sku) throw new BadRequest(shared.i18n.t('missingSubscriptionCode'));
|
||||
|
||||
let subCode;
|
||||
@@ -136,49 +140,33 @@ api.subscribe = async function subscribe (sku, user, receipt, headers, nextPayme
|
||||
throw new NotAuthorized(api.constants.RESPONSE_NO_ITEM_PURCHASED);
|
||||
}
|
||||
|
||||
let originalTransactionId;
|
||||
let newTransactionId;
|
||||
let transactionId;
|
||||
|
||||
for (const purchaseData of purchaseDataList) {
|
||||
const dateTerminated = new Date(Number(purchaseData.expirationDate));
|
||||
if (purchaseData.productId === sku && dateTerminated > new Date()) {
|
||||
originalTransactionId = purchaseData.originalTransactionId;
|
||||
newTransactionId = purchaseData.transactionId;
|
||||
transactionId = purchaseData.transactionId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalTransactionId) {
|
||||
let existingSub;
|
||||
if (user && user.isSubscribed()) {
|
||||
if (user.purchased.plan.customerId !== originalTransactionId) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
existingSub = shared.content.subscriptionBlocks[user.purchased.plan.planId];
|
||||
}
|
||||
if (transactionId) {
|
||||
const existingUser = await User.findOne({
|
||||
'purchased.plan.customerId': originalTransactionId,
|
||||
'purchased.plan.customerId': transactionId,
|
||||
}).exec();
|
||||
if (existingUser
|
||||
&& (originalTransactionId === newTransactionId || existingUser._id !== user._id)) {
|
||||
throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
}
|
||||
if (existingUser) throw new NotAuthorized(this.constants.RESPONSE_ALREADY_USED);
|
||||
|
||||
nextPaymentProcessing = nextPaymentProcessing || moment.utc().add({ days: 2 }); // eslint-disable-line max-len, no-param-reassign
|
||||
|
||||
const data = {
|
||||
await payments.createSubscription({
|
||||
user,
|
||||
customerId: originalTransactionId,
|
||||
customerId: transactionId,
|
||||
paymentMethod: this.constants.PAYMENT_METHOD_APPLE,
|
||||
sub,
|
||||
headers,
|
||||
nextPaymentProcessing,
|
||||
additionalData: receipt,
|
||||
};
|
||||
if (existingSub) {
|
||||
data.updatedFrom = existingSub;
|
||||
}
|
||||
await payments.createSubscription(data);
|
||||
});
|
||||
} else {
|
||||
throw new NotAuthorized(api.constants.RESPONSE_INVALID_RECEIPT);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { // eslint-disable-line import/no-cycle
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
TooManyRequests,
|
||||
} from '../errors';
|
||||
import shared from '../../../common';
|
||||
import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle
|
||||
@@ -75,15 +74,7 @@ async function prepareSubscriptionValues (data) {
|
||||
? data.gift.subscription.key
|
||||
: data.sub.key];
|
||||
const autoRenews = data.autoRenews !== undefined ? data.autoRenews : true;
|
||||
const updatedFrom = data.updatedFrom
|
||||
? shared.content.subscriptionBlocks[data.updatedFrom.key]
|
||||
: undefined;
|
||||
let months;
|
||||
if (updatedFrom && Number(updatedFrom.months) !== 1) {
|
||||
months = Math.max(0, Number(block.months) - Number(updatedFrom.months));
|
||||
} else {
|
||||
months = Number(block.months);
|
||||
}
|
||||
const months = Number(block.months);
|
||||
const today = new Date();
|
||||
let group;
|
||||
let groupId;
|
||||
@@ -92,12 +83,6 @@ async function prepareSubscriptionValues (data) {
|
||||
let emailType = 'subscription-begins';
|
||||
let recipientIsSubscribed = recipient.isSubscribed();
|
||||
|
||||
if (data.user && !data.gift && !data.groupId && data.customerId !== 'group-plan') {
|
||||
if (moment().diff(data.user.purchased.plan.dateUpdated, 'minutes') < 3) {
|
||||
throw new TooManyRequests('Subscription already processed, likely duplicate request');
|
||||
}
|
||||
}
|
||||
|
||||
// If we are buying a group subscription
|
||||
if (data.groupId) {
|
||||
const groupFields = basicGroupFields.concat(' purchased');
|
||||
|
||||
@@ -150,21 +150,10 @@ function _setUpNewUser (user) {
|
||||
user.items.quests.dustbunnies = 1;
|
||||
user.purchased.background.violet = true;
|
||||
user.preferences.background = 'violet';
|
||||
if (moment().isBefore('2022-03-15T20:00-04:00')) {
|
||||
user.items.gear.owned.head_special_piDay = true;
|
||||
user.items.gear.equipped.head = 'head_special_piDay';
|
||||
user.items.gear.owned.shield_special_piDay = true;
|
||||
user.items.gear.equipped.shield = 'shield_special_piDay';
|
||||
user.items.food.Pie_Skeleton = 1;
|
||||
user.items.food.Pie_Base = 1;
|
||||
user.items.food.Pie_CottonCandyBlue = 1;
|
||||
user.items.food.Pie_CottonCandyPink = 1;
|
||||
user.items.food.Pie_Shade = 1;
|
||||
user.items.food.Pie_White = 1;
|
||||
user.items.food.Pie_Golden = 1;
|
||||
user.items.food.Pie_Zombie = 1;
|
||||
user.items.food.Pie_Desert = 1;
|
||||
user.items.food.Pie_Red = 1;
|
||||
if (moment().isBetween('2022-11-22T08:00-05:00', '2022-11-27T20:00-05:00')) {
|
||||
user.migration = '20221122_harvest_feast';
|
||||
user.items.pets['Turkey-Base'] = 5;
|
||||
user.items.currentPet = 'Turkey-Base';
|
||||
}
|
||||
|
||||
user.markModified('items achievements');
|
||||
|
||||
Reference in New Issue
Block a user