mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-18 06:57:58 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ed8b4d633 | |||
| 330b8576ea | |||
| e1ad86bbf7 | |||
| 5ff2da17de | |||
| e8e4ff8687 |
@@ -21,4 +21,3 @@ services:
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
retries: 30
|
||||
|
||||
+1
-1
Submodule habitica-images updated: b7367f328a...32a4678c6b
Generated
+281
-169
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.47.9",
|
||||
"version": "5.47.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.47.9",
|
||||
"version": "5.47.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -57,7 +57,7 @@
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.23.0",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.1",
|
||||
"nan": "^2.25.0",
|
||||
"nconf": "^0.12.1",
|
||||
@@ -81,6 +81,7 @@
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
@@ -2623,19 +2624,6 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/trace-agent/node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@google-cloud/trace-agent/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -3064,9 +3052,9 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
|
||||
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz",
|
||||
"integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
@@ -3277,6 +3265,11 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.25",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
|
||||
"integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ=="
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
@@ -3939,6 +3932,17 @@
|
||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz",
|
||||
"integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@@ -6312,7 +6316,6 @@
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
|
||||
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.3.5",
|
||||
"safe-buffer": "^5.1.1"
|
||||
@@ -6322,15 +6325,13 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bl/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -6346,7 +6347,6 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -7751,6 +7751,11 @@
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
|
||||
"integrity": "sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw=="
|
||||
},
|
||||
"node_modules/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -11224,6 +11229,18 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -11967,19 +11984,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
@@ -12512,6 +12516,20 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gzip-size": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
|
||||
"integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==",
|
||||
"dependencies": {
|
||||
"duplexer": "^0.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/habitica-markdown": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
|
||||
@@ -12801,7 +12819,6 @@
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
|
||||
"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
@@ -12854,8 +12871,7 @@
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
@@ -15541,14 +15557,128 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
|
||||
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
|
||||
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bl": "^2.2.1",
|
||||
"bson": "^1.1.4",
|
||||
"denque": "^1.4.1",
|
||||
"optional-require": "^1.1.8",
|
||||
"safe-buffer": "^5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"saslprep": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"aws4": {
|
||||
"optional": true
|
||||
},
|
||||
"bson-ext": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-extjson": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.3.0",
|
||||
"bson": "^6.10.4",
|
||||
"mongodb-connection-string-url": "^3.0.2"
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
|
||||
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
|
||||
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb/node_modules/bson": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
|
||||
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.6.19"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.9.7",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
|
||||
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.1",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.12.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "17.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/mongodb": {
|
||||
"version": "6.12.0",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
|
||||
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@mongodb-js/saslprep": "^1.1.9",
|
||||
"bson": "^6.10.1",
|
||||
"mongodb-connection-string-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
@@ -15559,7 +15689,7 @@
|
||||
"gcp-metadata": "^5.2.0",
|
||||
"kerberos": "^2.0.1",
|
||||
"mongodb-client-encryption": ">=6.0.0 <7",
|
||||
"snappy": "^7.3.2",
|
||||
"snappy": "^7.2.2",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -15586,72 +15716,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
|
||||
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/whatwg-url": "^11.0.2",
|
||||
"whatwg-url": "^14.1.0 || ^13.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose": {
|
||||
"version": "8.23.0",
|
||||
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz",
|
||||
"integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bson": "^6.10.4",
|
||||
"kareem": "2.6.3",
|
||||
"mongodb": "~6.20.0",
|
||||
"mpath": "0.9.0",
|
||||
"mquery": "5.0.0",
|
||||
"ms": "2.1.3",
|
||||
"sift": "17.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -15711,56 +15775,6 @@
|
||||
"integrity": "sha512-jSTz73B/+pGTTvhu5Ym8xsG6+QqaWab53UXnXdNNlTijTdLvcHABCLJXudQiJxob5N1Mzr5EOSx5ziwn2sihPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/monk/node_modules/bson": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
|
||||
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.6.19"
|
||||
}
|
||||
},
|
||||
"node_modules/monk/node_modules/mongodb": {
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
|
||||
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"bl": "^2.2.1",
|
||||
"bson": "^1.1.4",
|
||||
"denque": "^1.4.1",
|
||||
"optional-require": "^1.1.8",
|
||||
"safe-buffer": "^5.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"saslprep": "^1.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"aws4": {
|
||||
"optional": true
|
||||
},
|
||||
"bson-ext": {
|
||||
"optional": true
|
||||
},
|
||||
"kerberos": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-client-encryption": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb-extjson": {
|
||||
"optional": true
|
||||
},
|
||||
"snappy": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/morgan": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
|
||||
@@ -15838,6 +15852,14 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz",
|
||||
"integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -17106,12 +17128,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
"integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
|
||||
"bin": {
|
||||
"opener": "bin/opener-bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/optional-require": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.10.tgz",
|
||||
"integrity": "sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==",
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
|
||||
"integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"require-at": "^1.0.6"
|
||||
},
|
||||
@@ -18593,7 +18622,6 @@
|
||||
"resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz",
|
||||
"integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
@@ -18892,7 +18920,6 @@
|
||||
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
|
||||
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
@@ -19319,6 +19346,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||
"integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/slash": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
@@ -20809,6 +20849,14 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/touch": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
|
||||
@@ -21822,6 +21870,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer": {
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz",
|
||||
"integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==",
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "0.5.7",
|
||||
"acorn": "^8.0.4",
|
||||
"acorn-walk": "^8.0.0",
|
||||
"commander": "^7.2.0",
|
||||
"debounce": "^1.2.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"html-escaper": "^2.0.2",
|
||||
"opener": "^1.5.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"sirv": "^2.0.3",
|
||||
"ws": "^7.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"webpack-bundle-analyzer": "lib/bin/analyzer.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-cli": {
|
||||
"version": "4.10.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
|
||||
@@ -22187,6 +22279,26 @@
|
||||
"typedarray-to-buffer": "^3.1.5"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-crypto": {
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz",
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.47.9",
|
||||
"version": "5.47.0",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -52,7 +52,7 @@
|
||||
"micromustache": "^8.0.3",
|
||||
"moment": "^2.29.4",
|
||||
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
|
||||
"mongoose": "^8.23.0",
|
||||
"mongoose": "^8.9.5",
|
||||
"morgan": "^1.10.1",
|
||||
"nan": "^2.25.0",
|
||||
"nconf": "^0.12.1",
|
||||
@@ -76,6 +76,7 @@
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"winston": "^3.10.0",
|
||||
"winston-loggly-bulk": "^3.3.0",
|
||||
"xml2js": "^0.6.2"
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
/* eslint-disable camelcase */
|
||||
import nconf from 'nconf';
|
||||
import Amplitude from 'amplitude';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analyticsService', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('#getServiceByEnvironment', () => {
|
||||
it('returns mock methods when not in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment())
|
||||
.to.equal(analyticsService.mockAnalyticsService);
|
||||
});
|
||||
|
||||
it('returns real methods when in production', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
|
||||
.to.equal(analyticsService.track);
|
||||
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
|
||||
.to.equal(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#track', () => {
|
||||
let eventType; let
|
||||
data;
|
||||
|
||||
beforeEach(() => {
|
||||
eventType = 'Cron';
|
||||
data = {
|
||||
category: 'behavior',
|
||||
uuid: 'unique-user-id',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about event', () => analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
category: 'behavior',
|
||||
resting: true,
|
||||
cronCount: 5,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends english item name for gear if itemKey is provided', () => {
|
||||
data.itemKey = 'headAccessory_special_foxEars';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Fox Ears',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for egg if itemKey is provided', () => {
|
||||
data.itemKey = 'Wolf';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Wolf Egg',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for food if itemKey is provided', () => {
|
||||
data.itemKey = 'Cake_Skeleton';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Bare Bones Cake',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for hatching potion if itemKey is provided', () => {
|
||||
data.itemKey = 'Golden';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Golden Hatching Potion',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for quest if itemKey is provided', () => {
|
||||
data.itemKey = 'atom1';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends english item name for purchased spell if itemKey is provided', () => {
|
||||
data.itemKey = 'seafoam';
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
itemKey: data.itemKey,
|
||||
itemName: 'Seafoam',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
balance: 12,
|
||||
loginIncentives: 1,
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.track(eventType, data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
balance: 12,
|
||||
balanceGemAmount: 48,
|
||||
loginIncentives: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trackPurchase', () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
uuid: 'user-id',
|
||||
sku: 'paypal-checkout',
|
||||
paymentMethod: 'PayPal',
|
||||
itemPurchased: 'Gems',
|
||||
purchaseValue: 8,
|
||||
purchaseType: 'checkout',
|
||||
gift: false,
|
||||
quantity: 1,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
user: {
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
context('Amplitude', () => {
|
||||
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledOnce;
|
||||
}));
|
||||
|
||||
it('uses a dummy user id if none is provided', () => {
|
||||
delete data.uuid;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_id: 'no-user-id-was-provided',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('platform', () => {
|
||||
it('logs web platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-web' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Web',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs iOS platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-ios' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'iOS',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs Android platform', () => {
|
||||
data.headers = { 'x-client': 'habitica-android' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Android',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs 3rd Party platform', () => {
|
||||
data.headers = { 'x-client': 'some-third-party' };
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: '3rd Party',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('logs unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
platform: 'Unknown',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Operating System', () => {
|
||||
it('sets default', () => {
|
||||
data.headers = {
|
||||
'x-client': 'third-party',
|
||||
'user-agent': 'foo',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Other',
|
||||
os_version: '0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets iOS', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-ios',
|
||||
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'iOS',
|
||||
os_version: '9.3.0',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Android', () => {
|
||||
data.headers = {
|
||||
'x-client': 'habitica-android',
|
||||
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
|
||||
};
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: 'Android',
|
||||
os_version: '4.0.4',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sets Unknown if headers are not passed in', () => {
|
||||
delete data.headers;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
os_name: undefined,
|
||||
os_version: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('sends details about purchase', () => analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
event_properties: {
|
||||
gift: false,
|
||||
itemPurchased: 'Gems',
|
||||
paymentMethod: 'PayPal',
|
||||
purchaseType: 'checkout',
|
||||
quantity: 1,
|
||||
sku: 'paypal-checkout',
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
||||
it('sends user data if provided', () => {
|
||||
const stats = {
|
||||
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
|
||||
};
|
||||
const user = {
|
||||
stats,
|
||||
contributor: { level: 1 },
|
||||
purchased: { plan: { planId: 'foo-plan' } },
|
||||
flags: { tour: { intro: -2 } },
|
||||
habits: [{ _id: 'habit' }],
|
||||
dailys: [{ _id: 'daily' }],
|
||||
todos: [{ _id: 'todo' }],
|
||||
rewards: [{ _id: 'reward' }],
|
||||
preferences: {
|
||||
analyticsConsent: true,
|
||||
},
|
||||
};
|
||||
|
||||
data.user = user;
|
||||
|
||||
return analyticsService.trackPurchase(data)
|
||||
.then(() => {
|
||||
expect(Amplitude.prototype.track).to.be.calledWithMatch({
|
||||
user_properties: {
|
||||
Class: 'wizard',
|
||||
Experience: 5,
|
||||
Gold: 23,
|
||||
Health: 10,
|
||||
Level: 4,
|
||||
Mana: 30,
|
||||
tutorialComplete: true,
|
||||
'Number Of Tasks': {
|
||||
habits: 1,
|
||||
dailys: 1,
|
||||
todos: 1,
|
||||
rewards: 1,
|
||||
},
|
||||
contributorLevel: 1,
|
||||
subscription: 'foo-plan',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mockAnalyticsService', () => {
|
||||
it('has stubbed track method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
|
||||
});
|
||||
|
||||
it('has stubbed trackPurchase method', () => {
|
||||
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
|
||||
});
|
||||
});
|
||||
});
|
||||
+136
-116
@@ -13,6 +13,7 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import * as Tasks from '../../../../website/server/models/task';
|
||||
import common from '../../../../website/common';
|
||||
import * as analytics from '../../../../website/server/libs/analyticsService';
|
||||
import { model as Group } from '../../../../website/server/models/group';
|
||||
|
||||
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
|
||||
@@ -40,17 +41,20 @@ describe('cron', async () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.spy(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (clock !== null) clock.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
|
||||
const timezoneUtcOffsetFromUserPrefs = -1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
|
||||
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
|
||||
});
|
||||
|
||||
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
|
||||
@@ -59,7 +63,7 @@ describe('cron', async () => {
|
||||
it('resets user.items.lastDrop.count', async () => {
|
||||
user.items.lastDrop.count = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.items.lastDrop.count).to.equal(0);
|
||||
});
|
||||
@@ -67,11 +71,26 @@ describe('cron', async () => {
|
||||
it('increments user cron count', async () => {
|
||||
const cronCountBefore = user.flags.cronCount;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
|
||||
});
|
||||
|
||||
it('calls analytics', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('calls analytics when user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(analytics.track.callCount).to.equal(1);
|
||||
});
|
||||
|
||||
describe('end of the month perks', async () => {
|
||||
beforeEach(async () => {
|
||||
user.purchased.plan.customerId = 'subscribedId';
|
||||
@@ -82,7 +101,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-12-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -93,7 +112,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = new Date('2018-11-11');
|
||||
clock = sinon.useFakeTimers(new Date('2019-01-29'));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
|
||||
@@ -103,7 +122,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -112,7 +131,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
user.purchased.plan.dateUpdated = undefined;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -123,7 +142,7 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
@@ -131,7 +150,7 @@ describe('cron', async () => {
|
||||
it('resets plan.dateUpdated on a new month', async () => {
|
||||
const currentMonth = moment().startOf('month');
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
|
||||
});
|
||||
@@ -139,7 +158,7 @@ describe('cron', async () => {
|
||||
it('increments plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(1);
|
||||
});
|
||||
@@ -147,7 +166,7 @@ describe('cron', async () => {
|
||||
it('increments plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(1);
|
||||
});
|
||||
@@ -156,7 +175,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(2);
|
||||
});
|
||||
@@ -165,7 +184,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(3);
|
||||
});
|
||||
@@ -177,7 +196,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
|
||||
@@ -187,7 +206,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -195,7 +214,7 @@ describe('cron', async () => {
|
||||
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.exist;
|
||||
});
|
||||
@@ -206,7 +225,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
@@ -245,7 +264,7 @@ describe('cron', async () => {
|
||||
// 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,
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -257,7 +276,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user1, tasksByType, daysMissed,
|
||||
user: user1, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user1.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -292,7 +311,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed,
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -304,7 +323,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3, tasksByType, daysMissed,
|
||||
user: user3, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -339,7 +358,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user6, tasksByType, daysMissed,
|
||||
user: user6, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user6.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -372,7 +391,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed,
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
|
||||
@@ -384,7 +403,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user12, tasksByType, daysMissed,
|
||||
user: user12, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user12.purchased.plan.consecutive.count).to.equal(10);
|
||||
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
|
||||
@@ -420,7 +439,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
|
||||
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
|
||||
@@ -433,7 +452,7 @@ describe('cron', async () => {
|
||||
.add(2, 'days')
|
||||
.toDate());
|
||||
await cron({
|
||||
user: user3g, tasksByType, daysMissed,
|
||||
user: user3g, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
// subscription has been erased by now
|
||||
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
|
||||
@@ -452,7 +471,7 @@ describe('cron', async () => {
|
||||
it('resets plan.gemsBought on a new month', async () => {
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(0);
|
||||
});
|
||||
@@ -463,14 +482,14 @@ describe('cron', async () => {
|
||||
|
||||
user.purchased.plan.gemsBought = 10;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.gemsBought).to.equal(10);
|
||||
});
|
||||
|
||||
it('does not reset plan.dateUpdated on a new month', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.dateUpdated).to.be.empty;
|
||||
});
|
||||
@@ -478,7 +497,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.count', async () => {
|
||||
user.purchased.plan.consecutive.count = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.count).to.equal(0);
|
||||
});
|
||||
@@ -486,7 +505,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.cumulativeCount', async () => {
|
||||
user.purchased.plan.cumulativeCount = 0;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.cumulativeCount).to.equal(0);
|
||||
});
|
||||
@@ -494,7 +513,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
|
||||
});
|
||||
@@ -502,7 +521,7 @@ describe('cron', async () => {
|
||||
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
|
||||
});
|
||||
@@ -511,7 +530,7 @@ describe('cron', async () => {
|
||||
user.purchased.plan.consecutive.gemCapExtra = 26;
|
||||
user.purchased.plan.consecutive.count = 5;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
|
||||
});
|
||||
@@ -519,7 +538,7 @@ describe('cron', async () => {
|
||||
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
|
||||
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.purchased.plan.customerId).to.not.exist;
|
||||
});
|
||||
@@ -545,7 +564,7 @@ describe('cron', async () => {
|
||||
it('should make uncompleted todos redder', async () => {
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
|
||||
});
|
||||
@@ -554,7 +573,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
const valueBefore = tasksByType.todos[0].value;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.todos[0].value).to.equal(valueBefore);
|
||||
});
|
||||
@@ -563,7 +582,7 @@ describe('cron', async () => {
|
||||
tasksByType.todos[0].completed = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.history.todos).to.be.lengthOf(1);
|
||||
@@ -589,7 +608,7 @@ describe('cron', async () => {
|
||||
expect(user.tasksOrder.todos).to.be.lengthOf(3);
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
|
||||
@@ -616,7 +635,7 @@ describe('cron', async () => {
|
||||
const original = user.tasksOrder.todos; // Preserve the original order
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
let listsAreEqual = true;
|
||||
@@ -656,7 +675,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.be.false;
|
||||
});
|
||||
@@ -667,7 +686,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].isDue).to.exist;
|
||||
});
|
||||
@@ -677,14 +696,14 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].everyX = 5;
|
||||
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
|
||||
});
|
||||
|
||||
it('should add history', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
|
||||
});
|
||||
@@ -692,7 +711,7 @@ describe('cron', async () => {
|
||||
it('should set tasks completed to false', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -701,7 +720,7 @@ describe('cron', async () => {
|
||||
user.preferences.sleep = true;
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].completed).to.be.false;
|
||||
});
|
||||
@@ -710,7 +729,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -720,7 +739,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].completed = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -730,7 +749,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
|
||||
});
|
||||
@@ -740,7 +759,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||
});
|
||||
@@ -751,7 +770,7 @@ describe('cron', async () => {
|
||||
const hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
});
|
||||
@@ -765,7 +784,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -778,7 +797,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(hpBefore);
|
||||
@@ -789,7 +808,7 @@ describe('cron', async () => {
|
||||
let hpBefore = user.stats.hp;
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -797,7 +816,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
|
||||
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
|
||||
|
||||
@@ -810,7 +829,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(-1);
|
||||
@@ -822,7 +841,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
|
||||
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(progress.down).to.equal(0);
|
||||
@@ -843,7 +862,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[1].frequency = 'daily';
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.hp).to.equal(48);
|
||||
@@ -867,7 +886,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -878,7 +897,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].up = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.be.lessThan(1);
|
||||
@@ -890,7 +909,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].down = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].value).to.equal(1);
|
||||
@@ -909,7 +928,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -922,7 +941,7 @@ describe('cron', async () => {
|
||||
tasksByType.habits[0].counterDown = 1;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -936,7 +955,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -945,7 +964,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 8;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -969,7 +988,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -983,7 +1002,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset after user CDS
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1007,7 +1026,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1017,7 +1036,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1041,7 +1060,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1065,7 +1084,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1079,7 +1098,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1088,7 +1107,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 32;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1113,7 +1132,7 @@ describe('cron', async () => {
|
||||
|
||||
// should reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1137,7 +1156,7 @@ describe('cron', async () => {
|
||||
|
||||
// should not reset
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(1);
|
||||
@@ -1147,7 +1166,7 @@ describe('cron', async () => {
|
||||
// should reset
|
||||
daysMissed = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(tasksByType.habits[0].counterUp).to.equal(0);
|
||||
@@ -1180,7 +1199,7 @@ describe('cron', async () => {
|
||||
user.stats.lvl = 2;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.history.exp).to.have.lengthOf(1);
|
||||
@@ -1193,7 +1212,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = true;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(1);
|
||||
@@ -1205,7 +1224,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].isDue = false;
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.achievements.perfect).to.equal(0);
|
||||
@@ -1219,7 +1238,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1237,7 +1256,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1261,7 +1280,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1288,7 +1307,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1314,7 +1333,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1341,7 +1360,7 @@ describe('cron', async () => {
|
||||
};
|
||||
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.equal(0);
|
||||
@@ -1362,7 +1381,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1382,7 +1401,7 @@ describe('cron', async () => {
|
||||
const previousBuffs = user.stats.buffs.toObject();
|
||||
|
||||
cronOverride({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
|
||||
@@ -1401,7 +1420,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.mp).to.be.greaterThan(mpBefore);
|
||||
|
||||
@@ -1417,7 +1436,7 @@ describe('cron', async () => {
|
||||
tasksByType.dailys[0].completed = true;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(mpBefore);
|
||||
|
||||
@@ -1430,7 +1449,7 @@ describe('cron', async () => {
|
||||
user.stats.mp = 120;
|
||||
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
|
||||
|
||||
@@ -1463,7 +1482,7 @@ describe('cron', async () => {
|
||||
|
||||
it('resets user progress', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.party.quest.progress.up).to.equal(0);
|
||||
expect(user.party.quest.progress.down).to.equal(0);
|
||||
@@ -1472,7 +1491,7 @@ describe('cron', async () => {
|
||||
|
||||
it('applies the user progress', async () => {
|
||||
const progress = await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(progress.down).to.equal(-1);
|
||||
});
|
||||
@@ -1510,19 +1529,19 @@ describe('cron', async () => {
|
||||
describe('login incentives', async () => {
|
||||
it('increments incentive counter each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
user.lastCron = moment(new Date()).subtract({ days: 1 });
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
});
|
||||
|
||||
it('pushes a notification of the day\'s incentive each cron', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.notifications.length).to.eql(1);
|
||||
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
|
||||
@@ -1530,13 +1549,13 @@ describe('cron', async () => {
|
||||
|
||||
it('replaces previous notifications', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
|
||||
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
|
||||
@@ -1547,7 +1566,7 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
|
||||
daysMissed = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
@@ -1555,14 +1574,14 @@ describe('cron', async () => {
|
||||
it('increments loginIncentives by 1 even if user is sleeping', async () => {
|
||||
user.preferences.sleep = true;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
});
|
||||
|
||||
it('awards user bard robes if login incentive is 1', async () => {
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(1);
|
||||
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
|
||||
@@ -1572,7 +1591,7 @@ describe('cron', async () => {
|
||||
it('awards user incentive backgrounds if login incentive is 2', async () => {
|
||||
user.loginIncentives = 1;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(2);
|
||||
expect(user.purchased.background.blue).to.eql(true);
|
||||
@@ -1586,7 +1605,7 @@ describe('cron', async () => {
|
||||
it('awards user Bard Hat if login incentive is 3', async () => {
|
||||
user.loginIncentives = 2;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(3);
|
||||
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
|
||||
@@ -1596,7 +1615,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
|
||||
user.loginIncentives = 3;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(4);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1606,7 +1625,7 @@ describe('cron', async () => {
|
||||
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
|
||||
user.loginIncentives = 4;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(5);
|
||||
|
||||
@@ -1620,7 +1639,7 @@ describe('cron', async () => {
|
||||
it('awards user moon quest if login incentive is 7', async () => {
|
||||
user.loginIncentives = 6;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(7);
|
||||
expect(user.items.quests.moon1).to.eql(1);
|
||||
@@ -1630,7 +1649,7 @@ describe('cron', async () => {
|
||||
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
|
||||
user.loginIncentives = 9;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(10);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1640,7 +1659,7 @@ describe('cron', async () => {
|
||||
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
|
||||
user.loginIncentives = 13;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(14);
|
||||
|
||||
@@ -1654,7 +1673,7 @@ describe('cron', async () => {
|
||||
it('awards user a bard instrument if login incentive is 18', async () => {
|
||||
user.loginIncentives = 17;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(18);
|
||||
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
|
||||
@@ -1664,7 +1683,7 @@ describe('cron', async () => {
|
||||
it('awards user second moon quest if login incentive is 22', async () => {
|
||||
user.loginIncentives = 21;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(22);
|
||||
expect(user.items.quests.moon2).to.eql(1);
|
||||
@@ -1674,7 +1693,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
|
||||
user.loginIncentives = 25;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(26);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1684,7 +1703,7 @@ describe('cron', async () => {
|
||||
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
|
||||
user.loginIncentives = 29;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(30);
|
||||
|
||||
@@ -1699,7 +1718,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
|
||||
user.loginIncentives = 34;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(35);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1709,7 +1728,7 @@ describe('cron', async () => {
|
||||
it('awards user the third moon quest if login incentive is 40', async () => {
|
||||
user.loginIncentives = 39;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(40);
|
||||
expect(user.items.quests.moon3).to.eql(1);
|
||||
@@ -1719,7 +1738,7 @@ describe('cron', async () => {
|
||||
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
|
||||
user.loginIncentives = 44;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(45);
|
||||
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
|
||||
@@ -1729,7 +1748,7 @@ describe('cron', async () => {
|
||||
it('awards user a saddle if login incentive is 50', async () => {
|
||||
user.loginIncentives = 49;
|
||||
await cron({
|
||||
user, tasksByType, daysMissed,
|
||||
user, tasksByType, daysMissed, analytics,
|
||||
});
|
||||
expect(user.loginIncentives).to.eql(50);
|
||||
expect(user.items.food.Saddle).to.eql(1);
|
||||
@@ -1747,6 +1766,7 @@ describe('cron wrapper', () => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
user = await res.locals.user.save();
|
||||
res.analytics = analytics;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { model as User } from '../../../../website/server/models/user';
|
||||
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
|
||||
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('localAnalytics', () => {
|
||||
let user;
|
||||
let localAnalytics;
|
||||
before(() => {
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
|
||||
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
|
||||
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
username: 'username',
|
||||
email: 'email@example.com',
|
||||
},
|
||||
},
|
||||
registeredThrough: 'habitica-web',
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackRegistrationEvent', () => {
|
||||
afterEach(async () => {
|
||||
await RegistrationEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a registration event when a user registers', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000001';
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(1);
|
||||
expect(registrationEvents[0]).to.have.property('userId', user._id);
|
||||
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
|
||||
});
|
||||
|
||||
it('saves the correct data to the database', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000002';
|
||||
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
|
||||
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
});
|
||||
|
||||
describe('trackSubscriptionEvent', () => {
|
||||
afterEach(async () => {
|
||||
await SubscriptionEventModel.deleteMany({});
|
||||
});
|
||||
|
||||
it('creates a subscription event when a user subscribes', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000003';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'subscribed',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_123',
|
||||
planId: 'plan_123',
|
||||
});
|
||||
|
||||
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
|
||||
expect(subscriptionEvents).to.have.lengthOf(1);
|
||||
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
|
||||
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
|
||||
});
|
||||
|
||||
it('creates a subscription event with cancellation reason when a user cancels', async () => {
|
||||
user._id = '00000000-0000-0000-0000-000000000004';
|
||||
await localAnalytics.trackSubscriptionEvent({
|
||||
eventType: 'cancelled',
|
||||
user,
|
||||
paymentMethod: 'stripe',
|
||||
customerId: 'cus_456',
|
||||
planId: 'plan_456',
|
||||
cancellationReason: 'No longer needed',
|
||||
});
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
|
||||
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
|
||||
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import moment from 'moment';
|
||||
import * as sender from '../../../../../website/server/libs/email';
|
||||
import common from '../../../../../website/common';
|
||||
import api from '../../../../../website/server/libs/payments/payments';
|
||||
import * as analytics from '../../../../../website/server/libs/analyticsService';
|
||||
import * as notifications from '../../../../../website/server/libs/pushNotifications';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { translate as t } from '../../../../helpers/api-integration/v3';
|
||||
@@ -12,7 +13,6 @@ import {
|
||||
import * as worldState from '../../../../../website/server/libs/worldState';
|
||||
import { TransactionModel } from '../../../../../website/server/models/transaction';
|
||||
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
|
||||
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
|
||||
|
||||
describe('payments/index', () => {
|
||||
let user;
|
||||
@@ -36,6 +36,8 @@ describe('payments/index', () => {
|
||||
|
||||
sandbox.stub(sender, 'sendTxn');
|
||||
sandbox.stub(user, 'sendMessage');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
|
||||
sandbox.stub(analytics.mockAnalyticsService, 'track');
|
||||
sandbox.stub(notifications, 'sendNotification');
|
||||
|
||||
data = {
|
||||
@@ -95,16 +97,6 @@ describe('payments/index', () => {
|
||||
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
|
||||
expect(subscriptionEvent).to.have.property('userId', recipient._id);
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('adds extra months to an existing subscription', async () => {
|
||||
recipient.purchased.plan = plan;
|
||||
|
||||
@@ -306,6 +298,28 @@ describe('payments/index', () => {
|
||||
expect(notifications.sendNotification).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('tracks subscription purchase as gift', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: true,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('No Active Promotion', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(worldState, 'getCurrentEventList').returns([]);
|
||||
@@ -441,16 +455,6 @@ describe('payments/index', () => {
|
||||
expect(user.purchased.plan.dateCreated).to.exist;
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.createSubscription(data);
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('ipAddress');
|
||||
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('sets plan.dateCreated if it did not previously exist', async () => {
|
||||
expect(user.purchased.plan.dateCreated).to.not.exist;
|
||||
|
||||
@@ -539,24 +543,29 @@ describe('payments/index', () => {
|
||||
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
it('tracks subscription purchase', async () => {
|
||||
await api.createSubscription(data);
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_6mo';
|
||||
data.updatedFrom = { key: 'basic_earned' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
|
||||
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
|
||||
uuid: user._id,
|
||||
groupId: undefined,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: 'payment method-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: false,
|
||||
purchaseValue: 15,
|
||||
firstPurchase: true,
|
||||
headers: {
|
||||
'x-client': 'habitica-web',
|
||||
'user-agent': '',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
context('Upgrades subscription', () => {
|
||||
it('from basic_earned to basic_6mo', async () => {
|
||||
data.sub.key = 'basic_earned';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -599,23 +608,6 @@ describe('payments/index', () => {
|
||||
});
|
||||
|
||||
context('Downgrades subscription', () => {
|
||||
it('tracks subscription events', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
|
||||
await api.createSubscription(data);
|
||||
|
||||
data.sub.key = 'basic_earned';
|
||||
data.updatedFrom = { key: 'basic_6mo' };
|
||||
await api.createSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
|
||||
});
|
||||
|
||||
it('from basic_6mo to basic_earned', async () => {
|
||||
data.sub.key = 'basic_6mo';
|
||||
expect(user.purchased.plan.planId).to.not.exist;
|
||||
@@ -1144,15 +1136,6 @@ describe('payments/index', () => {
|
||||
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
|
||||
});
|
||||
|
||||
it('tracks subscription events', async () => {
|
||||
await api.cancelSubscription(data);
|
||||
|
||||
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
|
||||
expect(subscriptionEvent).to.exist;
|
||||
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
|
||||
expect(subscriptionEvent).to.have.property('userId', user._id);
|
||||
});
|
||||
|
||||
it('adds extraMonths to dateTerminated value', async () => {
|
||||
user.purchased.plan.extraMonths = 2;
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/* eslint-disable global-require */
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
} from '../../../helpers/api-unit.helper';
|
||||
import * as analyticsService from '../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('analytics middleware', () => {
|
||||
let res; let req; let
|
||||
next;
|
||||
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
|
||||
|
||||
beforeEach(() => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
});
|
||||
|
||||
it('attaches analytics object to res', () => {
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics).to.exist;
|
||||
});
|
||||
|
||||
it('attaches stubbed methods for non-prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
|
||||
});
|
||||
|
||||
it('attaches real methods for prod environments', () => {
|
||||
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
|
||||
|
||||
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
|
||||
|
||||
attachAnalytics(req, res, next);
|
||||
|
||||
expect(res.analytics.track).to.eql(analyticsService.track);
|
||||
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
|
||||
});
|
||||
});
|
||||
@@ -32,8 +32,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('is disabled when the env var is not defined', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -44,8 +43,7 @@ describe('rateLimiter middleware', () => {
|
||||
|
||||
it('is disabled when the env var is an not "true"', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false');
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -57,8 +55,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('does not throw when there are available points', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -80,8 +77,7 @@ describe('rateLimiter middleware', () => {
|
||||
sandbox.stub(RateLimiterMemory.prototype, 'consume')
|
||||
.returns(Promise.reject(new Error('Unknown error.')));
|
||||
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
expect(next).to.have.been.calledOnce;
|
||||
@@ -96,8 +92,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'abc';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -112,8 +107,7 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = 'das';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -130,8 +124,7 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -147,8 +140,7 @@ describe('rateLimiter middleware', () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.query.liveliness = '';
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -164,8 +156,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('throws when there are no available points remaining', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
// call for 31 times
|
||||
for (let i = 0; i < 31; i += 1) {
|
||||
@@ -189,8 +180,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('uses the user id if supplied or the ip address', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
@@ -220,8 +210,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('applies increased cost for registration calls with and without user id', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
req.path = '/api/v4/user/auth/local/register';
|
||||
|
||||
req.ip = 1;
|
||||
@@ -252,8 +241,7 @@ describe('rateLimiter middleware', () => {
|
||||
it('applies increased cost for unauthenticated API calls', async () => {
|
||||
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
|
||||
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
|
||||
const setupRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
const attachRateLimiter = setupRateLimiter();
|
||||
const attachRateLimiter = requireAgain(pathToRateLimiter).default;
|
||||
|
||||
req.ip = 1;
|
||||
await attachRateLimiter(req, res, next);
|
||||
|
||||
@@ -6,8 +6,6 @@ import {
|
||||
SPAM_MESSAGE_LIMIT,
|
||||
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
|
||||
SPAM_WINDOW_LENGTH,
|
||||
MAX_CHAT_COUNT,
|
||||
MAX_SUBBED_GROUP_CHAT_COUNT,
|
||||
INVITES_LIMIT,
|
||||
model as Group,
|
||||
} from '../../../../website/server/models/group';
|
||||
@@ -20,7 +18,6 @@ import {
|
||||
import * as email from '../../../../website/server/libs/email';
|
||||
import { TAVERN_ID } from '../../../../website/common/script/constants';
|
||||
import shared from '../../../../website/common';
|
||||
import { chatModel as Chat } from '../../../../website/server/models/message';
|
||||
|
||||
describe('Group Model', () => {
|
||||
let party; let questLeader; let participatingMember;
|
||||
@@ -1359,29 +1356,6 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEffectiveChatLimit', () => {
|
||||
it('returns the correct chat limit', () => {
|
||||
const group = new Group();
|
||||
expect(group.getEffectiveChatLimit()).to.eql(MAX_CHAT_COUNT);
|
||||
});
|
||||
|
||||
it('returns the passed limit if it is lower than the max', () => {
|
||||
const group = new Group();
|
||||
expect(group.getEffectiveChatLimit(10)).to.eql(10);
|
||||
});
|
||||
|
||||
it('returns the max if the passed limit is higher', () => {
|
||||
const group = new Group();
|
||||
expect(group.getEffectiveChatLimit(MAX_CHAT_COUNT + 10)).to.eql(MAX_CHAT_COUNT);
|
||||
});
|
||||
|
||||
it('returns the max for group plans', () => {
|
||||
const group = new Group();
|
||||
group.purchased.plan.customerId = '110002222333';
|
||||
expect(group.getEffectiveChatLimit()).to.eql(MAX_SUBBED_GROUP_CHAT_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sendChat', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.spy(User, 'updateOne');
|
||||
@@ -1488,34 +1462,6 @@ describe('Group Model', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#trimChat', () => {
|
||||
it('Only checks last message when not enough messages to trim', async () => {
|
||||
sandbox.spy(Chat, 'find');
|
||||
sandbox.spy(Chat, 'deleteMany');
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await party.trimChat();
|
||||
|
||||
expect(Chat.find).to.be.calledOnce;
|
||||
expect(Chat.deleteMany).to.not.be.called;
|
||||
expect(await Chat.countDocuments({ groupId: party._id })).to.eql(3);
|
||||
});
|
||||
it('Deletes messages over the limit', async () => {
|
||||
sandbox.spy(Chat, 'find');
|
||||
sandbox.spy(Chat, 'deleteMany');
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
await Chat.insertOne({ groupId: party._id, timestamp: new Date() });
|
||||
|
||||
await party.trimChat(1);
|
||||
|
||||
expect(Chat.find).to.be.calledOnce;
|
||||
expect(Chat.deleteMany).to.be.calledOnce;
|
||||
expect(await Chat.countDocuments({ groupId: party._id })).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#startQuest', () => {
|
||||
context('Failure Conditions', () => {
|
||||
it('throws an error if group is not a party', async () => {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
generateUser,
|
||||
requester,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /analytics/track/:eventName', () => {
|
||||
it('calls res.analytics', async () => {
|
||||
const user = await generateUser();
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
|
||||
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
@@ -91,23 +91,6 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not accept invite for a quest already underway', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -100,23 +100,6 @@ describe('POST /groups/:groupId/quests/reject', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user rejected', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(false);
|
||||
});
|
||||
|
||||
it('return an error when a user rejects an invite already accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
|
||||
|
||||
describe('POST /user/sleep', () => {
|
||||
let user;
|
||||
@@ -22,4 +23,15 @@ describe('POST /user/sleep', () => {
|
||||
await user.sync();
|
||||
expect(user.preferences.sleep).to.be.false;
|
||||
});
|
||||
|
||||
it('sends sleep status to analytics service', async () => {
|
||||
sandbox.spy(analytics, 'track');
|
||||
|
||||
await user.post('/user/sleep');
|
||||
await user.sync();
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
|
||||
|
||||
sandbox.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
|
||||
import { encrypt } from '../../../../../../website/server/libs/encryption';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
function generateRandomUserName () {
|
||||
return (Date.now() + uuid()).substring(0, 20);
|
||||
@@ -42,25 +41,6 @@ describe('POST /user/auth/local/register', () => {
|
||||
expect(user.newUser).to.eql(true);
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
const password = 'password';
|
||||
|
||||
const user = await api.post('/user/auth/local/register', {
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
confirmPassword: password,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', user._id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
|
||||
});
|
||||
|
||||
it('registers a new user and sets verifiedUsername to true', async () => {
|
||||
const username = generateRandomUserName();
|
||||
const email = `${username}@example.com`;
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
getProperty,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
|
||||
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
|
||||
|
||||
describe('POST /user/auth/social', () => {
|
||||
let api;
|
||||
@@ -66,19 +65,6 @@ describe('POST /user/auth/social', () => {
|
||||
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
|
||||
});
|
||||
|
||||
it('tracks a registration event', async () => {
|
||||
const socialUser = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
|
||||
expect(registrationEvent).to.exist;
|
||||
expect(registrationEvent).to.have.property('userId', socialUser.id);
|
||||
expect(registrationEvent).to.have.property('ipAddress');
|
||||
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
|
||||
});
|
||||
|
||||
it('includes sanitized version of provided username', async () => {
|
||||
const response = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
@@ -245,17 +231,6 @@ describe('POST /user/auth/social', () => {
|
||||
expect(response.newUser).to.be.false;
|
||||
});
|
||||
|
||||
it('does not track a registration event for existing users', async () => {
|
||||
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
await user.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
network,
|
||||
});
|
||||
|
||||
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
|
||||
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
|
||||
});
|
||||
|
||||
it('does not log into other account if social auth already exists', async () => {
|
||||
const registerResponse = await api.post(endpoint, {
|
||||
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
|
||||
|
||||
@@ -13,6 +13,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buy', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
@@ -31,6 +32,12 @@ describe('shared.ops.buy', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('returns error when key is not provided', async () => {
|
||||
@@ -44,8 +51,10 @@ describe('shared.ops.buy', () => {
|
||||
|
||||
it('buys health potion', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buy(user, { params: { key: 'potion' } });
|
||||
await buy(user, { params: { key: 'potion' } }, analytics);
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds equipment to inventory', async () => {
|
||||
|
||||
@@ -29,9 +29,10 @@ describe('shared.ops.buyArmoire', () => {
|
||||
const YIELD_EQUIPMENT = 0.5;
|
||||
const YIELD_FOOD = 0.7;
|
||||
const YIELD_EXP = 0.9;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyArmoire (_user, _req) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req);
|
||||
async function buyArmoire (_user, _req, _analytics) {
|
||||
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -49,10 +50,12 @@ describe('shared.ops.buyArmoire', () => {
|
||||
user.items.food = {};
|
||||
|
||||
sandbox.stub(randomValFns, 'trueRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
randomValFns.trueRandom.restore();
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -144,7 +147,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(2);
|
||||
|
||||
await buyArmoire(user, {});
|
||||
await buyArmoire(user, {}, analytics);
|
||||
|
||||
expect(_.size(user.items.gear.owned)).to.equal(3);
|
||||
|
||||
@@ -152,6 +155,7 @@ describe('shared.ops.buyArmoire', () => {
|
||||
|
||||
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
|
||||
expect(user.stats.gp).to.eql(100);
|
||||
expect(analytics.track).to.be.calledTwice;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
@@ -10,14 +11,15 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
|
||||
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
|
||||
|
||||
async function buyGem (user, req) {
|
||||
const buyOp = new BuyGemOperation(user, req);
|
||||
async function buyGem (user, req, analytics) {
|
||||
const buyOp = new BuyGemOperation(user, req, analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyGem', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
const goldPoints = 40;
|
||||
const gemsBought = 40;
|
||||
const userGemAmount = 10;
|
||||
@@ -33,16 +35,23 @@ describe('shared.ops.buyGem', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Gems', () => {
|
||||
it('purchases gems', async () => {
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
|
||||
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases gems with a different language than the default', async () => {
|
||||
|
||||
@@ -10,9 +10,10 @@ import i18n from '../../../../website/common/script/i18n';
|
||||
|
||||
describe('shared.ops.buyHealthPotion', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyHealthPotion (_user, _req) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req);
|
||||
async function buyHealthPotion (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -31,13 +32,19 @@ describe('shared.ops.buyHealthPotion', () => {
|
||||
},
|
||||
stats: { gp: 200 },
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Potion', () => {
|
||||
it('recovers 15 hp', async () => {
|
||||
user.stats.hp = 30;
|
||||
await buyHealthPotion(user, {});
|
||||
await buyHealthPotion(user, {}, analytics);
|
||||
expect(user.stats.hp).to.eql(45);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('does not increase hp above 50', async () => {
|
||||
|
||||
@@ -13,14 +13,15 @@ import {
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
|
||||
|
||||
async function buyGear (user, req) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req);
|
||||
async function buyGear (user, req, analytics) {
|
||||
const buyOp = new BuyMarketGearOperation(user, req, analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyMarketGear', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -46,12 +47,14 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
sinon.stub(shared, 'randomVal');
|
||||
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
|
||||
sinon.stub(shared.fns, 'predictableRandom');
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
shared.randomVal.restore();
|
||||
shared.fns.predictableRandom.restore();
|
||||
shared.onboarding.checkOnboardingStatus.restore();
|
||||
analytics.track.restore();
|
||||
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
@@ -62,7 +65,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
it('adds equipment to inventory', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.items.gear.owned).to.eql({
|
||||
weapon_warrior_0: true,
|
||||
@@ -89,12 +92,13 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
eyewear_special_whiteHalfMoon: true,
|
||||
eyewear_special_yellowHalfMoon: true,
|
||||
});
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
|
||||
user.stats.gp = 31;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.be.calledOnce;
|
||||
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
|
||||
@@ -107,7 +111,7 @@ describe('shared.ops.buyMarketGear', () => {
|
||||
user.stats.gp = 31;
|
||||
user.achievements.purchasedEquipment = true;
|
||||
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } });
|
||||
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
|
||||
|
||||
expect(user.addAchievement).to.not.be.called;
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyMysterySet', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
let clock;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -26,9 +27,11 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -90,7 +93,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
context('successful purchases', () => {
|
||||
it('buys Steampunk Accessories Set', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '301404' } });
|
||||
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
|
||||
@@ -103,7 +106,7 @@ describe('shared.ops.buyMysterySet', () => {
|
||||
it('buys mystery set if it is available', async () => {
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
user.purchased.plan.consecutive.trinkets = 1;
|
||||
await buyMysterySet(user, { params: { key: '201601' } });
|
||||
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
|
||||
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
|
||||
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
|
||||
|
||||
@@ -12,9 +12,10 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req);
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
@@ -24,11 +25,13 @@ describe('shared.ops.buyQuestGems', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sinon.useFakeTimers(new Date('2024-01-16'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
|
||||
@@ -12,15 +12,21 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
|
||||
describe('shared.ops.buyQuest', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyQuest (_user, _req) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
|
||||
async function buyQuest (_user, _req, _analytics) {
|
||||
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
it('buys a Quest scroll', async () => {
|
||||
@@ -29,11 +35,12 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
|
||||
@@ -42,9 +49,10 @@ describe('shared.ops.buyQuest', () => {
|
||||
user.items.quests[key] = -1;
|
||||
await buyQuest(user, {
|
||||
params: { key },
|
||||
});
|
||||
}, analytics);
|
||||
expect(user.items.quests[key]).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(5);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
|
||||
@@ -53,13 +61,13 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
await buyQuest(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: '3',
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress1: 4,
|
||||
@@ -74,7 +82,7 @@ describe('shared.ops.buyQuest', () => {
|
||||
key: 'dilatoryDistress1',
|
||||
},
|
||||
quantity: 'a',
|
||||
});
|
||||
}, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -179,11 +187,12 @@ describe('shared.ops.buyQuest', () => {
|
||||
params: {
|
||||
key: 'dilatoryDistress3',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.items.quests).to.eql({
|
||||
dilatoryDistress3: 1,
|
||||
});
|
||||
expect(user.stats.gp).to.equal(100);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,17 +14,20 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
|
||||
describe('shared.ops.buySpecialSpell', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buySpecialSpell (_user, _req) {
|
||||
const buyOp = new BuySpellOperation(_user, _req);
|
||||
async function buySpecialSpell (_user, _req, _analytics) {
|
||||
const buyOp = new BuySpellOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
if (clock) {
|
||||
clock.restore();
|
||||
}
|
||||
@@ -75,7 +78,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'thankyou',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.thankyou).to.equal(1);
|
||||
@@ -86,6 +89,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a limited card when it is available', async () => {
|
||||
@@ -97,7 +101,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'nye',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.nye).to.equal(1);
|
||||
@@ -108,6 +112,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the card is not currently available', async () => {
|
||||
@@ -135,7 +140,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
params: {
|
||||
key: 'seafoam',
|
||||
},
|
||||
});
|
||||
}, analytics);
|
||||
|
||||
expect(user.stats.gp).to.equal(1);
|
||||
expect(user.items.special.seafoam).to.equal(1);
|
||||
@@ -146,6 +151,7 @@ describe('shared.ops.buySpecialSpell', () => {
|
||||
expect(message).to.equal(i18n.t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('throws an error if the spell is not currently available', async () => {
|
||||
|
||||
@@ -13,15 +13,21 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
|
||||
|
||||
describe('common.ops.hourglassPurchase', () => {
|
||||
let user;
|
||||
const analytics = { track () {} };
|
||||
|
||||
async function buyMount (_user, _req) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req);
|
||||
async function buyMount (_user, _req, _analytics) {
|
||||
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser();
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('failure conditions', () => {
|
||||
@@ -125,11 +131,12 @@ describe('common.ops.hourglassPurchase', () => {
|
||||
it('buys a pet', async () => {
|
||||
user.purchased.plan.consecutive.trinkets = 2;
|
||||
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
|
||||
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
|
||||
|
||||
expect(message).to.eql(i18n.t('hourglassPurchase'));
|
||||
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
|
||||
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('buys a mount', async () => {
|
||||
|
||||
@@ -17,17 +17,20 @@ describe('shared.ops.purchase', () => {
|
||||
let user;
|
||||
let clock;
|
||||
const goldPoints = 40;
|
||||
const analytics = { track () {} };
|
||||
|
||||
before(() => {
|
||||
user = generateUser({ 'stats.class': 'rogue' });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
sinon.stub(analytics, 'track');
|
||||
sinon.spy(pinnedGearUtils, 'removeItemByPath');
|
||||
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
pinnedGearUtils.removeItemByPath.restore();
|
||||
clock.restore();
|
||||
});
|
||||
@@ -184,10 +187,11 @@ describe('shared.ops.purchase', () => {
|
||||
const type = 'eggs';
|
||||
const key = 'Wolf';
|
||||
|
||||
await purchase(user, { params: { type, key } });
|
||||
await purchase(user, { params: { type, key } }, analytics);
|
||||
|
||||
expect(user.items[type][key]).to.equal(1);
|
||||
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases hatchingPotions', async () => {
|
||||
@@ -328,7 +332,7 @@ describe('shared.ops.purchase', () => {
|
||||
const key = 'Wolf';
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
|
||||
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -341,7 +345,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: -2 });
|
||||
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
@@ -354,7 +358,7 @@ describe('shared.ops.purchase', () => {
|
||||
user.balance = 10;
|
||||
|
||||
try {
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 });
|
||||
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
|
||||
@@ -54,4 +54,19 @@ describe('armoire', () => {
|
||||
const febuaryItems = armoire.all;
|
||||
expect(febuaryItems.length).to.equal(384);
|
||||
});
|
||||
|
||||
it('sets have at least 2 items', () => {
|
||||
const setMap = {};
|
||||
forEach(armoire.all, item => {
|
||||
// Gotta have one outlier
|
||||
if (!item.set || item.set.startsWith('armoire-')) return;
|
||||
if (setMap[item.set] === undefined) {
|
||||
setMap[item.set] = 0;
|
||||
}
|
||||
setMap[item.set] += 1;
|
||||
});
|
||||
Object.keys(setMap).forEach(set => {
|
||||
expect(setMap[set], set).to.be.at.least(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ function _requestMaker (user, method, additionalSets = {}) {
|
||||
|| route.indexOf('/paypal') === 0
|
||||
|| route.indexOf('/amazon') === 0
|
||||
|| route.indexOf('/stripe') === 0
|
||||
|| route.indexOf('/analytics') === 0
|
||||
) {
|
||||
url += `${route}`;
|
||||
} else {
|
||||
|
||||
@@ -12,12 +12,20 @@ module.exports = {
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
// TODO find a way to let eslint understand webpack aliases
|
||||
'import/no-unresolved': 'off',
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
'import/extensions': 'off',
|
||||
'prefer-regex-literals': 'warn',
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
// this creates issues with the current way we have to push the process.env vars to webpack
|
||||
// https://github.com/eslint/eslint/issues/14918
|
||||
// https://github.com/webpack/webpack/issues/5392
|
||||
// off for now, because any eslint --fix will then still do it anyway
|
||||
// maybe this can be turned on again once we switch to newer vue/vite
|
||||
// Important! process.env.XYZ should not be destructured
|
||||
'prefer-destructuring': 'off',
|
||||
'vue/html-self-closing': ['error', {
|
||||
html: {
|
||||
void: 'never',
|
||||
|
||||
Generated
+589
-20
@@ -41,6 +41,7 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -54,7 +55,9 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"vitest": "^3.0.5"
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-connector": {
|
||||
@@ -2108,9 +2111,8 @@
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
@@ -3632,12 +3634,41 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*",
|
||||
"@types/json-schema": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/eslint-scope": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
|
||||
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint": "*",
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json-schema": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -3648,9 +3679,8 @@
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -3846,6 +3876,181 @@
|
||||
"vue-template-compiler": "^2.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
|
||||
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/helper-numbers": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/floating-point-hex-parser": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
|
||||
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-api-error": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
|
||||
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-buffer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
|
||||
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-numbers": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
|
||||
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
|
||||
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/helper-wasm-section": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
|
||||
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ieee754": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
|
||||
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xtuc/ieee754": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/leb128": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
|
||||
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/utf8": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
|
||||
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-edit": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
|
||||
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-section": "1.14.1",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1",
|
||||
"@webassemblyjs/wasm-opt": "1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "1.14.1",
|
||||
"@webassemblyjs/wast-printer": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-gen": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
|
||||
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/ieee754": "1.13.2",
|
||||
"@webassemblyjs/leb128": "1.13.2",
|
||||
"@webassemblyjs/utf8": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-opt": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
|
||||
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-buffer": "1.14.1",
|
||||
"@webassemblyjs/wasm-gen": "1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "1.14.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wasm-parser": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
|
||||
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@webassemblyjs/helper-api-error": "1.13.2",
|
||||
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
|
||||
"@webassemblyjs/ieee754": "1.13.2",
|
||||
"@webassemblyjs/leb128": "1.13.2",
|
||||
"@webassemblyjs/utf8": "1.13.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/wast-printer": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
|
||||
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@webassemblyjs/ast": "1.14.1",
|
||||
"@xtuc/long": "4.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@xtuc/ieee754": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
|
||||
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@xtuc/long": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
|
||||
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
@@ -3893,6 +4098,48 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/amplitude-js": {
|
||||
"version": "8.21.9",
|
||||
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
|
||||
@@ -4370,9 +4617,8 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cac": {
|
||||
"version": "6.7.14",
|
||||
@@ -4537,6 +4783,16 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/chrome-trace-event": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
|
||||
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@@ -4603,9 +4859,8 @@
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
@@ -4941,6 +5196,20 @@
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"tapable": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/enquirer": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
|
||||
@@ -6115,6 +6384,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.x"
|
||||
}
|
||||
},
|
||||
"node_modules/expect-type": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
|
||||
@@ -6592,6 +6871,13 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "13.24.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
|
||||
@@ -6635,6 +6921,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/habitica-markdown": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
|
||||
@@ -7433,6 +7726,37 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
|
||||
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker/node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/jquery": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
|
||||
@@ -7517,6 +7841,13 @@
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
@@ -7580,6 +7911,20 @@
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
|
||||
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.11.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
@@ -7783,6 +8128,13 @@
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@@ -8106,6 +8458,13 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
@@ -9145,6 +9504,63 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
|
||||
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"ajv": "^8.9.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"ajv-keywords": "^5.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
"node_modules/schema-utils/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/secure-keys": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
|
||||
@@ -9422,9 +9838,8 @@
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
@@ -9715,6 +10130,20 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-mini": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
|
||||
@@ -9725,9 +10154,8 @@
|
||||
"version": "5.44.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
|
||||
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
@@ -9741,13 +10169,47 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webpack": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"esbuild": {
|
||||
"optional": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -10123,9 +10585,8 @@
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
@@ -10504,6 +10965,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-fragment": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-fragment/-/vue-fragment-1.6.0.tgz",
|
||||
"integrity": "sha512-a5T8ZZZK/EQzgVShEl374HbobUJ0a7v12BzOzS6Z/wd/5EE/5SffcyHC+7bf9hP3L7Yc0hhY/GhMdwFQ25O/8A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.5.16"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-functional-data-merge": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
|
||||
@@ -10571,6 +11041,20 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
@@ -10581,6 +11065,91 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.102.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
|
||||
"integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.26.3",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"loader-runner": "^4.2.0",
|
||||
"mime-types": "^2.1.27",
|
||||
"neo-async": "^2.6.2",
|
||||
"schema-utils": "^4.3.3",
|
||||
"tapable": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.4",
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"webpack-cli": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack/node_modules/acorn-import-phases": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-compression2": "^1.3.3",
|
||||
"vue": "^2.7.10",
|
||||
"vue-fragment": "^1.6.0",
|
||||
"vue-mugen-scroll": "^0.2.6",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuedraggable": "^2.24.3",
|
||||
@@ -59,6 +60,8 @@
|
||||
"jsdom": "^26.0.0",
|
||||
"mocha": "^11.1.0",
|
||||
"playwright": "^1.50.1",
|
||||
"vitest": "^3.0.5"
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"vitest": "^3.0.5",
|
||||
"webpack": "^5.94.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,3 @@
|
||||
.quest_lostMasterclasser4 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_solarSystem {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_virtualpet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_alien.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid,
|
||||
.Pet_HatchingPotion_Alien {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_VirtualPet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Fungi {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Alien {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Alien.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
@@ -91,6 +26,7 @@
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -98,7 +34,6 @@
|
||||
|
||||
/* Critical */
|
||||
.weapon_special_critical {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-left:-12px;
|
||||
@@ -109,6 +44,7 @@
|
||||
.weapon_special_1 {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -117,36 +53,15 @@
|
||||
.back_special_heroicAureole {
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
}
|
||||
.head_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.broad_armor_special_0,.slim_armor_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
}
|
||||
.broad_armor_special_1,.slim_armor_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Wolf-Cerberus {
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
|
||||
@@ -154,36 +69,17 @@
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.eyewear_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.Pet-Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
|
||||
width: 81px;
|
||||
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;
|
||||
}
|
||||
@@ -193,39 +89,11 @@
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Head_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_airship, .background_clocktower, .background_steamworks {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
|
||||
.background_airship {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_clocktower {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_steamworks {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
|
||||
@@ -695,11 +695,6 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_beach_with_volcano {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beach_with_volcano.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_beehive {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beehive.png');
|
||||
width: 141px;
|
||||
@@ -2351,11 +2346,6 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_tropical_coral_garden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tropical_coral_garden.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_tulip_garden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tulip_garden.png');
|
||||
width: 141px;
|
||||
@@ -2411,11 +2401,6 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_vegetable_garden {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_vegetable_garden.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_viking_ship {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_viking_ship.png');
|
||||
width: 141px;
|
||||
@@ -29895,11 +29880,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_kendoBogu {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_kendoBogu.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_armoire_lamplightersGreatcoat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lamplightersGreatcoat.png');
|
||||
width: 114px;
|
||||
@@ -30555,11 +30535,6 @@
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_kendoMen {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_kendoMen.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_armoire_lamplightersTopHat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lamplightersTopHat.png');
|
||||
width: 114px;
|
||||
@@ -30945,11 +30920,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_gardenHose {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenHose.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_armoire_gardenersSpade {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenersSpade.png');
|
||||
width: 114px;
|
||||
@@ -31580,11 +31550,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_kendoBogu {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_kendoBogu.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_lamplightersGreatcoat {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lamplightersGreatcoat.png');
|
||||
width: 114px;
|
||||
@@ -31965,11 +31930,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_brightRainbowKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_brightRainbowKite.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_buoyantBubbles {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_buoyantBubbles.png');
|
||||
width: 114px;
|
||||
@@ -32070,11 +32030,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_gardenRake {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenRake.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_gardenersWateringCan {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenersWateringCan.png');
|
||||
width: 114px;
|
||||
@@ -32170,11 +32125,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_kendoShinai {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_kendoShinai.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_lamplighter {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_lamplighter.png');
|
||||
width: 114px;
|
||||
@@ -32260,11 +32210,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pastelRainbowKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pastelRainbowKite.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_armoire_pinkKite {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pinkKite.png');
|
||||
width: 114px;
|
||||
@@ -34255,11 +34200,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.eyewear_mystery_202606 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/eyewear_mystery_202606.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.head_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34280,31 +34220,11 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_mystery_202606 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202606.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_mystery_202605 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_mystery_202606 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202606.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_mystery_202607 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202607.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.shield_mystery_202608 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202608.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.slim_armor_mystery_202512 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
|
||||
width: 114px;
|
||||
@@ -34330,16 +34250,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_mystery_202607 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202607.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.weapon_mystery_202608 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202608.png');
|
||||
width: 117px;
|
||||
height: 120px;
|
||||
}
|
||||
.back_mystery_201402 {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
|
||||
width: 90px;
|
||||
@@ -37805,26 +37715,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.broad_armor_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.broad_armor_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.broad_armor_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38075,26 +37965,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.head_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.head_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.head_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38285,21 +38155,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.shield_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.shield_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38540,26 +38395,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.slim_armor_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.slim_armor_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png');
|
||||
width: 90px;
|
||||
@@ -38800,26 +38635,6 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summer2026Healer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Healer.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summer2026Mage {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Mage.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.weapon_special_summer2026Rogue {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Rogue.png');
|
||||
width: 114px;
|
||||
height: 117px;
|
||||
}
|
||||
.weapon_special_summer2026Warrior {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summer2026Warrior.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.weapon_special_summerHealer {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png');
|
||||
width: 90px;
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.d-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3344_18)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12H7V10H9V12ZM16 2V14C16 15.1 15.1 16 14 16H2C0.9 16 0 15.1 0 14V2C0 0.9 0.9 0 2 0H14C15.1 0 16 0.9 16 2ZM14 2H2V14H14V2ZM9 4H7V9H9V4Z" fill="#4E4A57"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3344_18">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 450 B |
@@ -108,15 +108,15 @@ export default {
|
||||
const allEmails = [];
|
||||
if (user.auth.local.email) allEmails.push(user.auth.local.email);
|
||||
if (user.auth.google && user.auth.google.emails) {
|
||||
const { emails } = user.auth.google;
|
||||
const emails = user.auth.google.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.apple && user.auth.apple.emails) {
|
||||
const { emails } = user.auth.apple;
|
||||
const emails = user.auth.apple.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
if (user.auth.facebook && user.auth.facebook.emails) {
|
||||
const { emails } = user.auth.facebook;
|
||||
const emails = user.auth.facebook.emails;
|
||||
allEmails.push(...this.findSocialEmails(emails));
|
||||
}
|
||||
return allEmails;
|
||||
|
||||
+1
-1
@@ -609,7 +609,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
|
||||
import saveHero from '../mixins/saveHero';
|
||||
import LoadingSpinner from '@/components/ui/loadingSpinner';
|
||||
|
||||
const { PLAY_CONSOLE_ORDERS_BASE_URL } = import.meta.env;
|
||||
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
|
||||
|
||||
const humanReadablePaymentDetails = {
|
||||
customerId: {
|
||||
|
||||
@@ -20,29 +20,6 @@
|
||||
class="form mx-auto"
|
||||
@submit.prevent.stop="register()"
|
||||
>
|
||||
<div v-if="needsEmailField">
|
||||
<input
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('emailAddress')"
|
||||
:class="{
|
||||
'mb-3': !emailError,
|
||||
'input-invalid input-with-error mb-2': emailError,
|
||||
'input-valid': email && emailValid,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-if="emailError"
|
||||
class="input-error"
|
||||
>
|
||||
{{ emailError }}
|
||||
</div>
|
||||
<p class="purple-600 mb-3">
|
||||
{{ $t('emailRequiredForSupport') }}
|
||||
</p>
|
||||
</div>
|
||||
<input
|
||||
id="usernameInput"
|
||||
v-model="username"
|
||||
@@ -66,14 +43,6 @@
|
||||
<p class="purple-600">
|
||||
{{ $t('usernameLimitations') }}
|
||||
</p>
|
||||
<input
|
||||
v-if="needsEmailField"
|
||||
id="emailInput"
|
||||
v-model="email"
|
||||
class="form-control dark"
|
||||
type="text"
|
||||
:placeholder="$t('email')"
|
||||
>
|
||||
<div class="custom-control custom-checkbox mb-4">
|
||||
<input
|
||||
id="privacyTOS"
|
||||
@@ -89,9 +58,8 @@
|
||||
></label>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-info d-flex justify-content-center
|
||||
align-items-center w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
|
||||
class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
|
||||
:disabled="!username || usernameInvalid || !privacyAccepted"
|
||||
type="submit"
|
||||
>
|
||||
{{ $t('getStarted') }}
|
||||
@@ -165,12 +133,10 @@
|
||||
border: 2px solid transparent;
|
||||
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
|
||||
|
||||
&:not(:disabled):not(.disabled) {
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
&:focus, &:active {
|
||||
background-color: $blue-50;
|
||||
border: 2px solid $purple-400;
|
||||
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,19 +148,23 @@
|
||||
<script>
|
||||
import debounce from 'lodash/debounce';
|
||||
import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import accountCreation from '@/mixins/accountCreation';
|
||||
import sanitizeRedirect from '@/mixins/sanitizeRedirect';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PrivacyBanner,
|
||||
},
|
||||
mixins: [accountCreation, sanitizeRedirect],
|
||||
mixins: [sanitizeRedirect],
|
||||
data () {
|
||||
return {
|
||||
authData: {},
|
||||
email: '',
|
||||
password: '',
|
||||
passwordConfirm: '',
|
||||
privacyAccepted: false,
|
||||
registrationMethod: null,
|
||||
username: '',
|
||||
usernameIssues: [],
|
||||
needsEmailField: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -213,39 +183,30 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
this.authData = this.$store.state.registrationOptions.authData;
|
||||
this.email = this.$store.state.registrationOptions.email;
|
||||
this.username = this.$store.state.registrationOptions.username;
|
||||
this.password = this.$store.state.registrationOptions.password;
|
||||
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
|
||||
|
||||
if (window.sessionStorage.getItem('apple-token')) {
|
||||
this.registrationMethod = 'apple';
|
||||
if (!this.email) {
|
||||
this.email = window.sessionStorage.getItem('apple-email');
|
||||
}
|
||||
} else if (!this.$store.state.registrationOptions.registrationMethod) {
|
||||
this.$router.push('/');
|
||||
} else {
|
||||
this.registrationMethod = this.$store.state.registrationOptions.registrationMethod;
|
||||
}
|
||||
|
||||
if (!this.email && this.registrationMethod !== 'apple') {
|
||||
if (!this.email) {
|
||||
return;
|
||||
}
|
||||
if ((!this.email || this.email === '') && this.registrationMethod === 'apple') {
|
||||
this.needsEmailField = true;
|
||||
}
|
||||
if (this.email) {
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
}
|
||||
const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
username: usernameToCheck,
|
||||
}).then(res => {
|
||||
if (!res.issues) {
|
||||
this.username = usernameToCheck;
|
||||
}
|
||||
});
|
||||
document.getElementById('usernameInput').focus();
|
||||
},
|
||||
methods: {
|
||||
@@ -276,7 +237,6 @@ export default {
|
||||
idToken: window.sessionStorage.getItem('apple-token'),
|
||||
name: window.sessionStorage.getItem('apple-name'),
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
allowRegister: true,
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
this.cancel();
|
||||
return [];
|
||||
}
|
||||
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring
|
||||
this.currentSearch = regexRes[1];
|
||||
|
||||
if (this.currentSearch.length === 0) return [];
|
||||
|
||||
|
||||
@@ -470,7 +470,7 @@ export default {
|
||||
return this.userGuilds.filter(group => {
|
||||
const leaderId = group.leader?._id || group.leader;
|
||||
if (leaderId !== this.user._id) return false;
|
||||
const { purchased } = group;
|
||||
const purchased = group.purchased;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (this.activeGroupPlanIds.includes(group._id)) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
@@ -492,7 +492,7 @@ export default {
|
||||
},
|
||||
isPartyPreviouslyUpgraded () {
|
||||
if (!this.userParty) return false;
|
||||
const { purchased } = this.userParty;
|
||||
const purchased = this.userParty.purchased;
|
||||
if (!purchased?.wasUpgraded) return false;
|
||||
if (!purchased.dateTerminated) return false;
|
||||
return new Date(purchased.dateTerminated) < new Date();
|
||||
@@ -533,7 +533,7 @@ export default {
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.upgradeableGuilds.length > 0) {
|
||||
[this.selectedOption] = this.upgradeableGuilds;
|
||||
this.selectedOption = this.upgradeableGuilds[0];
|
||||
} else if (this.upgradeableParty) {
|
||||
this.selectedOption = this.upgradeableParty;
|
||||
} else {
|
||||
|
||||
@@ -198,6 +198,7 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
|
||||
import todoIcon from '@/assets/svg/todo.svg?raw';
|
||||
import rewardIcon from '@/assets/svg/reward.svg?raw';
|
||||
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapState } from '@/libs/store';
|
||||
|
||||
export default {
|
||||
@@ -437,6 +438,14 @@ export default {
|
||||
return false;
|
||||
},
|
||||
changeMirrorPreference (newVal) {
|
||||
Analytics.track({
|
||||
eventName: 'mirror tasks',
|
||||
eventAction: 'mirror tasks',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
mirror: newVal,
|
||||
group: this.group._id,
|
||||
}, { trackOnClient: true });
|
||||
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
|
||||
if (newVal) { // we're turning copy ON for this group
|
||||
groupsToMirror.push(this.group._id);
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import closeX from '../ui/closeX';
|
||||
|
||||
@@ -275,6 +276,11 @@ export default {
|
||||
this.$store.state.party.data = party;
|
||||
this.user.party._id = party._id;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: party._id,
|
||||
partySize: 1,
|
||||
});
|
||||
|
||||
this.$root.$emit('bv::hide::modal', 'create-party-modal');
|
||||
await this.$router.push('/party');
|
||||
},
|
||||
|
||||
@@ -314,6 +314,7 @@ import extend from 'lodash/extend';
|
||||
import groupUtilities from '@/mixins/groupsUtilities';
|
||||
import styleHelper from '@/mixins/styleHelper';
|
||||
import { mapGetters } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import participantListModal from './participantListModal';
|
||||
import groupFormModal from './groupFormModal';
|
||||
import groupGemsModal from '@/components/groups/groupGemsModal';
|
||||
@@ -559,6 +560,7 @@ export default {
|
||||
|
||||
if (this.isParty) {
|
||||
data.type = 'party';
|
||||
Analytics.updateUser({ partySize: null, partyID: null });
|
||||
this.$store.state.partyMembers = [];
|
||||
}
|
||||
|
||||
|
||||
@@ -334,6 +334,7 @@ import orderBy from 'lodash/orderBy';
|
||||
import * as quests from '@/../../common/script/content/quests';
|
||||
import getItemInfo from '@/../../common/script/libs/getItemInfo';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
|
||||
import questDialogContent from '../shops/quests/questDialogContent';
|
||||
@@ -420,6 +421,11 @@ export default {
|
||||
async questInit () {
|
||||
this.loading = true;
|
||||
|
||||
Analytics.updateUser({
|
||||
partyID: this.group._id,
|
||||
partySize: this.group.memberCount,
|
||||
});
|
||||
|
||||
const groupId = this.group._id || this.user.party._id;
|
||||
|
||||
const key = this.selectedQuest;
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
|
||||
<script>
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { mapGetters, mapActions } from '@/libs/store';
|
||||
import MemberDetails from '../memberDetails';
|
||||
import createPartyModal from '../groups/createPartyModal';
|
||||
@@ -235,8 +236,22 @@ export default {
|
||||
},
|
||||
async createOrInviteParty () {
|
||||
if (this.user.party._id) {
|
||||
await Analytics.track({
|
||||
eventName: 'Header Party CTA',
|
||||
eventAction: 'Header Party CTA',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
state: 'Find Party Members',
|
||||
});
|
||||
this.$router.push('/looking-for-party');
|
||||
} else {
|
||||
await Analytics.track({
|
||||
eventName: 'Header Party CTA',
|
||||
eventAction: 'Header Party CTA',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
state: 'Get Started',
|
||||
});
|
||||
this.$root.$emit('bv::show::modal', 'create-party-modal');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -114,6 +114,7 @@ import { mapState } from '@/libs/store';
|
||||
import notifications from '@/mixins/notifications';
|
||||
import guide from '@/mixins/guide';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
import yesterdailyModal from './tasks/yesterdailyModal';
|
||||
import newStuff from './news/modal';
|
||||
@@ -647,6 +648,15 @@ export default {
|
||||
// Reset daily analytics actions
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
|
||||
} else {
|
||||
// Note a failed cron event, for our records and investigation
|
||||
Analytics.track({
|
||||
eventName: 'cron failed',
|
||||
eventAction: 'cron failed',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
responseCode: response.status,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
|
||||
// Sync
|
||||
|
||||
@@ -433,6 +433,9 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import paymentsMixin from '@/mixins/payments';
|
||||
|
||||
// analytics
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
selectTranslatedArray,
|
||||
@@ -533,6 +536,16 @@ export default {
|
||||
this.close();
|
||||
},
|
||||
submit () {
|
||||
if (this.paymentData.group && !this.paymentData.newGroup) {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan upgrade',
|
||||
eventAction: 'group plan upgrade',
|
||||
eventCategory: 'behavior',
|
||||
demographics: this.upgradedGroup.demographics,
|
||||
type: this.paymentData.group.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
this.paymentData = {};
|
||||
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
|
||||
},
|
||||
|
||||
@@ -37,9 +37,6 @@ export default {
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
window.sessionStorage.setItem('apple-token', response.idToken);
|
||||
if (response.email) {
|
||||
window.sessionStorage.setItem('apple-email', response.email);
|
||||
}
|
||||
window.location.href = '/username';
|
||||
}
|
||||
},
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-if="taskList.length > 0 && !rerendering"
|
||||
v-if="taskList.length > 0"
|
||||
ref="tasksList"
|
||||
class="sortable-tasks"
|
||||
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
|
||||
@@ -432,7 +432,6 @@ export default {
|
||||
|
||||
selectedItemToBuy: {},
|
||||
dragging: false,
|
||||
rerendering: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -549,8 +548,8 @@ export default {
|
||||
if (this.taskListOverride) originTasks = this.taskListOverride;
|
||||
|
||||
// Server
|
||||
const taskIdToReplace = filteredList[data.newIndex]._id;
|
||||
const newIndexOnServer = originTasks.findIndex(task => task._id === taskIdToReplace);
|
||||
const taskIdToReplace = filteredList[data.newIndex];
|
||||
const newIndexOnServer = originTasks.findIndex(taskId => taskId === taskIdToReplace);
|
||||
|
||||
let newOrder;
|
||||
if (taskToMove.group.id && !this.isUser) {
|
||||
@@ -569,9 +568,6 @@ export default {
|
||||
// Client
|
||||
const deleted = originTasks.splice(data.oldIndex, 1);
|
||||
originTasks.splice(data.newIndex, 0, deleted[0]);
|
||||
this.rerendering = true;
|
||||
await this.$nextTick();
|
||||
this.rerendering = false;
|
||||
},
|
||||
async moveTo (task, where) { // where is 'top' or 'bottom'
|
||||
const taskIdToMove = task._id;
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
}, `type_${task.type}`
|
||||
]"
|
||||
@click="castEnd($event, task)"
|
||||
tabindex="0"
|
||||
@keypress.enter="$emit('editTask', task)"
|
||||
>
|
||||
<div
|
||||
class="d-flex"
|
||||
@@ -100,7 +98,9 @@
|
||||
<div
|
||||
class="task-clickable-area pt-1 pl-75 pb-0"
|
||||
:class="{ 'cursor-auto': !teamManagerAccess }"
|
||||
tabindex="0"
|
||||
@click="edit($event, task)"
|
||||
@keypress.enter="edit($event, task)"
|
||||
>
|
||||
<div class="d-flex justify-content-between">
|
||||
<h3
|
||||
@@ -432,6 +432,10 @@
|
||||
outline: none;
|
||||
transition: none;
|
||||
border: $purple-400 solid 1px;
|
||||
|
||||
:not(task-best-control-inner-habit) { // round icon
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.control-bottom-box {
|
||||
@@ -458,13 +462,16 @@
|
||||
&:hover:not(.task-not-editable.task-not-scoreable),
|
||||
&:focus-within:not(.task-not-editable.task-not-scoreable) {
|
||||
box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
|
||||
z-index: 11;
|
||||
}
|
||||
}
|
||||
|
||||
.task:not(.groupTask) {
|
||||
&:hover, &:focus {
|
||||
border: none;
|
||||
outline: 1px solid $purple-400;
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
.left-control, .right-control, .task-content {
|
||||
border-color: $purple-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +522,11 @@
|
||||
&-user {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: 4px;
|
||||
border: $purple-400 solid 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-title + .task-dropdown ::v-deep .dropdown-menu {
|
||||
|
||||
@@ -412,25 +412,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="task.type === 'daily' && schedulingSummary"
|
||||
class="scheduling-summary mt-2 mb-0"
|
||||
>
|
||||
{{ schedulingSummary }}
|
||||
</p>
|
||||
<div
|
||||
v-if="task.type === 'daily' && schedulingWarning"
|
||||
class="scheduling-warning mt-2"
|
||||
>
|
||||
<span
|
||||
class="scheduling-warning-icon"
|
||||
v-html="icons.exclamationInfo"
|
||||
></span>
|
||||
<span
|
||||
class="scheduling-warning-text"
|
||||
v-html="schedulingWarning"
|
||||
></span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!groupId"
|
||||
class="tags-select option mt-3"
|
||||
@@ -1128,42 +1109,6 @@
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.scheduling-summary {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-50;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.scheduling-warning {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.scheduling-warning-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-right: 6px;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.scheduling-warning-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1294,7 +1239,6 @@ import goldIcon from '@/assets/svg/gold.svg?raw';
|
||||
import chevronIcon from '@/assets/svg/chevron.svg?raw';
|
||||
import calendarIcon from '@/assets/svg/calendar.svg?raw';
|
||||
import gripIcon from '@/assets/svg/grip.svg?raw';
|
||||
import exclamationInfoIcon from '@/assets/svg/exclaimation_info.svg?raw';
|
||||
import InformationIcon from '@/components/ui/informationIcon.vue';
|
||||
import alertIcon from '@/assets/svg/for-css/alert-white.svg?raw';
|
||||
|
||||
@@ -1332,7 +1276,6 @@ export default {
|
||||
streak: streakIcon,
|
||||
calendar: calendarIcon,
|
||||
grip: gripIcon,
|
||||
exclamationInfo: exclamationInfoIcon,
|
||||
alert: alertIcon,
|
||||
}),
|
||||
members: [],
|
||||
@@ -1434,87 +1377,6 @@ export default {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
schedulingSummary () {
|
||||
if (!this.task || this.task.type !== 'daily') return '';
|
||||
const { task } = this;
|
||||
const everyXValue = +task.everyX;
|
||||
|
||||
let interval;
|
||||
if (task.frequency === 'daily') {
|
||||
interval = everyXValue === 1 ? this.$t('everyDay') : this.$t('everyXDays', { count: everyXValue });
|
||||
} else if (task.frequency === 'weekly') {
|
||||
interval = everyXValue === 1 ? this.$t('everyWeek') : this.$t('everyXWeeks', { count: everyXValue });
|
||||
} else if (task.frequency === 'monthly') {
|
||||
interval = everyXValue === 1 ? this.$t('everyMonth') : this.$t('everyXMonths', { count: everyXValue });
|
||||
} else if (task.frequency === 'yearly') {
|
||||
interval = everyXValue === 1 ? this.$t('everyYear') : this.$t('everyXYears', { count: everyXValue });
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
||||
let details = '';
|
||||
if (task.frequency === 'weekly') {
|
||||
const dayNames = {
|
||||
su: 'Sunday',
|
||||
m: 'Monday',
|
||||
t: 'Tuesday',
|
||||
w: 'Wednesday',
|
||||
th: 'Thursday',
|
||||
f: 'Friday',
|
||||
s: 'Saturday',
|
||||
};
|
||||
const activeDays = Object.keys(task.repeat || {}).filter(d => task.repeat[d]);
|
||||
if (activeDays.length > 0) {
|
||||
details = ` on ${activeDays.map(d => dayNames[d]).join(', ')}`;
|
||||
}
|
||||
} else if (task.frequency === 'monthly' && task.startDate) {
|
||||
const dayOfMonth = moment(task.startDate).date();
|
||||
if (task.weeksOfMonth && task.weeksOfMonth.length > 0) {
|
||||
const weekNum = task.weeksOfMonth[0] + 1;
|
||||
const weekStr = String(weekNum);
|
||||
const lastDigit = weekStr.slice(-1);
|
||||
let suffix = 'th';
|
||||
if (lastDigit === '1' && weekStr !== '11') suffix = 'st';
|
||||
if (lastDigit === '2' && weekStr !== '12') suffix = 'nd';
|
||||
if (lastDigit === '3' && weekStr !== '13') suffix = 'rd';
|
||||
const dayName = moment(task.startDate).format('dddd');
|
||||
details = ` on the ${weekNum}${suffix} ${dayName} of the month`;
|
||||
} else if (task.daysOfMonth && task.daysOfMonth.length > 0) {
|
||||
const dom = task.daysOfMonth[0];
|
||||
const domStr = String(dom);
|
||||
const lastDigit = domStr.slice(-1);
|
||||
let suffix = 'th';
|
||||
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
|
||||
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
|
||||
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
|
||||
details = ` on the ${dom}${suffix}`;
|
||||
} else {
|
||||
const domStr = String(dayOfMonth);
|
||||
const lastDigit = domStr.slice(-1);
|
||||
let suffix = 'th';
|
||||
if (lastDigit === '1' && domStr !== '11') suffix = 'st';
|
||||
if (lastDigit === '2' && domStr !== '12') suffix = 'nd';
|
||||
if (lastDigit === '3' && domStr !== '13') suffix = 'rd';
|
||||
details = ` on the ${dayOfMonth}${suffix}`;
|
||||
}
|
||||
} else if (task.frequency === 'yearly' && task.startDate) {
|
||||
details = ` on ${moment(task.startDate).format('MMMM Do')}`;
|
||||
}
|
||||
|
||||
return `${this.$t('repeats')} ${interval}${details}`;
|
||||
},
|
||||
schedulingWarning () {
|
||||
if (!this.task || this.task.type !== 'daily') return '';
|
||||
const { task } = this;
|
||||
if (task.frequency === 'monthly'
|
||||
&& task.weeksOfMonth && task.weeksOfMonth.length > 0
|
||||
&& task.weeksOfMonth[0] === 4
|
||||
&& task.startDate) {
|
||||
const dayName = moment(task.startDate).format('dddd');
|
||||
return this.$t('fifthWeekWarning', { day: dayName });
|
||||
}
|
||||
return '';
|
||||
},
|
||||
repeatsOn: {
|
||||
get () {
|
||||
let repeatsOn = 'dayOfMonth';
|
||||
@@ -1588,7 +1450,7 @@ export default {
|
||||
this.task.down = !this.task.down;
|
||||
},
|
||||
weekdaysMin (dayNumber) {
|
||||
return this.$t(`weekdaysMin${dayNumber}`);
|
||||
return moment.weekdaysMin(dayNumber);
|
||||
},
|
||||
formattedDate (date) {
|
||||
return moment(date).format('MM/DD/YYYY');
|
||||
|
||||
@@ -222,22 +222,14 @@ export default {
|
||||
return usernames;
|
||||
},
|
||||
summarySentence () {
|
||||
let fifthWeekWarning = '';
|
||||
if (this.task.type === 'daily' && this.task.frequency === 'monthly'
|
||||
&& this.task.weeksOfMonth && this.task.weeksOfMonth.length > 0
|
||||
&& this.task.weeksOfMonth[0] === 4) {
|
||||
const activeDays = keys(pickBy(this.task.repeat, value => value === true));
|
||||
const dayName = this.expandDayString[activeDays[0]];
|
||||
fifthWeekWarning = ` ${this.$t('fifthWeekWarning', { day: dayName })}`;
|
||||
}
|
||||
if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.${fifthWeekWarning}`;
|
||||
starting on <strong>${moment(this.task.startDate).format('MM/DD/YYYY')}</strong>.`;
|
||||
}
|
||||
if (this.task.type === 'daily') {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.${fifthWeekWarning}`;
|
||||
${this.formattedRepeatInterval(this.task.frequency, this.task.everyX)}${this.formattedDays(this.task.frequency, this.task.repeat, this.task.daysOfMonth, this.task.weeksOfMonth, this.task.startDate)}.`;
|
||||
}
|
||||
if (this.task.date) {
|
||||
return `This is ${this.formattedDifficulty(this.task.priority)} task that is due <strong>${moment(this.task.date).format('MM/DD/YYYY')}.`;
|
||||
@@ -295,14 +287,25 @@ export default {
|
||||
});
|
||||
dayStringArray.push('</strong>');
|
||||
} else if (weeksOfMonth.length > 0) {
|
||||
const weekNum = weeksOfMonth[0] + 1;
|
||||
const weekNumStr = String(weekNum);
|
||||
const lastDigit = weekNumStr.slice(-1);
|
||||
let ordinalSuffix = 'th';
|
||||
if (lastDigit === '1' && weekNumStr !== '11') ordinalSuffix = 'st';
|
||||
if (lastDigit === '2' && weekNumStr !== '12') ordinalSuffix = 'nd';
|
||||
if (lastDigit === '3' && weekNumStr !== '13') ordinalSuffix = 'rd';
|
||||
dayStringArray.push(`${weekNum}${ordinalSuffix}`);
|
||||
switch (weeksOfMonth[0]) {
|
||||
case 0:
|
||||
dayStringArray.push('first');
|
||||
break;
|
||||
case 1:
|
||||
dayStringArray.push('second');
|
||||
break;
|
||||
case 2:
|
||||
dayStringArray.push('third');
|
||||
break;
|
||||
case 3:
|
||||
dayStringArray.push('fourth');
|
||||
break;
|
||||
case 4:
|
||||
dayStringArray.push('fifth');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
activeDays = keys(pickBy(repeat, value => value === true));
|
||||
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`);
|
||||
}
|
||||
@@ -340,8 +343,9 @@ export default {
|
||||
if (numericX === 2) return '<strong>every other week</strong>';
|
||||
return `<strong>every ${numericX} weeks</strong>`;
|
||||
case 'monthly':
|
||||
if (numericX === 1) return `<strong>${this.$t('everyMonth')}</strong>`;
|
||||
return `<strong>${this.$t('everyXMonths', { count: numericX })}</strong>`;
|
||||
if (numericX === 1) return '<strong>every month</strong>';
|
||||
if (numericX === 2) return '<strong>every other month</strong>';
|
||||
return `<strong>every ${numericX} months</strong>`;
|
||||
case 'yearly':
|
||||
if (numericX === 1) return '<strong>every year</strong>';
|
||||
return `<strong>every ${everyX} years</strong>`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
|
||||
import Vue from 'vue';
|
||||
import getStore from '@/store';
|
||||
|
||||
const { AMPLITUDE_KEY } = import.meta.env;
|
||||
const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
|
||||
const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
|
||||
|
||||
let analyticsLoading = false;
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
// Vue plugin to globally expose a '$t' method that calls common/i18n.t.
|
||||
// Can be anywhere inside vue as 'this.$t' or '$t' in templates.
|
||||
|
||||
import moment from 'moment';
|
||||
import i18n from '@/../../common/script/i18n';
|
||||
|
||||
function loadLocale (i18nData) {
|
||||
// Load i18n strings
|
||||
i18n.strings = i18nData.strings;
|
||||
|
||||
// Load Moment.js locale
|
||||
const { language } = i18nData;
|
||||
|
||||
if (language && i18nData.momentLang && language.momentLangCode) {
|
||||
// Make moment available under `window` so that the locale can be set
|
||||
window.moment = moment;
|
||||
|
||||
// Execute the script and set the locale
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.text = i18nData.momentLang;
|
||||
head.appendChild(script);
|
||||
moment.updateLocale(language.momentLangCode);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
NavbarPlugin,
|
||||
CollapsePlugin,
|
||||
} from 'bootstrap-vue';
|
||||
import Fragment from 'vue-fragment';
|
||||
import AppComponent from './app';
|
||||
import { setUpLogging } from '@/libs/logging';
|
||||
import router from './router/index';
|
||||
@@ -43,6 +44,7 @@ Vue.use(FormRadioPlugin);
|
||||
Vue.use(TooltipPlugin);
|
||||
Vue.use(NavbarPlugin);
|
||||
Vue.use(CollapsePlugin);
|
||||
Vue.use(Fragment.Plugin);
|
||||
|
||||
setUpLogging();
|
||||
const store = getStore();
|
||||
|
||||
@@ -6,8 +6,9 @@ import { mapState } from '@/libs/store';
|
||||
import encodeParams from '@/libs/encodeParams';
|
||||
import notificationsMixin from '@/mixins/notifications';
|
||||
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
const { STRIPE_PUB_KEY } = import.meta.env;
|
||||
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
|
||||
|
||||
let stripeInstance = null;
|
||||
|
||||
@@ -206,6 +207,16 @@ export default {
|
||||
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
|
||||
throw checkoutSessionResult.error;
|
||||
}
|
||||
if (paymentType === 'groupPlan') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'group plan create',
|
||||
eventAction: 'group plan create',
|
||||
eventCategory: 'behavior',
|
||||
demographics: appState.newGroup.demographics,
|
||||
type: appState.newGroup.type,
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
|
||||
alert(`Error while redirecting to Stripe: ${err.message}`);
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import scoreTask from '@/../../common/script/ops/scoreTask';
|
||||
import notifications from './notifications';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
|
||||
|
||||
export default {
|
||||
@@ -57,6 +58,15 @@ export default {
|
||||
|
||||
const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT);
|
||||
if (!tasksScoredCount || tasksScoredCount < 2) {
|
||||
Analytics.track({
|
||||
eventName: 'task scored',
|
||||
eventAction: 'task scored',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
uuid: user._id,
|
||||
taskType: task.type,
|
||||
direction,
|
||||
}, { trackOnClient: true });
|
||||
if (!tasksScoredCount) {
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -90,7 +90,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content" v-if="allowedToChangeClass">
|
||||
<fragment v-if="allowedToChangeClass">
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -71,7 +71,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -55,7 +55,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -83,7 +83,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr>
|
||||
<td class="settings-label">
|
||||
{{ $t("showHeader") }}
|
||||
@@ -26,7 +26,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -67,7 +67,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-for="network in SOCIAL_AUTH_NETWORKS"
|
||||
:key="network.key"
|
||||
@@ -39,7 +39,7 @@
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -66,7 +66,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -111,7 +111,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -48,7 +48,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="d-content">
|
||||
<fragment>
|
||||
<tr
|
||||
v-if="!mixinData.inlineSettingMixin.modalVisible"
|
||||
>
|
||||
@@ -76,7 +76,7 @@
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</fragment>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
:class="{
|
||||
'casting-spell': castingSpell,
|
||||
}"
|
||||
@dragover.prevent
|
||||
>
|
||||
<!-- <banned-account-modal /> -->
|
||||
<amazon-payments-modal v-if="!isStaticPage" />
|
||||
@@ -131,6 +130,7 @@ import PrivacyBanner from '@/components/header/banners/privacy';
|
||||
import AppFooter from '@/components/appFooter';
|
||||
import notificationsDisplay from '@/components/notifications';
|
||||
import { mapState } from '@/libs/store';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import BuyModal from '@/components/shops/buyModal.vue';
|
||||
import SelectMembersModal from '@/components/selectMembersModal.vue';
|
||||
import notifications from '@/mixins/notifications';
|
||||
@@ -276,6 +276,7 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
Analytics.updateUser();
|
||||
return this.loadAllTranslations();
|
||||
}).then(() => {
|
||||
this.$store.state.isUserLoaded = true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import VueRouter from 'vue-router';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import getStore from '@/store';
|
||||
import handleRedirect from './handleRedirect';
|
||||
|
||||
@@ -10,56 +11,56 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
|
||||
|
||||
// NOTE: when adding a page make sure to implement the `common:setTitle` action
|
||||
|
||||
const Logout = () => import('@/components/auth/logout');
|
||||
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
|
||||
|
||||
// Hall
|
||||
const HallPage = () => import('@/components/hall/index');
|
||||
const PatronsPage = () => import('@/components/hall/patrons');
|
||||
const HeroesPage = () => import('@/components/hall/heroes');
|
||||
const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
|
||||
const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
|
||||
const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
|
||||
|
||||
// Admin Pages
|
||||
const AdminContainerPage = () => import('@/components/admin/container');
|
||||
const AdminPanelPage = () => import('@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import('@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import('@/components/admin/blocker');
|
||||
const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
|
||||
const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
|
||||
const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
|
||||
const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
|
||||
const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
|
||||
const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
|
||||
const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
|
||||
|
||||
// Tasks
|
||||
const UserTasks = () => import('@/components/tasks/user');
|
||||
const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
|
||||
|
||||
// Inventory
|
||||
const InventoryContainer = () => import('@/components/inventory/index');
|
||||
const ItemsPage = () => import('@/components/inventory/items/index');
|
||||
const EquipmentPage = () => import('@/components/inventory/equipment/index');
|
||||
const StablePage = () => import('@/components/inventory/stable/index');
|
||||
const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
|
||||
const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
|
||||
const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
|
||||
const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
|
||||
|
||||
// Guilds & Parties
|
||||
const GroupPage = () => import('@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import('@/components/static/groupPlans');
|
||||
const LookingForParty = () => import('@/components/groups/lookingForParty');
|
||||
const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
|
||||
const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
|
||||
const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
|
||||
|
||||
// Group Plans
|
||||
const GroupPlanIndex = () => import('@/components/group-plans/index');
|
||||
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import('@/components/group-plans/billing');
|
||||
const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
|
||||
const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
|
||||
const GroupPlanBilling = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/billing');
|
||||
|
||||
const MessagesIndex = () => import('@/pages/private-messages/index.vue');
|
||||
const MessagesIndex = () => import(/* webpackChunkName: "private-messages" */ '@/pages/private-messages/index.vue');
|
||||
|
||||
// Challenges
|
||||
const ChallengeIndex = () => import('@/components/challenges/index');
|
||||
const MyChallenges = () => import('@/components/challenges/myChallenges');
|
||||
const FindChallenges = () => import('@/components/challenges/findChallenges');
|
||||
const ChallengeDetail = () => import('@/components/challenges/challengeDetail');
|
||||
const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
|
||||
const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
|
||||
const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
|
||||
const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
|
||||
|
||||
// Shops
|
||||
const ShopsContainer = () => import('@/components/shops/index');
|
||||
const MarketPage = () => import('@/components/shops/market/index');
|
||||
const QuestsPage = () => import('@/components/shops/quests/index');
|
||||
const CustomizationsPage = () => import('@/components/shops/customizations/index');
|
||||
const SeasonalPage = () => import('@/components/shops/seasonal/index');
|
||||
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index');
|
||||
const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
|
||||
const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
|
||||
const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
|
||||
const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
|
||||
const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
|
||||
const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -317,6 +318,15 @@ router.beforeEach(async (to, from, next) => {
|
||||
router.app.$root.$emit('update-party');
|
||||
}
|
||||
|
||||
if (to.name === 'lookingForParty') {
|
||||
Analytics.track({
|
||||
hitType: 'event',
|
||||
eventName: 'View Find Members',
|
||||
eventAction: 'View Find Members',
|
||||
eventCategory: 'behavior',
|
||||
}, { trackOnClient: true });
|
||||
}
|
||||
|
||||
// Redirect old guild urls
|
||||
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
|
||||
const splits = to.hash.split('/');
|
||||
|
||||
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
|
||||
const OverviewPage = () => import('@/components/static/overview');
|
||||
const PressKitPage = () => import('@/components/static/pressKit');
|
||||
const PrivacyPage = () => import('@/components/static/privacy');
|
||||
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import('@/components/auth/registerUsername');
|
||||
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
|
||||
const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
|
||||
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
|
||||
const TermsPage = () => import('@/components/static/terms');
|
||||
|
||||
|
||||
@@ -101,7 +101,6 @@ export async function appleAuth (store, params) {
|
||||
id_token: params.idToken,
|
||||
name: params.name,
|
||||
username: params.username,
|
||||
email: params.email,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -110,10 +109,7 @@ export async function appleAuth (store, params) {
|
||||
}
|
||||
|
||||
if (result.data.message && result.data.id_token) {
|
||||
return {
|
||||
idToken: result.data.id_token,
|
||||
email: result.data.email,
|
||||
};
|
||||
return { idToken: result.data.id_token };
|
||||
}
|
||||
|
||||
const user = result.data.data;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import axios from 'axios';
|
||||
import Vue from 'vue';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
export async function getChat (store, payload) {
|
||||
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
|
||||
@@ -16,6 +17,13 @@ export async function postChat (store, payload) {
|
||||
url += `?previousMsg=${payload.previousMsg}`;
|
||||
}
|
||||
|
||||
if (group.type === 'party') {
|
||||
Analytics.updateUser({
|
||||
partyID: group.id,
|
||||
partySize: group.memberCount,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await axios.post(url, {
|
||||
message: payload.message,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import omit from 'lodash/omit';
|
||||
import findIndex from 'lodash/findIndex';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { loadAsyncResource } from '@/libs/asyncResource';
|
||||
|
||||
export async function getPublicGuilds (store, payload) {
|
||||
@@ -73,6 +74,7 @@ export async function join (store, payload) {
|
||||
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
|
||||
|
||||
user.party._id = groupId;
|
||||
Analytics.updateUser({ partyID: groupId });
|
||||
// load the party members so that they get shown in the header
|
||||
store.dispatch('party:getMembers');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import * as shops from './shops';
|
||||
import * as snackbars from './snackbars';
|
||||
import * as worldState from './worldState';
|
||||
import * as news from './news';
|
||||
import * as analytics from './analytics';
|
||||
import * as faq from './faq';
|
||||
import * as blockers from './blockers';
|
||||
|
||||
@@ -43,6 +44,7 @@ const actions = flattenAndNamespace({
|
||||
snackbars,
|
||||
worldState,
|
||||
news,
|
||||
analytics,
|
||||
faq,
|
||||
blockers,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
import axios from 'axios';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
|
||||
// export async function initQuest (store) {
|
||||
// }
|
||||
|
||||
export async function sendAction (store, payload) { // eslint-disable-line import/prefer-default-export, max-len
|
||||
// @TODO: Maybe move this to server
|
||||
let partyData = {};
|
||||
if (store.state.party && store.state.party.data) {
|
||||
partyData = {
|
||||
partyID: store.state.party.data._id,
|
||||
partySize: store.state.party.data.memberCount,
|
||||
};
|
||||
} else {
|
||||
partyData = {
|
||||
partyID: store.state.user.data.party._id,
|
||||
partySize: store.state.partyMembers.data.length,
|
||||
};
|
||||
}
|
||||
|
||||
Analytics.updateUser(partyData);
|
||||
|
||||
const response = await axios.post(`/api/v4/groups/${payload.groupId}/${payload.action}`);
|
||||
|
||||
// @TODO: Update user?
|
||||
|
||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
||||
import compact from 'lodash/compact';
|
||||
import omit from 'lodash/omit';
|
||||
import { loadAsyncResource } from '@/libs/asyncResource';
|
||||
import * as Analytics from '@/libs/analytics';
|
||||
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
|
||||
|
||||
export function fetchUserTasks (store, options = {}) {
|
||||
@@ -111,6 +112,15 @@ export async function create (store, createdTask) {
|
||||
}
|
||||
const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT);
|
||||
if (!tasksCreatedCount || tasksCreatedCount < 2) {
|
||||
const uuid = store.state.user.data._id;
|
||||
Analytics.track({
|
||||
eventName: 'task created',
|
||||
eventAction: 'task created',
|
||||
eventCategory: 'behavior',
|
||||
hitType: 'event',
|
||||
uuid,
|
||||
taskType: taskRes.type,
|
||||
}, { trackOnClient: true });
|
||||
if (!tasksCreatedCount) {
|
||||
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
|
||||
} else {
|
||||
@@ -158,15 +168,11 @@ export async function collapseChecklist (store, task) {
|
||||
}
|
||||
|
||||
export async function destroy (store, task) {
|
||||
const type = `${task.type}s`;
|
||||
const listIndex = store.state.tasks.data[type].findIndex(t => t._id === task._id);
|
||||
const orderIndex = store.state.user.data.tasksOrder[type].indexOf(task._id);
|
||||
const list = store.state.tasks.data[`${task.type}s`];
|
||||
const taskIndex = list.findIndex(t => t._id === task._id);
|
||||
|
||||
if (listIndex > -1) {
|
||||
store.state.tasks.data[type].splice(listIndex, 1);
|
||||
}
|
||||
if (orderIndex > -1) {
|
||||
store.state.user.data.tasksOrder[type].splice(orderIndex, 1);
|
||||
if (taskIndex > -1) {
|
||||
list.splice(taskIndex, 1);
|
||||
}
|
||||
|
||||
await axios.delete(`/api/v4/tasks/${task._id}`);
|
||||
|
||||
@@ -159,6 +159,10 @@ export default defineConfig({
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'^/analytics': {
|
||||
target: DEV_BASE_URL,
|
||||
changeOrigin: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
"backgroundStormyRooftopsNotes": "Промъквайте се върху буреносни покриви.",
|
||||
"backgroundWindyAutumnText": "Ветровита есен",
|
||||
"backgroundWindyAutumnNotes": "Гонете листа през ветровита есен.",
|
||||
"incentiveBackgrounds": "Стандартни фонове",
|
||||
"incentiveBackgrounds": "Комплект едноцветни фонове",
|
||||
"backgroundVioletText": "Виолетово",
|
||||
"backgroundVioletNotes": "Енергичен виолетов фон.",
|
||||
"backgroundBlueText": "Синьо",
|
||||
@@ -494,7 +494,7 @@
|
||||
"backgroundSnowglobeText": "Снежна топка",
|
||||
"backgroundDesertWithSnowNotes": "Бъди свидетел на рядката и мълчалива красота на Снежната пустиня.",
|
||||
"backgroundTeaPartyNotes": "Участвай в изискано Чаено парти.",
|
||||
"backgroundButterflyGardenNotes": "Купонясвайте с опрашители в градина за пеперуди",
|
||||
"backgroundButterflyGardenNotes": "Забавлявайте се с опрашителите в Градина на пеперудите",
|
||||
"backgroundAnimalCloudsText": "Животински облаци",
|
||||
"backgroundButterflyGardenText": "Градина на пеперудите",
|
||||
"backgroundWinterNocturneText": "Зимен ноктюрн",
|
||||
@@ -503,18 +503,5 @@
|
||||
"hideLockedBackgrounds": "Скрий заключените фонове",
|
||||
"backgroundSnowglobeNotes": "Разклати Снежната топка и заеми мястото си в микрокосмоса на снежния пейзаж.",
|
||||
"backgroundAmongGiantFlowersText": "Сред гигантски цветя",
|
||||
"backgroundAnimalCloudsNotes": "Използвай въображението си, за да намериш Животни в Облаците.",
|
||||
"backgroundSucculentGardenNotes": "",
|
||||
"backgroundSucculentGardenText": "Градина със сукуленти",
|
||||
"backgroundHotAirBalloonText": "горещ въздух балон",
|
||||
"backgroundHeatherFieldText": "пирен поле",
|
||||
"backgroundRainyBarnyardText": "Дъждовен фермерски двор",
|
||||
"backgroundRelaxationRiverText": "Релаксация Река",
|
||||
"backgroundFlyingOverGlacierNotes": "‐",
|
||||
"backgroundUnderwaterRuinsText": "Подводен Руини",
|
||||
"backgroundBeachCabanaText": "Плаж Кабана",
|
||||
"backgroundSaltLakeText": "Сол Езеро",
|
||||
"backgroundWintryCastleText": "Зимен Замък",
|
||||
"backgroundVikingShipText": "Викинг Кораб",
|
||||
"backgroundCampingOutText": "Къмпинг Навън"
|
||||
"backgroundAnimalCloudsNotes": "Използвай въображението си, за да намериш Животни в Облаците."
|
||||
}
|
||||
|
||||
@@ -162,8 +162,5 @@
|
||||
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!",
|
||||
"achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
|
||||
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!",
|
||||
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!",
|
||||
"achievementRodentRuler": "Vládce hlodavců",
|
||||
"achievementCats": "Pasák koček",
|
||||
"achievementDomesticated": "Hejá"
|
||||
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!"
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
"backgroundTavernNotes": "Navštiv krčmu města Habitica.",
|
||||
"backgrounds102015": "Sada 17: zveřejněna v říjnu 2015",
|
||||
"backgroundHarvestMoonText": "Měsíc při sklizni",
|
||||
"backgroundHarvestMoonNotes": "Chechtej se pod sklizňovým měsícem.",
|
||||
"backgroundHarvestMoonNotes": "Kdákání pod měsícem při sklizni.",
|
||||
"backgroundSlimySwampText": "Slizká bažina",
|
||||
"backgroundSlimySwampNotes": "Přebroď se slizkou bažinou.",
|
||||
"backgroundSwarmingDarknessText": "Valící se temnota",
|
||||
@@ -213,7 +213,7 @@
|
||||
"backgroundStormyRooftopsNotes": "Propliž se přes bouřlivé střechy.",
|
||||
"backgroundWindyAutumnText": "Větrný podzim",
|
||||
"backgroundWindyAutumnNotes": "Hoň se za listy během větrného podzimu.",
|
||||
"incentiveBackgrounds": "Standardní pozadí",
|
||||
"incentiveBackgrounds": "Prosté pozadí",
|
||||
"backgroundVioletText": "Fialová",
|
||||
"backgroundVioletNotes": "Živá fialová tapeta.",
|
||||
"backgroundBlueText": "Modrá",
|
||||
@@ -736,64 +736,7 @@
|
||||
"backgroundMaskMakersWorkshopNotes": "Vyzkoušej novou tvář v maskářově dílně.",
|
||||
"backgroundCemeteryGateText": "Hřbitovní brána",
|
||||
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány.",
|
||||
"backgroundAutumnBridgeText": "Most na podzim",
|
||||
"backgroundAutumnBridgeNotes": "Obdivuj krásu mostu na podzim.",
|
||||
"backgroundInsideACrystalText": "Uvnitř krystalu",
|
||||
"backgrounds032023": "Sada 106: Zveřejněna v březnu 2023",
|
||||
"backgroundOldTimeyBasketballCourtText": "Retro basketbalové hřiště",
|
||||
"backgroundOldTimeyBasketballCourtNotes": "Zaházej si na koš na retro basketbalovém hřišti.",
|
||||
"backgroundJungleWateringHoleText": "Napajedlo v džungli",
|
||||
"backgroundJungleWateringHoleNotes": "Zastav se na doušek u džunglového napajedla.",
|
||||
"backgroundMangroveForestText": "Mangrovový les",
|
||||
"backgroundMangroveForestNotes": "Prozkoumej okraj mangrovového lesa.",
|
||||
"backgrounds052023": "Sada 108: Zveřejněna v květnu 2023",
|
||||
"backgroundInAPaintingText": "V obraze",
|
||||
"backgroundFlyingOverHedgeMazeText": "Let nad labyrintem ze živého plotu",
|
||||
"backgroundFlyingOverHedgeMazeNotes": "Žasněte při letu nad labyrintem ze živého plotu.",
|
||||
"backgroundCretaceousForestText": "Křídový les",
|
||||
"backgroundCretaceousForestNotes": "Vychutnejte si pradávnou zeleň křídového lesa.",
|
||||
"backgroundLeafyTreeTunnelNotes": "Procházejte se tunelem z listnatých stromů.",
|
||||
"backgroundSpringtimeShowerText": "Jarní přeháňka",
|
||||
"backgroundSpringtimeShowerNotes": "Podívejte se na květnatou jarní přeháňku.",
|
||||
"backgroundUnderWisteriaText": "Pod vistérií",
|
||||
"backgrounds022023": "SADA 105: Vydáno v únoru 2023",
|
||||
"backgroundInFrontOfFountainText": "Před Fontánou",
|
||||
"backgroundInFrontOfFountainNotes": "Procházej se před Fontánou.",
|
||||
"backgroundGoldenBirdcageText": "Zlatá klec",
|
||||
"backgroundGoldenBirdcageNotes": "Schovej se v zlaté kleci.",
|
||||
"backgroundFancyBedroomText": "Luxusní ložnice",
|
||||
"backgroundFancyBedroomNotes": "Dopřej si luxus v luxusní ložnici.",
|
||||
"backgrounds042023": "Sada 107: Zveřejněna v dubnu 2023",
|
||||
"backgroundLeafyTreeTunnelText": "Tunel z listnatých stromů",
|
||||
"backgroundUnderWisteriaNotes": "Odpočiňte si pod vistérií.",
|
||||
"backgroundInAPaintingNotes": "Užijte si kreativní činnosti uvnitř obrazu.",
|
||||
"backgrounds012023": "SADA 104: Vydáno v lednu 2023",
|
||||
"backgroundRimeIceText": "Jinovatka",
|
||||
"backgroundRimeIceNotes": "Pokochej se třpytivou jinovatkou.",
|
||||
"backgroundSnowyTempleText": "Zasněžený chrám",
|
||||
"backgroundSnowyTempleNotes": "Pokochej se klidným zasněženým chrámem.",
|
||||
"backgroundWinterLakeWithSwansText": "Zimní jezero s labutěmi",
|
||||
"backgroundWinterLakeWithSwansNotes": "Užij si přírodu u zimního jezera s labutěmi.",
|
||||
"backgrounds122022": "SADA 103: Vydáno v prosinci 2022",
|
||||
"backgroundBranchesOfAHolidayTreeText": "Větve svátečního stromku",
|
||||
"backgroundBranchesOfAHolidayTreeNotes": "Dováděj na větvích svátečního stromku.",
|
||||
"backgroundInsideACrystalNotes": "Vyhlédni z nitra krystalu.",
|
||||
"backgroundSnowyVillageText": "Zasněžená vesnice",
|
||||
"backgroundSnowyVillageNotes": "Pokochej se zasněženou vesnicí.",
|
||||
"backgrounds062023": "Sada 109: Zveřejněna v červnu 2023",
|
||||
"backgroundInAnAquariumText": "V akváriu",
|
||||
"backgroundInAnAquariumNotes": "Zaplavejte si poklidně s rybkami v akváriu.",
|
||||
"backgroundInsideAdventurersHideoutText": "V úkrytu dobrodruhů",
|
||||
"backgroundInsideAdventurersHideoutNotes": "Naplánujte cestu v úkrytu dobrodruhů.",
|
||||
"backgroundCraterLakeText": "Kráterové jezero",
|
||||
"backgroundCraterLakeNotes": "Obdivujte nádherné kráterové jezero.",
|
||||
"backgrounds072023": "Sada 110: Zveřejněna v červenci 2023",
|
||||
"backgroundOnAPaddlewheelBoatText": "Na loďce s lopatkovým kolem",
|
||||
"backgroundOnAPaddlewheelBoatNotes": "Projet se na loďce s lopatkovým kolem.",
|
||||
"backgroundColorfulCoralText": "Barevný korál",
|
||||
"backgroundColorfulCoralNotes": "Potopte se mezi barevné korály.",
|
||||
"backgrounds082023": "Sada 111: zveřejněaa v srpnu 2023",
|
||||
"backgroundBonsaiCollectionText": "Sbírka bonsají",
|
||||
"backgroundBoardwalkIntoSunsetNotes": "Vydejte se po Stezce do západu slunce.",
|
||||
"backgroundBoardwalkIntoSunsetText": "Stezka do západu slunce"
|
||||
"backgroundAutumnBridgeText": "Podzimní most",
|
||||
"backgroundAutumnBridgeNotes": "Obdivuj krásu podzimního mostu.",
|
||||
"backgroundInsideACrystalText": "Uvnitř krystalu."
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"brokenChaLink": "Nefunkční odkaz na výzvu",
|
||||
"keepIt": "Ponechat",
|
||||
"removeIt": "Odstranit",
|
||||
"brokenChallenge": "Neplatný odkaz na výzvu",
|
||||
"brokenChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale ta (nebo skupina, která ji vytvořila) byla odstraněna. Co chceš dělat s osiřelými úkoly?",
|
||||
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%= user %></span>! Co chceš dělat s osiřelými úkoly?",
|
||||
"unsubChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ze které jsi se odhlásil/a. Co chceš dělat s osiřelými úkoly?",
|
||||
"challenges": "Výzvy",
|
||||
@@ -85,7 +85,7 @@
|
||||
"summaryRequired": "Je požadováno shrnutí",
|
||||
"summaryTooLong": "Shrnutí je příliš dlouhé",
|
||||
"descriptionRequired": "Je požadován popis",
|
||||
"locationRequired": "Je nutné vybrat umístění výzvy („Přidat do“)",
|
||||
"locationRequired": "Je požadováno vybrat lokaci výzvy ('Přidat k')",
|
||||
"categoiresRequired": "Musí být vybrána jedna nebo více kategorií",
|
||||
"viewProgressOf": "Zobrazit pokrok",
|
||||
"viewProgress": "Zobrazit pokrok",
|
||||
@@ -94,16 +94,5 @@
|
||||
"selectParticipant": "Zvol účastníka",
|
||||
"filters": "Filtry",
|
||||
"wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.",
|
||||
"yourReward": "Tvá odměna",
|
||||
"brokenTaskDescription": "Tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš udělat?",
|
||||
"brokenChallengeDescription": "Tento úkol byl součástí výzvy, ale výzva (nebo skupina) byla smazána. Co chceš udělat s osiřelými úkoly?",
|
||||
"challengeCompletedDescription": "Vítězem je <%= user %>! Co chceš udělat s osiřelými úkoly?",
|
||||
"messageChallengeFlagAlreadyReported": "Tuto výzvu jsi už nahlásil.",
|
||||
"flaggedNotHidden": "Výzva byla nahlášena jednou, není skrytá",
|
||||
"flaggedAndHidden": "Výzva byla nahlášena a je skrytá",
|
||||
"resetFlagCount": "Resetovat počet nahlášení",
|
||||
"deleteChallengeRefundDescription": "Pokud tuto výzvu smažeš, bude ti vrácena odměna v drahokamech a úkoly z výzvy zůstanou na nástěnkách úkolů účastníků.",
|
||||
"messageChallengeFlagOfficial": "Oficiální výzvy nelze nahlásit.",
|
||||
"brokenTask": "Nefunkční odkaz na výzvu",
|
||||
"removeTasks": "Odstranit Úkoly"
|
||||
"yourReward": "Tvá odměna"
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
"battleGear": "Bojová výzbroj",
|
||||
"gear": "Výbava",
|
||||
"autoEquipBattleGear": "Automaticky použít nové vybavení",
|
||||
"costume": "kostým",
|
||||
"costume": "Kostým",
|
||||
"useCostume": "Použít kostým",
|
||||
"costumePopoverText": "Vyber \"Použít kostým\", abys vybavil svého avatara, aniž bys nějak ovlivnil statistiky tvé bojové výzbroje! To znamená, že můžeš obléct svého avatara do jakéhokoliv vybavení chceš a stále mít tvojí nejlepší bojovou výzbroj na sobě.",
|
||||
"autoEquipPopoverText": "Zvol tuto možnost pro automatické nasazení koupeného vybavení.",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stable": "Mazlíčci a Mounty",
|
||||
"stable": "Stáj",
|
||||
"pets": "Mazlíčci",
|
||||
"activePet": "Aktivní mazlíček",
|
||||
"noActivePet": "Bez aktivního mazlíčka",
|
||||
@@ -109,7 +109,5 @@
|
||||
"wackyPets": "Šílená zvířátka",
|
||||
"invalidAmount": "Neplatný počet jídla,je vyžadováno pozitivní celé číslo",
|
||||
"tooMuchFood": "Snažíš se dát svému zvířeti moc jídla, akce byla zrušena",
|
||||
"notEnoughFood": "Nemáš dost jídla",
|
||||
"veteranCactus": "Kaktus Veterán",
|
||||
"veteranDragon": "Drak Veterán"
|
||||
"notEnoughFood": "Nemáš dost jídla"
|
||||
}
|
||||
|
||||
@@ -160,25 +160,5 @@
|
||||
"newPMNotificationTitle": "Nová zpráva od <%= name %>",
|
||||
"displaynameIssueNewline": "Zobrazovaná jména nesmí obsahovat zpětné lomítko následované písmenem N.",
|
||||
"resetAccount": "Resetovat účet",
|
||||
"giftedSubscriptionWinterPromo": "Ahoj <%= username %>, získal/a jsi <%= monthCount %> měsíce/ů předplatného jako součást naší sváteční dárkové akce!",
|
||||
"generalSettings": "Hlavní nastavení",
|
||||
"siteData": "Údaje o webu",
|
||||
"taskSettings": "Nastavení úkolu",
|
||||
"confirmCancelChanges": "Jste si jistí? Neuložené změny přijdou vniveč.",
|
||||
"account": "Účet",
|
||||
"loginMethods": "Možnosti přihlášení",
|
||||
"character": "Postava",
|
||||
"siteLanguage": "Jazyk webu",
|
||||
"showLevelUpModal": "Při dosažení vyšší úrovně",
|
||||
"showHatchPetModal": "Při odchovu zvířátka",
|
||||
"showRaisePetModal": "Jak z domácího mazlíčka vychovat jízdní zvíře",
|
||||
"showStreakModal": "Při dosažení úspěchu v sérii",
|
||||
"baileyAnnouncement": "Nejnovější oznámení společnosti Bailey",
|
||||
"view": "Zobrazit",
|
||||
"feedbackPlaceholder": "Vlož zpětnou vazbu",
|
||||
"downloadCSV": "Stáhni si CSV",
|
||||
"downloadAs": "Ulož jako",
|
||||
"yourUserData": "Tvá uživatelská data",
|
||||
"taskHistory": "Historie",
|
||||
"yourUserDataDisclaimer": "Zde si lze stáhnout výpis historie úkolů nebo kompletní uživatelská data."
|
||||
"giftedSubscriptionWinterPromo": "Ahoj <%= username %>, získal/a jsi <%= monthCount %> měsíce/ů předplatného jako součást naší sváteční dárkové akce!"
|
||||
}
|
||||
|
||||
@@ -935,8 +935,5 @@
|
||||
"backgroundWaterfallWithRainbowText": "Wasserfall mit Regenbogen",
|
||||
"backgroundWaterfallWithRainbowNotes": "Bewundere die atemberaubende Schönheit eines Wasserfalls mit Regenbogen.",
|
||||
"backgrounds042026": "SET 143: Veröffentlicht im April 2026",
|
||||
"backgrounds052026": "SET 144: Veröffentlicht im Mai 2026",
|
||||
"backgroundRidingACometText": "Ein Kometenritt",
|
||||
"backgroundRidingACometNotes": "Reise durch das All bei einem Kometenritt!",
|
||||
"backgroundElvenCitadelText": "Elven Citadel"
|
||||
"backgrounds052026": "SET 144: Veröffentlicht im Mai 2026"
|
||||
}
|
||||
|
||||
@@ -3484,7 +3484,7 @@
|
||||
"shieldSpecialWinter2026WarriorText": "Raureif Schild",
|
||||
"shieldSpecialWinter2026WarriorNotes": "Stoppe eiskalt Hindernisse mit diesem praktischen, pieksigen Schild. Erhöht Ausdauer um %= con %>. Limitierte Ausgabe Winterausrüstung 2025-2026.",
|
||||
"headMystery202602Text": "Kirschblüte Fuchsohren",
|
||||
"headMystery202602Notes": "Diese Ohren schärfen dein Gehör so sehr, dass du im nahenden Frühling das Wachsen der Blütenknospen an den Zweigen der Bäume hören kannst. Gewährt keinen Attributbonus. Februar 2026 Abonnentengegenstand.",
|
||||
"headMystery202602Notes": " Diese Ohren schärfen dein Gehör so sehr, dass du im nahenden Frühling das Wachsen der Blütenknospen an den Zweigen der Bäume hören kannst. Gewährt keinen Attributbonus. Februar 2026 Abonnentengegenstand.",
|
||||
"headArmoireLoneCowpokeHatNotes": "Howdy Kumpel! Hasst du’s auch so, wenn du draußen auf dem Schießstand bist, an Aufgaben arbeitest und dir die Sonne in die Augen scheint? Also, gute Sache, dass du dafür jetzt ’nen Hut hast. Erhöht deine Wahrnehmung um <%= per %>. Verzauberter Schrank: Einsamer Cowboy Set (Item 1 of 2)",
|
||||
"shieldSpecialWinter2026HealerText": "Sternenexplosion",
|
||||
"shieldArmoireDoubleBassNotes": "Bom doo bom brrrr brr brr brrrr! Versammle deine Party, um euch zu erden oder zu tanzen, während ihr euch Musik von dieser tiefen Double Bass anhört. Erhört Ausdauer und Stärke um jeweils <%= attrs %>. Verzauberter Schwank: Musikinstrumente Set 2 (Gegenstand 3 von 3)",
|
||||
@@ -3495,11 +3495,5 @@
|
||||
"backMystery202601Notes": "Dieses Zeichen gewährt dem Anwender die Kontrolle über die Elemente der Jahreszeit von Kälte und Frost. Gewährt keinen Attributbonus. Januar 2026 Abonnentengegenstand.",
|
||||
"backMystery202602Text": "Fünf Schweife der Sakura",
|
||||
"backMystery202602Notes": "Diese flauschigen Schweife haben die Farbe der Kirschblüte, eine Erinnerung, dass der Frühling auf dem Weg ist. Gewährt keinen Autobusbonus. Februar 2026 Abonnentengegenstand.",
|
||||
"backArmoireHarpsichordText": "Cembalo",
|
||||
"weaponSpecialSpring2026HealerText": "Schneeglöckchen Stab",
|
||||
"weaponSpecialSpring2026HealerNotes": "Eine Gelegenheit für einen Neuanfang liegt direkt vor dir, und mit diesem prächtigen Stab wirst du bereit sein! Erhöht Intelligenz um <%= int %>. Limitierte Ausgabe Frühlingsausrüstung 2026.",
|
||||
"armorSpecialSpring2026WarriorText": "Froschrüstung",
|
||||
"armorSpecialSpring2026WarriorNotes": "Hüpf in Aktion, sobald der Schnee taut. Erhöht Ausdauer um <%= con %>. Limitierte Ausgabe Frühlingsausrüstung 2026.",
|
||||
"armorSpecialSpring2026RogueText": "Birkenrinde Rüstung",
|
||||
"armorSpecialSpring2026RogueNotes": "Trotze dem unvermeidlichen Frühlingsregen ebenso wie leichten Brisen. Erhöht Wahrnehmung um <%= per %>. Limitierte Ausgabe Frühlingsausrüstung 2026."
|
||||
"backArmoireHarpsichordText": "Cembalo"
|
||||
}
|
||||
|
||||
@@ -11,11 +11,5 @@
|
||||
"rebirthPop": "Beginne sofort von vorn mit einem Charakter auf Level 1, aber behalte Erfolge, Sammelgegenstände und Ausrüstung. Deine Aufgaben und ihre Verläufe bleiben erhalten, werden aber auf gelb zurückgesetzt. Deine Strähnen verfallen, außer für Aufgaben, die von aktiven Herausforderungen oder Gruppenplänen stammen. Gold, Erfahrung, Mana und alle Effekte von Fähigkeiten werden entfernt. All das wird sofort in Kraft treten.",
|
||||
"rebirthName": "Sphäre der Wiedergeburt",
|
||||
"rebirthComplete": "Du wurdest wiedergeboren!",
|
||||
"nextFreeRebirth": "<strong><%= days %> Tage</strong> bis zur <strong>KOSTENLOSEN</strong> Sphäre der Wiedergeburt",
|
||||
"rebirthUnlockedNewItem": "Ort der Wiedergeburt Freigeschaltet",
|
||||
"rebirthUnlockedOrb": "Ein neues Abendteuer ist bverfügbar!",
|
||||
"rebirthUnlockedDesc": "Nutze den Ort der Wiedergeburt um ein neues Leben in dein Habitica Abendteuer zu bekommen wenn du das Gefühl hast, alles erreicht zu haben. Du beginnst wieder bei Level 1 und es beginnt wieder von vorne.",
|
||||
"rebirthNewAchievement": "Neue Auszeichnung",
|
||||
"rebirthNewAdventure": "Ein neues Abendteuer beginnt nun!",
|
||||
"rebirthStackInfo": "Diese Auszeichnung kann sich stapeln, jedes Mal, wenn du den Ort der Wiedergeburt nutzt."
|
||||
"nextFreeRebirth": "<strong><%= days %> Tage</strong> bis zur <strong>KOSTENLOSEN</strong> Sphäre der Wiedergeburt"
|
||||
}
|
||||
|
||||
@@ -1075,18 +1075,6 @@
|
||||
"backgroundElvenCitadelText": "Elven Citadel",
|
||||
"backgroundElvenCitadelNotes": "Take the scenic journey to an Elven Citadel.",
|
||||
|
||||
"backgrounds062026": "SET 145: Released June 2026",
|
||||
"backgroundBeachWithVolcanoText": "Beach with Volcano",
|
||||
"backgroundBeachWithVolcanoNotes": "Watch nature's wonder on a Beach with a Volcano.",
|
||||
|
||||
"backgrounds072026": "SET 146: Released July 2026",
|
||||
"backgroundTropicalCoralGardenText": "Tropical Coral Garden",
|
||||
"backgroundTropicalCoralGardenNotes": "Dive into a Tropical Coral Garden.",
|
||||
|
||||
"backgrounds082026": "SET 147: Released August 2026",
|
||||
"backgroundVegetableGardenText": "Vegetable Garden",
|
||||
"backgroundVegetableGardenNotes": "Plant tasty greens in a Vegetable Garden.",
|
||||
|
||||
"timeTravelBackgrounds": "Steampunk Backgrounds",
|
||||
"backgroundAirshipText": "Airship",
|
||||
"backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.",
|
||||
|
||||
@@ -187,7 +187,5 @@
|
||||
"learnMore": "Learn More",
|
||||
"translateHabitica": "Translate Habitica",
|
||||
"whatToCallYou": "What should we call you?",
|
||||
"acceptPrivacyTOS": "You confirm that you are at least 18 years old, and that you have read and agree to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/privacy' target='_blank'>Privacy Policy</a>",
|
||||
"emailAddress": "Email address",
|
||||
"emailRequiredForSupport": "We require an email address for user support. Please enter an email address to continue creating your account."
|
||||
"acceptPrivacyTOS": "You confirm that you are at least 18 years old, and that you have read and agree to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/privacy' target='_blank'>Privacy Policy</a>"
|
||||
}
|
||||
|
||||
@@ -587,15 +587,6 @@
|
||||
"weaponSpecialSpring2026MageText": "Maypole Parasol",
|
||||
"weaponSpecialSpring2026MageNotes": "An opportunity to celebrate approaches, and with this pretty parasol pole, you will be ready! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"weaponSpecialSummer2026WarriorText": "Gator Machete",
|
||||
"weaponSpecialSummer2026WarriorNotes": "This flashy, fancy weapon fits right into your swampcore aesthetic. Increases Strength by <%= str %>. Limited Edition Summer 2026 Gear.",
|
||||
"weaponSpecialSummer2026RogueText": "Tsunami Blade",
|
||||
"weaponSpecialSummer2026RogueNotes": "This clever, curvy weapon fits right into your seacore aesthetic. Increases Strength by <%= str %>. Limited Edition Summer 2026 Gear.",
|
||||
"weaponSpecialSummer2026HealerText": "Puffin Lance",
|
||||
"weaponSpecialSummer2026HealerNotes": "This fine, feather-adorned weapon fits right into your islandcore aesthetic. Increases Intelligence by <%= int %>. Limited Edition Summer 2026 Gear.",
|
||||
"weaponSpecialSummer2026MageText": "Tiger Shark Spear",
|
||||
"weaponSpecialSummer2026MageNotes": "This dangerous, double-ended weapon fits right into your oceancore aesthetic. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition Summer 2026 Gear.",
|
||||
|
||||
"weaponMystery201411Text": "Pitchfork of Feasting",
|
||||
"weaponMystery201411Notes": "Stab your enemies or dig in to your favorite foods - this versatile pitchfork does it all! Confers no benefit. November 2014 Subscriber Item.",
|
||||
"weaponMystery201502Text": "Shimmery Winged Staff of Love and Also Truth",
|
||||
@@ -646,10 +637,6 @@
|
||||
"weaponMystery202601Notes": "An icy bubble shield that grants magical protection from opposing elements. Confers no benefit. January 2026 Subscriber Item.",
|
||||
"weaponMystery202603Text": "Wisteria Wizard Staff",
|
||||
"weaponMystery202603Notes": "Cast spells to warm the spring air and encourage the blossoms to bud! Confers no benefit. March 2026 Subscriber Item.",
|
||||
"weaponMystery202607Text": "Oceanmancer's Fishy Familiars",
|
||||
"weaponMystery202607Notes": "These colorful companions will channel your aqueous abilities. Confers no benefit. July 2026 Subscriber Item.",
|
||||
"weaponMystery202608Text": "Beaming Magenta Blade",
|
||||
"weaponMystery202608Notes": "Bright, beautiful, dangerous to your undone Dailies. Confers no benefit. August 2026 Subscriber Item.",
|
||||
|
||||
"weaponMystery301404Text": "Steampunk Cane",
|
||||
"weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.",
|
||||
@@ -878,14 +865,6 @@
|
||||
"weaponArmoireBambooFluteNotes": "Hwhoooo! Hu-whooooo! Gather your party for a meditation session or self-care nap while relaxing to tunes played on this bamboo flute. Increases Constitution and Intelligence by <%= attrs %> each. Enchanted Armoire: Musical Instrument Set 2 (Item 2 of 3)",
|
||||
"weaponArmoirePrettyPinkParasolText": "Pretty Pink Parasol",
|
||||
"weaponArmoirePrettyPinkParasolNotes": "Pretty and practical is the preeminent permutation. And for a particularly impressive presentation, give this parasol a spin! Increases all stats by <%= attrs %> each. Enchanted Armoire: Pretty in Pink Set (Item 1 of 2)",
|
||||
"weaponArmoireBrightRainbowKiteText": "Rainbow Kite",
|
||||
"weaponArmoireBrightRainbowKiteNotes": "This kite’s colors are bright and loud. Watching it soar high will make you proud! Increases all stats by <%= attrs %> each. Enchanted Armoire: Rainbow Kite Set (Item 1 of 2).",
|
||||
"weaponArmoirePastelRainbowKiteText": "Pastel Rainbow Kite",
|
||||
"weaponArmoirePastelRainbowKiteNotes": "This kite’s colors are muted and soft. It dances and spins as it soars aloft! Increases all stats by <%= attrs %> each. Enchanted Armoire: Rainbow Kite Set (Item 2 of 2).",
|
||||
"weaponArmoireKendoShinaiText": "Kendo Shinai",
|
||||
"weaponArmoireKendoShinaiNotes": "Light and soft, you can use this bamboo practice sword as you strive to improve yourself. Increases Strength by <%= str %>. Enchanted Armoire: Kendo Set (Item 3 of 3).",
|
||||
"weaponArmoireGardenRakeText": "Garden Rake",
|
||||
"weaponArmoireGardenRakeNotes": "Step 1: Rake all the fallen leaves into a giant pile. Step 2: Celebrate a job well done by jumping into the pile. Step 3: Repeat. Increases Constitution by <%= con %>. Enchanted Armoire: Gardener Set 2 (Item 1 of 2).",
|
||||
|
||||
"armor": "armor",
|
||||
"armorCapitalized": "Armor",
|
||||
@@ -1453,15 +1432,6 @@
|
||||
"armorSpecialSpring2026MageText": "Maypole Dancer Outfit",
|
||||
"armorSpecialSpring2026MageNotes": "Arrive ready to dance, picnic, and enjoy the warm weather spring brings. Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"armorSpecialSummer2026WarriorText": "Gator Suit",
|
||||
"armorSpecialSummer2026WarriorNotes": "Conceal yourself in this suit, but don’t hide from your problems. Gather your gator grit and meet your tasks like the alligator you are. Increases Constitution by <%= con %>. Limited Edition Summer 2026 Gear.",
|
||||
"armorSpecialSummer2026RogueText": "Tsunami Suit",
|
||||
"armorSpecialSummer2026RogueNotes": "Cloak yourself in this tsunami suit, but don’t hide from your problems. Summon a strong storm to have your back and meet your tasks like the adventurer you are. Increases Perception by <%= per %>. Limited Edition Summer 2026 Gear.",
|
||||
"armorSpecialSummer2026HealerText": "Puffin Suit",
|
||||
"armorSpecialSummer2026HealerNotes": "Fit yourself in this suit, but don’t hide from your problems. Produce your puffin power and tackle your tasks like the puffin you are. Increases Constitution by <%= con %>. Limited Edition Summer 2026 Gear.",
|
||||
"armorSpecialSummer2026MageText": "Tiger Shark Suit",
|
||||
"armorSpecialSummer2026MageNotes": "Slide into this suit, but don’t hide from your problems. Show your shark shine and swim right up to face those tasks like the shark you are. Increases Intelligence by <%= int %>. Limited Edition Summer 2026 Gear.",
|
||||
|
||||
"armorMystery201402Text": "Messenger Robes",
|
||||
"armorMystery201402Notes": "Shimmering and strong, these robes have many pockets to carry letters. Confers no benefit. February 2014 Subscriber Item.",
|
||||
"armorMystery201403Text": "Forest Walker Armor",
|
||||
@@ -1856,8 +1826,6 @@
|
||||
"armorArmoireSoftYellowSuitNotes": "Yellow is an energetic color. Wear this to bed, and you will wake up with the sun the next morning ready to tackle a day full of tasks. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 2 of 3).",
|
||||
"armorArmoireHandstandOutfitText": "Handstand",
|
||||
"armorArmoireHandstandOutfitNotes": "Things sure do look different when you’re upside-down, don’t they? If you’re feeling stuck, it’s time for a fresh perspective! Increases Perception by <%= per %>. Enchanted Armoire: Handstand Set (Item 1 of 1).",
|
||||
"armorArmoireKendoBoguText": "Kendo Bōgu",
|
||||
"armorArmoireKendoBoguNotes": "This might be training armor, but it offers more than enough protection for your path ahead. Increases Constitution by <%= con %>. Enchanted Armoire: Kendo Set (Item 2 of 3).",
|
||||
|
||||
"headgear": "helm",
|
||||
"headgearCapitalized": "Headgear",
|
||||
@@ -2419,15 +2387,6 @@
|
||||
"headSpecialSpring2026MageText": "Mayflower Crown",
|
||||
"headSpecialSpring2026MageNotes": "Make a joyous statement with bright blooms encircling your head. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"headSpecialSummer2026WarriorText": "Gator Helm",
|
||||
"headSpecialSummer2026WarriorNotes": "Go forth and be productive! If you get any pushback, just snap back and show your sharp teeth. Increases Strength by <%= str %>. Limited Edition Summer 2026 Gear.",
|
||||
"headSpecialSummer2026RogueText": "Tsunami Helm",
|
||||
"headSpecialSummer2026RogueNotes": "Go forth and be productive! If you lose your way, just follow the flow. Increases Perception by <%= per %>. Limited Edition Summer 2026 Gear.",
|
||||
"headSpecialSummer2026HealerText": "Puffin Helm",
|
||||
"headSpecialSummer2026HealerNotes": "Go forth and be productive! If you encounter complications, just gather them up in your colorful beak and take them somewhere else. Increases Intelligence by <%= int %>. Limited Edition Summer 2026 Gear.",
|
||||
"headSpecialSummer2026MageText": "Tiger Shark Helm",
|
||||
"headSpecialSummer2026MageNotes": "Go forth and be productive! If an obstacle dares to get in your way, just crush it with your mighty jaws. Increases Perception by <%= per %>. Limited Edition Summer 2026 Gear.",
|
||||
|
||||
"headSpecialGaymerxText": "Rainbow Warrior Helm",
|
||||
"headSpecialGaymerxNotes": "In celebration of the GaymerX Conference, this special helmet is decorated with a radiant, colorful rainbow pattern! GaymerX is a game convention celebrating LGTBQ and gaming and is open to everyone.",
|
||||
|
||||
@@ -2619,8 +2578,6 @@
|
||||
"headMystery202603Notes": "This jaunty hat not only enhances your magical ability, it also has a lovely spring scent! Confers no benefit. March 2026 Subscriber Item.",
|
||||
"headMystery202604Text": "Audacious Astronaut Helmet",
|
||||
"headMystery202604Notes": "In space, no one can hear you check off your To Do’s. But the real reward is your sense of personal accomplishment! Confers no benefit. April 2026 Subscriber Item.",
|
||||
"headMystery202606Text": "Holiday Hat",
|
||||
"headMystery202606Notes": "Holidays are made for enjoying the sunshine - but don’t get burned! Confers no benefit. June 2026 Subscriber Item.",
|
||||
|
||||
"headMystery301404Text": "Fancy Top Hat",
|
||||
"headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.",
|
||||
@@ -2855,8 +2812,6 @@
|
||||
"headArmoireFloppyYellowHatNotes": "Many spells have been sewn into this simple hat, giving it a youthful yellow color. Increases all stats by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 1 of 3).",
|
||||
"headArmoireVerdantArmingCapText": "Verdant Page Arming Cap",
|
||||
"headArmoireVerdantArmingCapNotes": "This comfy, cushioned coif makes you battle-ready and helps you withstand anything heavy that could come your way. Increases Perception and Constitution by <%= attrs %> each. Enchanted Armoire: Verdant Page Set (Item 1 of 2).",
|
||||
"headArmoireKendoMenText": "Kendo Men",
|
||||
"headArmoireKendoMenNotes": "You might be surprised by how well you can see through the grille as you follow the way of the sword. Increases Perception by <%= per %>. Enchanted Armoire: Kendo Set (Item 1 of 3).",
|
||||
|
||||
"offhand": "off-hand item",
|
||||
"offHandCapitalized": "Off-Hand Item",
|
||||
@@ -3181,11 +3136,6 @@
|
||||
"shieldSpecialSpring2026HealerText": "Snowdrop Leaf",
|
||||
"shieldSpecialSpring2026HealerNotes": "Create a light breeze with this fan as the days grow warmer. It doubles as a writing utensil in a pinch. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.",
|
||||
|
||||
"shieldSpecialSummer2026WarriorText": "Gator Shield",
|
||||
"shieldSpecialSummer2026WarriorNotes": "Deflect oncoming challenges with this stylish, shiny shield. And when you’ve successfully cleared your list, crank up the music and have a party! Increases Constitution by <%= con %>. Limited Edition Summer 2026 Gear.",
|
||||
"shieldSpecialSummer2026HealerText": "Puffin Potion",
|
||||
"shieldSpecialSummer2026HealerNotes": "Keep your colony of fellow puffins healthy with this potion. It tastes great with fish! Increases Constitution by <%= con %>. Limited Edition Summer 2026 Gear.",
|
||||
|
||||
"shieldMystery201601Text": "Resolution Slayer",
|
||||
"shieldMystery201601Notes": "This blade can be used to parry away all distractions. Confers no benefit. January 2016 Subscriber Item.",
|
||||
"shieldMystery201701Text": "Time-Freezer Shield",
|
||||
@@ -3218,12 +3168,6 @@
|
||||
"shieldMystery202511Notes": "This rugged shield of icy rock protects you from bad Habits but won't freeze your hands. Confers no benefit. November 2025 Subscriber Item.",
|
||||
"shieldMystery202605Text": "Nightfall Shield",
|
||||
"shieldMystery202605Notes": "Let the moon’s shining light protect you from dangers in the dark. Confers no benefit. May 2026 Subscriber Item.",
|
||||
"shieldMystery202606Text": "Holiday Hammock",
|
||||
"shieldMystery202606Notes": "Between tasks, hop in this hammock, relax, and enjoy the scenery! Confers no benefit. June 2026 Subscriber Item.",
|
||||
"shieldMystery202607Text": "Oceanmancer's Briny Bubble",
|
||||
"shieldMystery202607Notes": "Tumultuous waters bend to your mighty magical will. Confers no benefit. July 2026 Subscriber Item.",
|
||||
"shieldMystery202608Text": "Brilliant Emerald Blade",
|
||||
"shieldMystery202608Notes": "Slice and dice all your tasks into manageable pieces! Confers no benefit. August 2026 Subscriber Item.",
|
||||
|
||||
"shieldMystery301405Text": "Clock Shield",
|
||||
"shieldMystery301405Notes": "Time is on your side with this towering clock shield! Confers no benefit. June 3015 Subscriber Item.",
|
||||
@@ -3409,9 +3353,7 @@
|
||||
"shieldArmoireSoftYellowPillowText": "Soft Yellow Pillow",
|
||||
"shieldArmoireSoftYellowPillowNotes": "The experienced warrior packs a pillow for any expedition. Grow and shine as you consolidate all you’ve learned during past adventures… even while you nap. Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 3 of 3).",
|
||||
"shieldArmoireVerdantBannerText": "Verdant Page Banner",
|
||||
"shieldArmoireVerdantBannerNotes": "Wave your banner high to signal friends it’s time to rally together! Increases Intelligence by <%= int %>. Enchanted Armoire: Verdant Page Set (Item 2 of 2).",
|
||||
"shieldArmoireGardenHoseText": "Garden Hose",
|
||||
"shieldArmoireGardenHoseNotes": "This magical hose never kinks and can infinitely stretch to reach every inch of your space. All your flowers, trees, shrubs, and thirsty pets can enjoy a drink from it. Increases Perception by <%= per %>. Enchanted Armoire: Gardener Set 2 (Item 2 of 2).",
|
||||
"shieldArmoireVerdantBannerNotes": "Wave your banner high to signal friends it’s time to rally together! Intelligence by <%= int %>. Enchanted Armoire: Verdant Page Set (Item 2 of 2).",
|
||||
|
||||
"back": "Back Accessory",
|
||||
"backBase0Text": "No Back Accessory",
|
||||
@@ -3863,8 +3805,6 @@
|
||||
"eyewearMystery202503Notes": "This piercing gaze will strike terror into any fighter who dares to challenge you! Confers no benefit. March 2025 Subscriber Item.",
|
||||
"eyewearMystery202510Text": "Gliding Ghoul Eyes",
|
||||
"eyewearMystery202510Notes": "These spooky eyes glow like the Harvest Moon. Confers no benefit. October 2025 Subscriber Item.",
|
||||
"eyewearMystery202606Text": "Holiday Shades",
|
||||
"eyewearMystery202606Notes": "Your eyes are shaded but your outlook is still sunny! Confers no benefit. June 2026 Subscriber Item.",
|
||||
|
||||
"eyewearMystery301404Text": "Eyewear Goggles",
|
||||
"eyewearMystery301404Notes": "No eyewear could be fancier than a pair of goggles - except, perhaps, for a monocle. Confers no benefit. April 3015 Subscriber Item.",
|
||||
|
||||
@@ -209,48 +209,44 @@
|
||||
"fall2023BogCreatureHealerSet": "Bog Creature (Healer)",
|
||||
"winter2024SnowyOwlRogueSet": "Snowy Owl (Rogue)",
|
||||
"winter2024FrozenHealerSet": "Frozen (Healer)",
|
||||
"winter2024PeppermintBarkWarriorSet": "Peppermint Bark (Warrior)",
|
||||
"winter2024NarwhalWizardMageSet": "Narwhal Wizard (Mage)",
|
||||
"spring2024FluoriteWarriorSet": "Fluorite (Warrior)",
|
||||
"spring2024HibiscusMageSet": "Hibiscus (Mage)",
|
||||
"spring2024BluebirdHealerSet": "Bluebird (Healer)",
|
||||
"spring2024MeltingSnowRogueSet": "Melting Snow (Rogue)",
|
||||
"summer2024WhaleSharkWarriorSet": "Whale Shark (Warrior)",
|
||||
"summer2024SeaAnemoneMageSet": "Sea Anemone (Mage)",
|
||||
"summer2024SeaSnailHealerSet": "Sea Snail (Healer)",
|
||||
"summer2024NudibranchRogueSet": "Nudibranch (Rogue)",
|
||||
"fall2024FieryImpWarriorSet": "Fiery Imp (Warrior)",
|
||||
"fall2024UnderworldSorcerorMageSet": "Underworld Sorceror (Mage)",
|
||||
"fall2024SpaceInvaderHealerSet": "Space Invader (Healer)",
|
||||
"fall2024BlackCatRogueSet": "Black Cat (Rogue)",
|
||||
"winter2025MooseWarriorSet": "Moose (Warrior)",
|
||||
"winter2025AuroraMageSet": "Aurora (Mage)",
|
||||
"winter2025StringLightsHealerSet": "String Lights (Healer)",
|
||||
"winter2025SnowRogueSet": "Snow (Rogue)",
|
||||
"spring2025SunshineWarriorSet": "Sunshine (Warrior)",
|
||||
"spring2025CrystalPointRogueSet": "Crystal Point (Rogue)",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria (Healer)",
|
||||
"spring2025MantisMageSet": "Mantis (Mage)",
|
||||
"summer2025ScallopWarriorSet": "Scallop (Warrior)",
|
||||
"summer2025SquidRogueSet": "Squid (Rogue)",
|
||||
"summer2025SeaAngelHealerSet": "Sea Angel (Healer)",
|
||||
"summer2025FairyWrasseMageSet": "Fairy Wrasse (Mage)",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch (Warrior)",
|
||||
"fall2025SkeletonRogueSet": "Skeleton (Rogue)",
|
||||
"fall2025KoboldHealerSet": "Kobold (Healer)",
|
||||
"fall2025MaskedGhostMageSet": "Masked Ghost (Mage)",
|
||||
"winter2026RimeReaperWarriorSet": "Rime Reaper (Warrior)",
|
||||
"winter2026SkiRogueSet": "Ski (Rogue)",
|
||||
"winter2026PolarBearHealerSet": "Polar Bear (Healer)",
|
||||
"winter2026MidwinterCandleMageSet": "Midwinter Candle (Mage)",
|
||||
"spring2026FrogWarriorSet": "Frog (Warrior)",
|
||||
"spring2026BranchRogueSet": "Spring Branch (Rogue)",
|
||||
"spring2026SnowdropHealerSet": "Snowdrop (Healer)",
|
||||
"spring2026MaypoleMageSet": "Maypole (Mage)",
|
||||
"summer2026AlligatorWarriorSet": "Alligator (Warrior)",
|
||||
"summer2026PuffinHealerSet": "Puffin (Healer)",
|
||||
"summer2026TigerSharkMageSet": "Tiger Shark (Mage)",
|
||||
"summer2026TsunamiRogueSet": "Tsunami (Rogue)",
|
||||
"winter2024PeppermintBarkWarriorSet": "Peppermint Bark Set (Warrior)",
|
||||
"winter2024NarwhalWizardMageSet": "Narwhal Wizard Set (Mage)",
|
||||
"spring2024FluoriteWarriorSet": "Fluorite Set (Warrior)",
|
||||
"spring2024HibiscusMageSet": "Hibiscus Set (Mage)",
|
||||
"spring2024BluebirdHealerSet": "Bluebird Set (Healer)",
|
||||
"spring2024MeltingSnowRogueSet": "Melting Snow Set (Rogue)",
|
||||
"summer2024WhaleSharkWarriorSet": "Whale Shark Set (Warrior)",
|
||||
"summer2024SeaAnemoneMageSet": "Sea Anemone Set (Mage)",
|
||||
"summer2024SeaSnailHealerSet": "Sea Snail Set (Healer)",
|
||||
"summer2024NudibranchRogueSet": "Nudibranch Set (Rogue)",
|
||||
"fall2024FieryImpWarriorSet": "Fiery Imp Set (Warrior)",
|
||||
"fall2024UnderworldSorcerorMageSet": "Underworld Sorceror Set (Mage)",
|
||||
"fall2024SpaceInvaderHealerSet": "Space Invader Set (Healer)",
|
||||
"fall2024BlackCatRogueSet": "Black Cat Set (Rogue)",
|
||||
"winter2025MooseWarriorSet": "Moose Set (Warrior)",
|
||||
"winter2025AuroraMageSet": "Aurora Set (Mage)",
|
||||
"winter2025StringLightsHealerSet": "String Lights Set (Healer)",
|
||||
"winter2025SnowRogueSet": "Snow Set (Rogue)",
|
||||
"spring2025SunshineWarriorSet": "Sunshine Set (Warrior)",
|
||||
"spring2025CrystalPointRogueSet": "Crystal Point Set (Rogue)",
|
||||
"spring2025PlumeriaHealerSet": "Plumeria Set (Healer)",
|
||||
"spring2025MantisMageSet": "Mantis Set (Mage)",
|
||||
"summer2025ScallopWarriorSet": "Scallop Set (Warrior)",
|
||||
"summer2025SquidRogueSet": "Squid Set (Rogue)",
|
||||
"summer2025SeaAngelHealerSet": "Sea Angel Set (Healer)",
|
||||
"summer2025FairyWrasseMageSet": "Fairy Wrasse Set (Mage)",
|
||||
"fall2025SasquatchWarriorSet": "Sasquatch Set (Warrior)",
|
||||
"fall2025SkeletonRogueSet": "Skeleton Set (Rogue)",
|
||||
"fall2025KoboldHealerSet": "Kobold Set (Healer)",
|
||||
"fall2025MaskedGhostMageSet": "Masked Ghost Set (Mage)",
|
||||
"winter2026RimeReaperWarriorSet": "Rime Reaper Set (Warrior)",
|
||||
"winter2026SkiRogueSet": "Ski Set (Rogue)",
|
||||
"winter2026PolarBearHealerSet": "Polar Bear Set (Healer)",
|
||||
"winter2026MidwinterCandleMageSet": "Midwinter Candle Set (Mage)",
|
||||
"spring2026FrogWarriorSet": "Frog Set (Warrior)",
|
||||
"spring2026BranchRogueSet": "Spring Branch Set (Rogue)",
|
||||
"spring2026SnowdropHealerSet": "Snowdrop Set (Healer)",
|
||||
"spring2026MaypoleMageSet": "Maypole Set (Mage)",
|
||||
"winterPromoGiftHeader": "GIFT A SUBSCRIPTION, GET ONE FREE!",
|
||||
"winterPromoGiftDetails1": "Until January 6th only, when you gift somebody a subscription, you get the same subscription for yourself for free!",
|
||||
"winterPromoGiftDetails2": "Please note that if you or your gift recipient already have a recurring subscription, the gifted subscription will only start after that subscription is cancelled or has expired. Thanks so much for your support! <3",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user