Compare commits

..

5 Commits

Author SHA1 Message Date
Kalista Payne 0ed8b4d633 fix(lint): dangle 2026-03-26 17:51:17 -05:00
Kalista Payne 330b8576ea fix(lint): semi 2026-03-26 17:38:49 -05:00
Kalista Payne e1ad86bbf7 fix(spi): unlink 2026-03-26 17:38:16 -05:00
Kalista Payne 5ff2da17de fix(links): unmangle, distinct jumps 2026-03-26 17:36:55 -05:00
Kalista Payne e8e4ff8687 feat(tasks): warn about adding SPI 2026-03-25 16:36:53 -05:00
350 changed files with 6196 additions and 8409 deletions
-1
View File
@@ -21,4 +21,3 @@ services:
timeout: 30s timeout: 30s
start_period: 0s start_period: 0s
retries: 30 retries: 30
+281 -266
View File
@@ -1,12 +1,12 @@
{ {
"name": "habitica", "name": "habitica",
"version": "5.48.0", "version": "5.47.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "habitica", "name": "habitica",
"version": "5.48.0", "version": "5.47.0",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@babel/core": "^7.22.10", "@babel/core": "^7.22.10",
@@ -35,7 +35,6 @@
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"express": "^4.21.1", "express": "^4.21.1",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-sitemap-xml": "^3.1.0",
"express-validator": "^5.2.0", "express-validator": "^5.2.0",
"firebase-admin": "^12.1.1", "firebase-admin": "^12.1.1",
"glob": "^8.1.0", "glob": "^8.1.0",
@@ -58,7 +57,7 @@
"micromustache": "^8.0.3", "micromustache": "^8.0.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119", "moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.23.0", "mongoose": "^8.9.5",
"morgan": "^1.10.1", "morgan": "^1.10.1",
"nan": "^2.25.0", "nan": "^2.25.0",
"nconf": "^0.12.1", "nconf": "^0.12.1",
@@ -82,6 +81,7 @@
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"validator": "^13.11.0", "validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0", "winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0", "winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
@@ -2624,19 +2624,6 @@
"node": ">=12.0.0" "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": { "node_modules/@google-cloud/trace-agent/node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -3065,9 +3052,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
}, },
"node_modules/@mongodb-js/saslprep": { "node_modules/@mongodb-js/saslprep": {
"version": "1.4.6", "version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz",
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==", "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"sparse-bitfield": "^3.0.3" "sparse-bitfield": "^3.0.3"
@@ -3278,6 +3265,11 @@
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" "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": { "node_modules/@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -3940,6 +3932,17 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "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": { "node_modules/agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -6313,7 +6316,6 @@
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz", "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==", "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"readable-stream": "^2.3.5", "readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1" "safe-buffer": "^5.1.1"
@@ -6323,15 +6325,13 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true, "dev": true
"license": "MIT"
}, },
"node_modules/bl/node_modules/readable-stream": { "node_modules/bl/node_modules/readable-stream": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@@ -6347,7 +6347,6 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@@ -7752,6 +7751,11 @@
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
"integrity": "sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -10141,30 +10145,6 @@
"basic-auth": "^2.0.1" "basic-auth": "^2.0.1"
} }
}, },
"node_modules/express-sitemap-xml": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/express-sitemap-xml/-/express-sitemap-xml-3.1.0.tgz",
"integrity": "sha512-rhm4ydngymgQlUyKor2kiY9Xf3wWWb/tbXYVMvidxyA83D1JjKOqYo23clhMvwJ+fk2ht11KtJwcaHnhhdo4PQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"p-memoize": "^4.0.1",
"xmlbuilder": "^15.1.1"
}
},
"node_modules/express-validator": { "node_modules/express-validator": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-5.3.1.tgz", "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-5.3.1.tgz",
@@ -11249,6 +11229,18 @@
"node": ">=12" "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": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11992,19 +11984,6 @@
"node": ">=12" "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": { "node_modules/google-auth-library/node_modules/lru-cache": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -12537,6 +12516,20 @@
"node": ">= 0.10" "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": { "node_modules/habitica-markdown": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz", "resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
@@ -12826,7 +12819,6 @@
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz", "resolved": "https://registry.npmjs.org/helmet/-/helmet-4.6.0.tgz",
"integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==", "integrity": "sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==",
"license": "MIT",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
} }
@@ -12879,8 +12871,7 @@
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="
"dev": true
}, },
"node_modules/http-cache-semantics": { "node_modules/http-cache-semantics": {
"version": "4.1.1", "version": "4.1.1",
@@ -14887,18 +14878,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/map-age-cleaner": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
"integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
"license": "MIT",
"dependencies": {
"p-defer": "^1.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/map-cache": { "node_modules/map-cache": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@@ -15578,14 +15557,128 @@
} }
}, },
"node_modules/mongodb": { "node_modules/mongodb": {
"version": "6.20.0", "version": "3.7.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "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", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@mongodb-js/saslprep": "^1.3.0", "@types/whatwg-url": "^11.0.2",
"bson": "^6.10.4", "whatwg-url": "^14.1.0 || ^13.0.0"
"mongodb-connection-string-url": "^3.0.2" }
},
"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": { "engines": {
"node": ">=16.20.1" "node": ">=16.20.1"
@@ -15596,7 +15689,7 @@
"gcp-metadata": "^5.2.0", "gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1", "kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7", "mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.3.2", "snappy": "^7.2.2",
"socks": "^2.7.1" "socks": "^2.7.1"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -15623,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": { "node_modules/mongoose/node_modules/ms": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -15748,56 +15775,6 @@
"integrity": "sha512-jSTz73B/+pGTTvhu5Ym8xsG6+QqaWab53UXnXdNNlTijTdLvcHABCLJXudQiJxob5N1Mzr5EOSx5ziwn2sihPQ==", "integrity": "sha512-jSTz73B/+pGTTvhu5Ym8xsG6+QqaWab53UXnXdNNlTijTdLvcHABCLJXudQiJxob5N1Mzr5EOSx5ziwn2sihPQ==",
"dev": true "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": { "node_modules/morgan": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@@ -15875,6 +15852,14 @@
"node": ">=14.0.0" "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": { "node_modules/ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -17143,12 +17128,19 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/optional-require": {
"version": "1.1.10", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.10.tgz", "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
"integrity": "sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==", "integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"require-at": "^1.0.6" "require-at": "^1.0.6"
}, },
@@ -17264,15 +17256,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/p-defer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
"integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/p-event": { "node_modules/p-event": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz",
@@ -17352,32 +17335,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/p-memoize": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-4.0.4.tgz",
"integrity": "sha512-ijdh0DP4Mk6J4FXlOM6vPPoCjPytcEseW8p/k5SDTSSfGV3E9bpt9Yzfifvzp6iohIieoLTkXRb32OWV0fB2Lw==",
"license": "MIT",
"dependencies": {
"map-age-cleaner": "^0.1.3",
"mimic-fn": "^3.0.0",
"p-settle": "^4.1.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/p-memoize?sponsor=1"
}
},
"node_modules/p-memoize/node_modules/mimic-fn": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz",
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/p-pipe": { "node_modules/p-pipe": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz", "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz",
@@ -17398,31 +17355,6 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/p-reflect": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-reflect/-/p-reflect-2.1.0.tgz",
"integrity": "sha512-paHV8NUz8zDHu5lhr/ngGWQiW067DK/+IbJ+RfZ4k+s8y4EKyYCz8pGYWjxCg35eHztpJAt+NUgvN4L+GCbPlg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/p-settle": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/p-settle/-/p-settle-4.1.1.tgz",
"integrity": "sha512-6THGh13mt3gypcNMm0ADqVNCcYa3BK6DWsuJWFCuEKP1rpY+OKGp7gaZwVmLspmic01+fsg/fN57MfvDzZ/PuQ==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.2",
"p-reflect": "^2.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": { "node_modules/p-timeout": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz",
@@ -18690,7 +18622,6 @@
"resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz", "resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz",
"integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==", "integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==",
"dev": true, "dev": true,
"license": "Apache-2.0",
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
@@ -18989,7 +18920,6 @@
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"sparse-bitfield": "^3.0.3" "sparse-bitfield": "^3.0.3"
@@ -19416,6 +19346,19 @@
"node": ">=8" "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": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -20906,6 +20849,14 @@
"node": ">=0.6" "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": { "node_modules/touch": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
@@ -21919,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": { "node_modules/webpack-cli": {
"version": "4.10.0", "version": "4.10.0",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz",
@@ -22284,6 +22279,26 @@
"typedarray-to-buffer": "^3.1.5" "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": { "node_modules/xml-crypto": {
"version": "0.10.1", "version": "0.10.1",
"resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz", "resolved": "https://registry.npmjs.org/xml-crypto/-/xml-crypto-0.10.1.tgz",
+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "habitica", "name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.", "description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.48.0", "version": "5.47.0",
"main": "./website/server/index.js", "main": "./website/server/index.js",
"dependencies": { "dependencies": {
"@babel/core": "^7.22.10", "@babel/core": "^7.22.10",
@@ -30,7 +30,6 @@
"eslint-plugin-mocha": "^5.0.0", "eslint-plugin-mocha": "^5.0.0",
"express": "^4.21.1", "express": "^4.21.1",
"express-basic-auth": "^1.2.1", "express-basic-auth": "^1.2.1",
"express-sitemap-xml": "^3.1.0",
"express-validator": "^5.2.0", "express-validator": "^5.2.0",
"firebase-admin": "^12.1.1", "firebase-admin": "^12.1.1",
"glob": "^8.1.0", "glob": "^8.1.0",
@@ -53,7 +52,7 @@
"micromustache": "^8.0.3", "micromustache": "^8.0.3",
"moment": "^2.29.4", "moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119", "moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.23.0", "mongoose": "^8.9.5",
"morgan": "^1.10.1", "morgan": "^1.10.1",
"nan": "^2.25.0", "nan": "^2.25.0",
"nconf": "^0.12.1", "nconf": "^0.12.1",
@@ -77,6 +76,7 @@
"useragent": "^2.1.9", "useragent": "^2.1.9",
"uuid": "^9.0.0", "uuid": "^9.0.0",
"validator": "^13.11.0", "validator": "^13.11.0",
"webpack-bundle-analyzer": "^4.10.2",
"winston": "^3.10.0", "winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0", "winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2" "xml2js": "^0.6.2"
+560
View File
@@ -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
View File
@@ -13,6 +13,7 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user'; import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task'; import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common'; import common from '../../../../website/common';
import * as analytics from '../../../../website/server/libs/analyticsService';
import { model as Group } from '../../../../website/server/models/group'; import { model as Group } from '../../../../website/server/models/group';
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime(); const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
@@ -40,17 +41,20 @@ describe('cron', async () => {
}, },
}, },
}); });
sinon.spy(analytics, 'track');
}); });
afterEach(async () => { afterEach(async () => {
if (clock !== null) clock.restore(); if (clock !== null) clock.restore();
analytics.track.restore();
}); });
it('updates user.preferences.timezoneOffsetAtLastCron', async () => { it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
const timezoneUtcOffsetFromUserPrefs = -1; const timezoneUtcOffsetFromUserPrefs = -1;
await cron({ await cron({
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs, user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
}); });
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1); expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
@@ -59,7 +63,7 @@ describe('cron', async () => {
it('resets user.items.lastDrop.count', async () => { it('resets user.items.lastDrop.count', async () => {
user.items.lastDrop.count = 4; user.items.lastDrop.count = 4;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.items.lastDrop.count).to.equal(0); expect(user.items.lastDrop.count).to.equal(0);
}); });
@@ -67,11 +71,26 @@ describe('cron', async () => {
it('increments user cron count', async () => { it('increments user cron count', async () => {
const cronCountBefore = user.flags.cronCount; const cronCountBefore = user.flags.cronCount;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore); 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 () => { describe('end of the month perks', async () => {
beforeEach(async () => { beforeEach(async () => {
user.purchased.plan.customerId = 'subscribedId'; user.purchased.plan.customerId = 'subscribedId';
@@ -82,7 +101,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-12-11'); user.purchased.plan.dateUpdated = new Date('2018-12-11');
clock = sinon.useFakeTimers(new Date('2019-01-29')); clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.mysteryItems.length).to.eql(2); expect(user.purchased.plan.mysteryItems.length).to.eql(2);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS'); 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'); user.purchased.plan.dateUpdated = new Date('2018-11-11');
clock = sinon.useFakeTimers(new Date('2019-01-29')); clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.mysteryItems.length).to.eql(4); expect(user.purchased.plan.mysteryItems.length).to.eql(4);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS'); 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 () => { it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10; user.purchased.plan.gemsBought = 10;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.gemsBought).to.equal(0); expect(user.purchased.plan.gemsBought).to.equal(0);
}); });
@@ -112,7 +131,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10; user.purchased.plan.gemsBought = 10;
user.purchased.plan.dateUpdated = undefined; user.purchased.plan.dateUpdated = undefined;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.gemsBought).to.equal(0); expect(user.purchased.plan.gemsBought).to.equal(0);
}); });
@@ -123,7 +142,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10; user.purchased.plan.gemsBought = 10;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.gemsBought).to.equal(10); expect(user.purchased.plan.gemsBought).to.equal(10);
}); });
@@ -131,7 +150,7 @@ describe('cron', async () => {
it('resets plan.dateUpdated on a new month', async () => { it('resets plan.dateUpdated on a new month', async () => {
const currentMonth = moment().startOf('month'); const currentMonth = moment().startOf('month');
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true); 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 () => { it('increments plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0; user.purchased.plan.consecutive.count = 0;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.count).to.equal(1); expect(user.purchased.plan.consecutive.count).to.equal(1);
}); });
@@ -147,7 +166,7 @@ describe('cron', async () => {
it('increments plan.cumulativeCount', async () => { it('increments plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0; user.purchased.plan.cumulativeCount = 0;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.cumulativeCount).to.equal(1); 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.dateUpdated = moment().subtract(2, 'months').toDate();
user.purchased.plan.consecutive.count = 0; user.purchased.plan.consecutive.count = 0;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.count).to.equal(2); 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.dateUpdated = moment().subtract(3, 'months').toDate();
user.purchased.plan.cumulativeCount = 0; user.purchased.plan.cumulativeCount = 0;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.cumulativeCount).to.equal(3); expect(user.purchased.plan.cumulativeCount).to.equal(3);
}); });
@@ -177,7 +196,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.trinkets = 1; user.purchased.plan.consecutive.trinkets = 1;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.trinkets).to.equal(1); 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.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5; user.purchased.plan.consecutive.count = 5;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26); 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 () => { 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 }); user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.customerId).to.exist; expect(user.purchased.plan.customerId).to.exist;
}); });
@@ -206,7 +225,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.count = 5; user.purchased.plan.consecutive.count = 5;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.customerId).to.not.exist; 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 // Add 2 days so that we're sure we're not affected by any start-of-month effects
// e.g., from time zone oddness. // e.g., from time zone oddness.
await cron({ 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.count).to.equal(1);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2); expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -257,7 +276,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ 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.count).to.equal(10);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11); expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -292,7 +311,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ 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.count).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2); expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -304,7 +323,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ 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.count).to.equal(10);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11); expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -339,7 +358,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ 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.count).to.equal(1);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2); expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -372,7 +391,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ 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.count).to.equal(1);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2); expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -384,7 +403,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ 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.count).to.equal(10);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11); expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -420,7 +439,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ await cron({
user: user3g, tasksByType, daysMissed, user: user3g, tasksByType, daysMissed, analytics,
}); });
expect(user3g.purchased.plan.consecutive.count).to.equal(1); expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.cumulativeCount).to.equal(1); expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
@@ -433,7 +452,7 @@ describe('cron', async () => {
.add(2, 'days') .add(2, 'days')
.toDate()); .toDate());
await cron({ await cron({
user: user3g, tasksByType, daysMissed, user: user3g, tasksByType, daysMissed, analytics,
}); });
// subscription has been erased by now // subscription has been erased by now
expect(user3g.purchased.plan.consecutive.count).to.equal(0); 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 () => { it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10; user.purchased.plan.gemsBought = 10;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.gemsBought).to.equal(0); expect(user.purchased.plan.gemsBought).to.equal(0);
}); });
@@ -463,14 +482,14 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10; user.purchased.plan.gemsBought = 10;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.gemsBought).to.equal(10); expect(user.purchased.plan.gemsBought).to.equal(10);
}); });
it('does not reset plan.dateUpdated on a new month', async () => { it('does not reset plan.dateUpdated on a new month', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.dateUpdated).to.be.empty; expect(user.purchased.plan.dateUpdated).to.be.empty;
}); });
@@ -478,7 +497,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.count', async () => { it('does not increment plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0; user.purchased.plan.consecutive.count = 0;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.count).to.equal(0); expect(user.purchased.plan.consecutive.count).to.equal(0);
}); });
@@ -486,7 +505,7 @@ describe('cron', async () => {
it('does not increment plan.cumulativeCount', async () => { it('does not increment plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0; user.purchased.plan.cumulativeCount = 0;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.cumulativeCount).to.equal(0); 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 () => { 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; user.purchased.plan.consecutive.count = 5;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.trinkets).to.equal(0); 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 () => { 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; user.purchased.plan.consecutive.count = 5;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0); 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.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5; user.purchased.plan.consecutive.count = 5;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26); 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 () => { 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 }); user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.purchased.plan.customerId).to.not.exist; expect(user.purchased.plan.customerId).to.not.exist;
}); });
@@ -545,7 +564,7 @@ describe('cron', async () => {
it('should make uncompleted todos redder', async () => { it('should make uncompleted todos redder', async () => {
const valueBefore = tasksByType.todos[0].value; const valueBefore = tasksByType.todos[0].value;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore); expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
}); });
@@ -554,7 +573,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true; tasksByType.todos[0].completed = true;
const valueBefore = tasksByType.todos[0].value; const valueBefore = tasksByType.todos[0].value;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.todos[0].value).to.equal(valueBefore); expect(tasksByType.todos[0].value).to.equal(valueBefore);
}); });
@@ -563,7 +582,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true; tasksByType.todos[0].completed = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.history.todos).to.be.lengthOf(1); expect(user.history.todos).to.be.lengthOf(1);
@@ -589,7 +608,7 @@ describe('cron', async () => {
expect(user.tasksOrder.todos).to.be.lengthOf(3); expect(user.tasksOrder.todos).to.be.lengthOf(3);
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged // 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 const original = user.tasksOrder.todos; // Preserve the original order
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
let listsAreEqual = true; let listsAreEqual = true;
@@ -656,7 +675,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5; tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate(); tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].isDue).to.be.false; expect(tasksByType.dailys[0].isDue).to.be.false;
}); });
@@ -667,7 +686,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5; tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate(); tasksByType.dailys[0].startDate = moment().toDate();
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].isDue).to.exist; expect(tasksByType.dailys[0].isDue).to.exist;
}); });
@@ -677,14 +696,14 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5; tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate(); tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].nextDue.length).to.eql(6); expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
}); });
it('should add history', async () => { it('should add history', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].history).to.be.lengthOf(1); expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
}); });
@@ -692,7 +711,7 @@ describe('cron', async () => {
it('should set tasks completed to false', async () => { it('should set tasks completed to false', async () => {
tasksByType.dailys[0].completed = true; tasksByType.dailys[0].completed = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].completed).to.be.false; expect(tasksByType.dailys[0].completed).to.be.false;
}); });
@@ -701,7 +720,7 @@ describe('cron', async () => {
user.preferences.sleep = true; user.preferences.sleep = true;
tasksByType.dailys[0].completed = true; tasksByType.dailys[0].completed = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].completed).to.be.false; 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].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true; tasksByType.dailys[0].completed = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false; 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].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true; tasksByType.dailys[0].completed = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false; 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].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false; expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
}); });
@@ -740,7 +759,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp; const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.hp).to.be.lessThan(hpBefore); expect(user.stats.hp).to.be.lessThan(hpBefore);
}); });
@@ -751,7 +770,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp; const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.hp).to.equal(hpBefore); expect(user.stats.hp).to.equal(hpBefore);
}); });
@@ -765,7 +784,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
cronOverride({ cronOverride({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.hp).to.equal(hpBefore); expect(user.stats.hp).to.equal(hpBefore);
@@ -778,7 +797,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.hp).to.equal(hpBefore); expect(user.stats.hp).to.equal(hpBefore);
@@ -789,7 +808,7 @@ describe('cron', async () => {
let hpBefore = user.stats.hp; let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp; 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: 'test', completed: true });
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false }); tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp; const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
@@ -810,7 +829,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({ const progress = await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(progress.down).to.equal(-1); expect(progress.down).to.equal(-1);
@@ -822,7 +841,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 }); tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({ const progress = await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(progress.down).to.equal(0); expect(progress.down).to.equal(0);
@@ -843,7 +862,7 @@ describe('cron', async () => {
tasksByType.dailys[1].frequency = 'daily'; tasksByType.dailys[1].frequency = 'daily';
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.hp).to.equal(48); expect(user.stats.hp).to.equal(48);
@@ -867,7 +886,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = false; tasksByType.habits[0].down = false;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].value).to.be.lessThan(1); expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -878,7 +897,7 @@ describe('cron', async () => {
tasksByType.habits[0].up = false; tasksByType.habits[0].up = false;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].value).to.be.lessThan(1); expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -890,7 +909,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = true; tasksByType.habits[0].down = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].value).to.equal(1); expect(tasksByType.habits[0].value).to.equal(1);
@@ -909,7 +928,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1; tasksByType.habits[0].counterDown = 1;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -922,7 +941,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1; tasksByType.habits[0].counterDown = 1;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -936,7 +955,7 @@ describe('cron', async () => {
// should not reset // should not reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(1); expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -945,7 +964,7 @@ describe('cron', async () => {
// should reset // should reset
daysMissed = 8; daysMissed = 8;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -969,7 +988,7 @@ describe('cron', async () => {
// should not reset // should not reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(1); expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -983,7 +1002,7 @@ describe('cron', async () => {
// should reset after user CDS // should reset after user CDS
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1007,7 +1026,7 @@ describe('cron', async () => {
// should not reset // should not reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(1); expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1017,7 +1036,7 @@ describe('cron', async () => {
// should reset // should reset
daysMissed = 2; daysMissed = 2;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1041,7 +1060,7 @@ describe('cron', async () => {
// should reset // should reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1065,7 +1084,7 @@ describe('cron', async () => {
// should not reset // should not reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(1); expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1079,7 +1098,7 @@ describe('cron', async () => {
// should not reset // should not reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(1); expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1088,7 +1107,7 @@ describe('cron', async () => {
// should reset // should reset
daysMissed = 32; daysMissed = 32;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1113,7 +1132,7 @@ describe('cron', async () => {
// should reset // should reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1137,7 +1156,7 @@ describe('cron', async () => {
// should not reset // should not reset
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(1); expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1147,7 +1166,7 @@ describe('cron', async () => {
// should reset // should reset
daysMissed = 2; daysMissed = 2;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(tasksByType.habits[0].counterUp).to.equal(0); expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1180,7 +1199,7 @@ describe('cron', async () => {
user.stats.lvl = 2; user.stats.lvl = 2;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.history.exp).to.have.lengthOf(1); expect(user.history.exp).to.have.lengthOf(1);
@@ -1193,7 +1212,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = true; tasksByType.dailys[0].isDue = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.achievements.perfect).to.equal(1); expect(user.achievements.perfect).to.equal(1);
@@ -1205,7 +1224,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = false; tasksByType.dailys[0].isDue = false;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.achievements.perfect).to.equal(0); expect(user.achievements.perfect).to.equal(0);
@@ -1219,7 +1238,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject(); const previousBuffs = user.stats.buffs.toObject();
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str); expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1237,7 +1256,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject(); const previousBuffs = user.stats.buffs.toObject();
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str); expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1261,7 +1280,7 @@ describe('cron', async () => {
}; };
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.equal(0); expect(user.stats.buffs.str).to.equal(0);
@@ -1288,7 +1307,7 @@ describe('cron', async () => {
}; };
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.equal(0); expect(user.stats.buffs.str).to.equal(0);
@@ -1314,7 +1333,7 @@ describe('cron', async () => {
}; };
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.equal(0); expect(user.stats.buffs.str).to.equal(0);
@@ -1341,7 +1360,7 @@ describe('cron', async () => {
}; };
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.equal(0); expect(user.stats.buffs.str).to.equal(0);
@@ -1362,7 +1381,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject(); const previousBuffs = user.stats.buffs.toObject();
cronOverride({ cronOverride({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str); expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1382,7 +1401,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject(); const previousBuffs = user.stats.buffs.toObject();
cronOverride({ cronOverride({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str); expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1401,7 +1420,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true; tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 })); stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.mp).to.be.greaterThan(mpBefore); expect(user.stats.mp).to.be.greaterThan(mpBefore);
@@ -1417,7 +1436,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true; tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 })); stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.mp).to.equal(mpBefore); expect(user.stats.mp).to.equal(mpBefore);
@@ -1430,7 +1449,7 @@ describe('cron', async () => {
user.stats.mp = 120; user.stats.mp = 120;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 })); stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP); expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
@@ -1463,7 +1482,7 @@ describe('cron', async () => {
it('resets user progress', async () => { it('resets user progress', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.party.quest.progress.up).to.equal(0); expect(user.party.quest.progress.up).to.equal(0);
expect(user.party.quest.progress.down).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 () => { it('applies the user progress', async () => {
const progress = await cron({ const progress = await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(progress.down).to.equal(-1); expect(progress.down).to.equal(-1);
}); });
@@ -1510,19 +1529,19 @@ describe('cron', async () => {
describe('login incentives', async () => { describe('login incentives', async () => {
it('increments incentive counter each cron', async () => { it('increments incentive counter each cron', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(1); expect(user.loginIncentives).to.eql(1);
user.lastCron = moment(new Date()).subtract({ days: 1 }); user.lastCron = moment(new Date()).subtract({ days: 1 });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(2); expect(user.loginIncentives).to.eql(2);
}); });
it('pushes a notification of the day\'s incentive each cron', async () => { it('pushes a notification of the day\'s incentive each cron', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.notifications.length).to.eql(1); expect(user.notifications.length).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE'); expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
@@ -1530,13 +1549,13 @@ describe('cron', async () => {
it('replaces previous notifications', async () => { it('replaces previous notifications', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE'); 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 () => { it('increments loginIncentives by 1 even if days are skipped in between', async () => {
daysMissed = 3; daysMissed = 3;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(1); expect(user.loginIncentives).to.eql(1);
}); });
@@ -1555,14 +1574,14 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if user is sleeping', async () => { it('increments loginIncentives by 1 even if user is sleeping', async () => {
user.preferences.sleep = true; user.preferences.sleep = true;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(1); expect(user.loginIncentives).to.eql(1);
}); });
it('awards user bard robes if login incentive is 1', async () => { it('awards user bard robes if login incentive is 1', async () => {
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(1); expect(user.loginIncentives).to.eql(1);
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true); 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 () => { it('awards user incentive backgrounds if login incentive is 2', async () => {
user.loginIncentives = 1; user.loginIncentives = 1;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(2); expect(user.loginIncentives).to.eql(2);
expect(user.purchased.background.blue).to.eql(true); 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 () => { it('awards user Bard Hat if login incentive is 3', async () => {
user.loginIncentives = 2; user.loginIncentives = 2;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(3); expect(user.loginIncentives).to.eql(3);
expect(user.items.gear.owned.head_special_bardHat).to.eql(true); 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 () => { it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
user.loginIncentives = 3; user.loginIncentives = 3;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(4); expect(user.loginIncentives).to.eql(4);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1); 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 () => { it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
user.loginIncentives = 4; user.loginIncentives = 4;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(5); expect(user.loginIncentives).to.eql(5);
@@ -1620,7 +1639,7 @@ describe('cron', async () => {
it('awards user moon quest if login incentive is 7', async () => { it('awards user moon quest if login incentive is 7', async () => {
user.loginIncentives = 6; user.loginIncentives = 6;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(7); expect(user.loginIncentives).to.eql(7);
expect(user.items.quests.moon1).to.eql(1); 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 () => { it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
user.loginIncentives = 9; user.loginIncentives = 9;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(10); expect(user.loginIncentives).to.eql(10);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1); 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 () => { it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
user.loginIncentives = 13; user.loginIncentives = 13;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(14); 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 () => { it('awards user a bard instrument if login incentive is 18', async () => {
user.loginIncentives = 17; user.loginIncentives = 17;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(18); expect(user.loginIncentives).to.eql(18);
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true); 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 () => { it('awards user second moon quest if login incentive is 22', async () => {
user.loginIncentives = 21; user.loginIncentives = 21;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(22); expect(user.loginIncentives).to.eql(22);
expect(user.items.quests.moon2).to.eql(1); 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 () => { it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
user.loginIncentives = 25; user.loginIncentives = 25;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(26); expect(user.loginIncentives).to.eql(26);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1); 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 () => { it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
user.loginIncentives = 29; user.loginIncentives = 29;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(30); 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 () => { it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
user.loginIncentives = 34; user.loginIncentives = 34;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(35); expect(user.loginIncentives).to.eql(35);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1); 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 () => { it('awards user the third moon quest if login incentive is 40', async () => {
user.loginIncentives = 39; user.loginIncentives = 39;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(40); expect(user.loginIncentives).to.eql(40);
expect(user.items.quests.moon3).to.eql(1); 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 () => { it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
user.loginIncentives = 44; user.loginIncentives = 44;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(45); expect(user.loginIncentives).to.eql(45);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1); 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 () => { it('awards user a saddle if login incentive is 50', async () => {
user.loginIncentives = 49; user.loginIncentives = 49;
await cron({ await cron({
user, tasksByType, daysMissed, user, tasksByType, daysMissed, analytics,
}); });
expect(user.loginIncentives).to.eql(50); expect(user.loginIncentives).to.eql(50);
expect(user.items.food.Saddle).to.eql(1); expect(user.items.food.Saddle).to.eql(1);
@@ -1747,6 +1766,7 @@ describe('cron wrapper', () => {
res = generateRes(); res = generateRes();
req = generateReq(); req = generateReq();
user = await res.locals.user.save(); user = await res.locals.user.save();
res.analytics = analytics;
}); });
afterEach(() => { afterEach(() => {
-100
View File
@@ -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');
});
});
});
+44 -61
View File
@@ -3,6 +3,7 @@ import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email'; import * as sender from '../../../../../website/server/libs/email';
import common from '../../../../../website/common'; import common from '../../../../../website/common';
import api from '../../../../../website/server/libs/payments/payments'; 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 * as notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-integration/v3'; import { translate as t } from '../../../../helpers/api-integration/v3';
@@ -12,7 +13,6 @@ import {
import * as worldState from '../../../../../website/server/libs/worldState'; import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction'; import { TransactionModel } from '../../../../../website/server/models/transaction';
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events'; import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
describe('payments/index', () => { describe('payments/index', () => {
let user; let user;
@@ -36,6 +36,8 @@ describe('payments/index', () => {
sandbox.stub(sender, 'sendTxn'); sandbox.stub(sender, 'sendTxn');
sandbox.stub(user, 'sendMessage'); sandbox.stub(user, 'sendMessage');
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
sandbox.stub(analytics.mockAnalyticsService, 'track');
sandbox.stub(notifications, 'sendNotification'); sandbox.stub(notifications, 'sendNotification');
data = { data = {
@@ -95,16 +97,6 @@ describe('payments/index', () => {
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5); 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 () => { it('adds extra months to an existing subscription', async () => {
recipient.purchased.plan = plan; recipient.purchased.plan = plan;
@@ -306,6 +298,28 @@ describe('payments/index', () => {
expect(notifications.sendNotification).to.be.calledOnce; 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', () => { context('No Active Promotion', () => {
beforeEach(() => { beforeEach(() => {
sinon.stub(worldState, 'getCurrentEventList').returns([]); sinon.stub(worldState, 'getCurrentEventList').returns([]);
@@ -441,16 +455,6 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist; 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 () => { it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist; 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'); expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
}); });
context('Upgrades subscription', () => { it('tracks subscription purchase', async () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data); await api.createSubscription(data);
data.sub.key = 'basic_6mo'; expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
data.updatedFrom = { key: 'basic_earned' }; expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
await api.createSubscription(data); uuid: user._id,
groupId: undefined,
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' }); itemPurchased: 'Subscription',
expect(subscriptionEvent).to.exist; sku: 'payment method-subscription',
expect(subscriptionEvent).to.have.property('eventType', 'upgraded'); purchaseType: 'subscribe',
expect(subscriptionEvent).to.have.property('userId', user._id); paymentMethod: data.paymentMethod,
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method'); 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 () => { it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned'; data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist; expect(user.purchased.plan.planId).to.not.exist;
@@ -599,23 +608,6 @@ describe('payments/index', () => {
}); });
context('Downgrades subscription', () => { 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 () => { it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo'; data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist; 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 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 () => { it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2; 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);
});
});
+12 -24
View File
@@ -32,8 +32,7 @@ describe('rateLimiter middleware', () => {
it('is disabled when the env var is not defined', () => { it('is disabled when the env var is not defined', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns(undefined);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
attachRateLimiter(req, res, next); attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce; expect(next).to.have.been.calledOnce;
@@ -44,8 +43,7 @@ describe('rateLimiter middleware', () => {
it('is disabled when the env var is an not "true"', () => { it('is disabled when the env var is an not "true"', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('false');
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
attachRateLimiter(req, res, next); attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce; expect(next).to.have.been.calledOnce;
@@ -57,8 +55,7 @@ describe('rateLimiter middleware', () => {
it('does not throw when there are available points', async () => { it('does not throw when there are available points', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce; expect(next).to.have.been.calledOnce;
@@ -80,8 +77,7 @@ describe('rateLimiter middleware', () => {
sandbox.stub(RateLimiterMemory.prototype, 'consume') sandbox.stub(RateLimiterMemory.prototype, 'consume')
.returns(Promise.reject(new Error('Unknown error.'))); .returns(Promise.reject(new Error('Unknown error.')));
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
expect(next).to.have.been.calledOnce; expect(next).to.have.been.calledOnce;
@@ -96,8 +92,7 @@ describe('rateLimiter middleware', () => {
it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => { it('does not throw when LIVELINESS_PROBE_KEY is correct', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc'); nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.query.liveliness = 'abc'; req.query.liveliness = 'abc';
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
@@ -112,8 +107,7 @@ describe('rateLimiter middleware', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc'); nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('abc');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.query.liveliness = 'das'; req.query.liveliness = 'das';
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
@@ -130,8 +124,7 @@ describe('rateLimiter middleware', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined); nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(undefined);
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
@@ -147,8 +140,7 @@ describe('rateLimiter middleware', () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns(''); nconfGetStub.withArgs('LIVELINESS_PROBE_KEY').returns('');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.query.liveliness = ''; req.query.liveliness = '';
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
@@ -164,8 +156,7 @@ describe('rateLimiter middleware', () => {
it('throws when there are no available points remaining', async () => { it('throws when there are no available points remaining', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
// call for 31 times // call for 31 times
for (let i = 0; i < 31; i += 1) { 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 () => { it('uses the user id if supplied or the ip address', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(1);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.ip = 1; req.ip = 1;
await attachRateLimiter(req, res, next); 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 () => { it('applies increased cost for registration calls with and without user id', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3); nconfGetStub.withArgs('RATE_LIMITER_REGISTRATION_COST').returns(3);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.path = '/api/v4/user/auth/local/register'; req.path = '/api/v4/user/auth/local/register';
req.ip = 1; req.ip = 1;
@@ -252,8 +241,7 @@ describe('rateLimiter middleware', () => {
it('applies increased cost for unauthenticated API calls', async () => { it('applies increased cost for unauthenticated API calls', async () => {
nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true'); nconfGetStub.withArgs('RATE_LIMITER_ENABLED').returns('true');
nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10); nconfGetStub.withArgs('RATE_LIMITER_IP_COST').returns(10);
const setupRateLimiter = requireAgain(pathToRateLimiter).default; const attachRateLimiter = requireAgain(pathToRateLimiter).default;
const attachRateLimiter = setupRateLimiter();
req.ip = 1; req.ip = 1;
await attachRateLimiter(req, res, next); await attachRateLimiter(req, res, next);
-54
View File
@@ -6,8 +6,6 @@ import {
SPAM_MESSAGE_LIMIT, SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL, SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH, SPAM_WINDOW_LENGTH,
MAX_CHAT_COUNT,
MAX_SUBBED_GROUP_CHAT_COUNT,
INVITES_LIMIT, INVITES_LIMIT,
model as Group, model as Group,
} from '../../../../website/server/models/group'; } from '../../../../website/server/models/group';
@@ -20,7 +18,6 @@ import {
import * as email from '../../../../website/server/libs/email'; import * as email from '../../../../website/server/libs/email';
import { TAVERN_ID } from '../../../../website/common/script/constants'; import { TAVERN_ID } from '../../../../website/common/script/constants';
import shared from '../../../../website/common'; import shared from '../../../../website/common';
import { chatModel as Chat } from '../../../../website/server/models/message';
describe('Group Model', () => { describe('Group Model', () => {
let party; let questLeader; let participatingMember; 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', () => { describe('#sendChat', () => {
beforeEach(() => { beforeEach(() => {
sandbox.spy(User, 'updateOne'); 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', () => { describe('#startQuest', () => {
context('Failure Conditions', () => { context('Failure Conditions', () => {
it('throws an error if group is not a party', async () => { 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; 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 () => { it('does not accept invite for a quest already underway', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`); await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`); 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; 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 () => { it('return an error when a user rejects an invite already accepted', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`); await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`); await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
@@ -1,6 +1,7 @@
import { import {
generateUser, generateUser,
} from '../../../../helpers/api-integration/v3'; } from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /user/sleep', () => { describe('POST /user/sleep', () => {
let user; let user;
@@ -22,4 +23,15 @@ describe('POST /user/sleep', () => {
await user.sync(); await user.sync();
expect(user.preferences.sleep).to.be.false; 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'; } from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes'; import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { encrypt } from '../../../../../../website/server/libs/encryption'; import { encrypt } from '../../../../../../website/server/libs/encryption';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
function generateRandomUserName () { function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20); return (Date.now() + uuid()).substring(0, 20);
@@ -42,25 +41,6 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true); 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 () => { it('registers a new user and sets verifiedUsername to true', async () => {
const username = generateRandomUserName(); const username = generateRandomUserName();
const email = `${username}@example.com`; const email = `${username}@example.com`;
@@ -7,7 +7,6 @@ import {
getProperty, getProperty,
} from '../../../../../helpers/api-integration/v3'; } from '../../../../../helpers/api-integration/v3';
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages'; import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
describe('POST /user/auth/social', () => { describe('POST /user/auth/social', () => {
let api; 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'); 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 () => { it('includes sanitized version of provided username', async () => {
const response = await api.post(endpoint, { const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -245,17 +231,6 @@ describe('POST /user/auth/social', () => {
expect(response.newUser).to.be.false; 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 () => { it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, { const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+10 -1
View File
@@ -13,6 +13,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buy', () => { describe('shared.ops.buy', () => {
let user; let user;
const analytics = { track () {} };
beforeEach(() => { beforeEach(() => {
user = generateUser({ 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 () => { it('returns error when key is not provided', async () => {
@@ -44,8 +51,10 @@ describe('shared.ops.buy', () => {
it('buys health potion', async () => { it('buys health potion', async () => {
user.stats.hp = 30; 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(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
}); });
it('adds equipment to inventory', async () => { it('adds equipment to inventory', async () => {
+7 -3
View File
@@ -29,9 +29,10 @@ describe('shared.ops.buyArmoire', () => {
const YIELD_EQUIPMENT = 0.5; const YIELD_EQUIPMENT = 0.5;
const YIELD_FOOD = 0.7; const YIELD_FOOD = 0.7;
const YIELD_EXP = 0.9; const YIELD_EXP = 0.9;
const analytics = { track () {} };
async function buyArmoire (_user, _req) { async function buyArmoire (_user, _req, _analytics) {
const buyOp = new BuyArmoireOperation(_user, _req); const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
@@ -49,10 +50,12 @@ describe('shared.ops.buyArmoire', () => {
user.items.food = {}; user.items.food = {};
sandbox.stub(randomValFns, 'trueRandom'); sandbox.stub(randomValFns, 'trueRandom');
sinon.stub(analytics, 'track');
}); });
afterEach(() => { afterEach(() => {
randomValFns.trueRandom.restore(); randomValFns.trueRandom.restore();
analytics.track.restore();
}); });
context('failure conditions', () => { context('failure conditions', () => {
@@ -144,7 +147,7 @@ describe('shared.ops.buyArmoire', () => {
expect(_.size(user.items.gear.owned)).to.equal(2); 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); 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(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
expect(user.stats.gp).to.eql(100); expect(user.stats.gp).to.eql(100);
expect(analytics.track).to.be.calledTwice;
}); });
}); });
}); });
+12 -3
View File
@@ -1,5 +1,6 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
import sinon from 'sinon'; // eslint-disable-line no-shadow
import { import {
generateUser, generateUser,
} from '../../../helpers/common.helper'; } 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 { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits'; import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
async function buyGem (user, req) { async function buyGem (user, req, analytics) {
const buyOp = new BuyGemOperation(user, req); const buyOp = new BuyGemOperation(user, req, analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
describe('shared.ops.buyGem', () => { describe('shared.ops.buyGem', () => {
let user; let user;
const analytics = { track () {} };
const goldPoints = 40; const goldPoints = 40;
const gemsBought = 40; const gemsBought = 40;
const userGemAmount = 10; const userGemAmount = 10;
@@ -33,16 +35,23 @@ describe('shared.ops.buyGem', () => {
}, },
}, },
}); });
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
}); });
context('Gems', () => { context('Gems', () => {
it('purchases gems', async () => { 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(message).to.equal(i18n.t('plusGem', { count: 1 }));
expect(user.balance).to.equal(userGemAmount + 0.25); expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1); expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate); 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 () => { it('purchases gems with a different language than the default', async () => {
+10 -3
View File
@@ -10,9 +10,10 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyHealthPotion', () => { describe('shared.ops.buyHealthPotion', () => {
let user; let user;
const analytics = { track () {} };
async function buyHealthPotion (_user, _req) { async function buyHealthPotion (_user, _req, _analytics) {
const buyOp = new BuyHealthPotionOperation(_user, _req); const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
@@ -31,13 +32,19 @@ describe('shared.ops.buyHealthPotion', () => {
}, },
stats: { gp: 200 }, stats: { gp: 200 },
}); });
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
}); });
context('Potion', () => { context('Potion', () => {
it('recovers 15 hp', async () => { it('recovers 15 hp', async () => {
user.stats.hp = 30; user.stats.hp = 30;
await buyHealthPotion(user, {}); await buyHealthPotion(user, {}, analytics);
expect(user.stats.hp).to.eql(45); expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
}); });
it('does not increase hp above 50', async () => { it('does not increase hp above 50', async () => {
+9 -5
View File
@@ -13,14 +13,15 @@ import {
import i18n from '../../../../website/common/script/i18n'; import i18n from '../../../../website/common/script/i18n';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage'; import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
async function buyGear (user, req) { async function buyGear (user, req, analytics) {
const buyOp = new BuyMarketGearOperation(user, req); const buyOp = new BuyMarketGearOperation(user, req, analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
describe('shared.ops.buyMarketGear', () => { describe('shared.ops.buyMarketGear', () => {
let user; let user;
const analytics = { track () {} };
let clock; let clock;
beforeEach(() => { beforeEach(() => {
@@ -46,12 +47,14 @@ describe('shared.ops.buyMarketGear', () => {
sinon.stub(shared, 'randomVal'); sinon.stub(shared, 'randomVal');
sinon.stub(shared.onboarding, 'checkOnboardingStatus'); sinon.stub(shared.onboarding, 'checkOnboardingStatus');
sinon.stub(shared.fns, 'predictableRandom'); sinon.stub(shared.fns, 'predictableRandom');
sinon.stub(analytics, 'track');
}); });
afterEach(() => { afterEach(() => {
shared.randomVal.restore(); shared.randomVal.restore();
shared.fns.predictableRandom.restore(); shared.fns.predictableRandom.restore();
shared.onboarding.checkOnboardingStatus.restore(); shared.onboarding.checkOnboardingStatus.restore();
analytics.track.restore();
if (clock) { if (clock) {
clock.restore(); clock.restore();
@@ -62,7 +65,7 @@ describe('shared.ops.buyMarketGear', () => {
it('adds equipment to inventory', async () => { it('adds equipment to inventory', async () => {
user.stats.gp = 31; 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({ expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true, weapon_warrior_0: true,
@@ -89,12 +92,13 @@ describe('shared.ops.buyMarketGear', () => {
eyewear_special_whiteHalfMoon: true, eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: 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 () => { it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
user.stats.gp = 31; 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.calledOnce;
expect(user.addAchievement).to.be.calledWith('purchasedEquipment'); expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
@@ -107,7 +111,7 @@ describe('shared.ops.buyMarketGear', () => {
user.stats.gp = 31; user.stats.gp = 31;
user.achievements.purchasedEquipment = true; 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; expect(user.addAchievement).to.not.be.called;
}); });
+5 -2
View File
@@ -14,6 +14,7 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyMysterySet', () => { describe('shared.ops.buyMysterySet', () => {
let user; let user;
const analytics = { track () {} };
let clock; let clock;
beforeEach(() => { beforeEach(() => {
@@ -26,9 +27,11 @@ describe('shared.ops.buyMysterySet', () => {
}, },
}, },
}); });
sinon.stub(analytics, 'track');
}); });
afterEach(() => { afterEach(() => {
analytics.track.restore();
if (clock) { if (clock) {
clock.restore(); clock.restore();
} }
@@ -90,7 +93,7 @@ describe('shared.ops.buyMysterySet', () => {
context('successful purchases', () => { context('successful purchases', () => {
it('buys Steampunk Accessories Set', async () => { it('buys Steampunk Accessories Set', async () => {
user.purchased.plan.consecutive.trinkets = 1; 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.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true); 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 () => { it('buys mystery set if it is available', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-16')); clock = sinon.useFakeTimers(new Date('2024-01-16'));
user.purchased.plan.consecutive.trinkets = 1; 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.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true); expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
+5 -2
View File
@@ -12,9 +12,10 @@ describe('shared.ops.buyQuestGems', () => {
let user; let user;
let clock; let clock;
const goldPoints = 40; const goldPoints = 40;
const analytics = { track () {} };
async function buyQuest (_user, _req) { async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGemOperation(_user, _req); const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
@@ -24,11 +25,13 @@ describe('shared.ops.buyQuestGems', () => {
}); });
beforeEach(() => { beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath'); sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sinon.useFakeTimers(new Date('2024-01-16')); clock = sinon.useFakeTimers(new Date('2024-01-16'));
}); });
afterEach(() => { afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore(); pinnedGearUtils.removeItemByPath.restore();
clock.restore(); clock.restore();
}); });
+17 -8
View File
@@ -12,15 +12,21 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyQuest', () => { describe('shared.ops.buyQuest', () => {
let user; let user;
const analytics = { track () {} };
async function buyQuest (_user, _req) { async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req); const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
beforeEach(() => { beforeEach(() => {
user = generateUser(); user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
}); });
it('buys a Quest scroll', async () => { it('buys a Quest scroll', async () => {
@@ -29,11 +35,12 @@ describe('shared.ops.buyQuest', () => {
params: { params: {
key: 'dilatoryDistress1', key: 'dilatoryDistress1',
}, },
}); }, analytics);
expect(user.items.quests).to.eql({ expect(user.items.quests).to.eql({
dilatoryDistress1: 1, dilatoryDistress1: 1,
}); });
expect(user.stats.gp).to.equal(5); 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 () => { 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; user.items.quests[key] = -1;
await buyQuest(user, { await buyQuest(user, {
params: { key }, params: { key },
}); }, analytics);
expect(user.items.quests[key]).to.equal(1); expect(user.items.quests[key]).to.equal(1);
expect(user.stats.gp).to.equal(5); 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 () => { 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: { params: {
key: 'dilatoryDistress1', key: 'dilatoryDistress1',
}, },
}); }, analytics);
await buyQuest(user, { await buyQuest(user, {
params: { params: {
key: 'dilatoryDistress1', key: 'dilatoryDistress1',
}, },
quantity: '3', quantity: '3',
}); }, analytics);
expect(user.items.quests).to.eql({ expect(user.items.quests).to.eql({
dilatoryDistress1: 4, dilatoryDistress1: 4,
@@ -74,7 +82,7 @@ describe('shared.ops.buyQuest', () => {
key: 'dilatoryDistress1', key: 'dilatoryDistress1',
}, },
quantity: 'a', quantity: 'a',
}); }, analytics);
} catch (err) { } catch (err) {
expect(err).to.be.an.instanceof(BadRequest); expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity')); expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -179,11 +187,12 @@ describe('shared.ops.buyQuest', () => {
params: { params: {
key: 'dilatoryDistress3', key: 'dilatoryDistress3',
}, },
}); }, analytics);
expect(user.items.quests).to.eql({ expect(user.items.quests).to.eql({
dilatoryDistress3: 1, dilatoryDistress3: 1,
}); });
expect(user.stats.gp).to.equal(100); expect(user.stats.gp).to.equal(100);
expect(analytics.track).to.be.calledOnce;
}); });
}); });
+11 -5
View File
@@ -14,17 +14,20 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buySpecialSpell', () => { describe('shared.ops.buySpecialSpell', () => {
let user; let user;
let clock; let clock;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req) { async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req); const buyOp = new BuySpellOperation(_user, _req, _analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
beforeEach(() => { beforeEach(() => {
user = generateUser(); user = generateUser();
sinon.stub(analytics, 'track');
}); });
afterEach(() => { afterEach(() => {
analytics.track.restore();
if (clock) { if (clock) {
clock.restore(); clock.restore();
} }
@@ -75,7 +78,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: { params: {
key: 'thankyou', key: 'thankyou',
}, },
}); }, analytics);
expect(user.stats.gp).to.equal(1); expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).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', { expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(), itemText: item.text(),
})); }));
expect(analytics.track).to.be.calledOnce;
}); });
it('buys a limited card when it is available', async () => { it('buys a limited card when it is available', async () => {
@@ -97,7 +101,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: { params: {
key: 'nye', key: 'nye',
}, },
}); }, analytics);
expect(user.stats.gp).to.equal(1); expect(user.stats.gp).to.equal(1);
expect(user.items.special.nye).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', { expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(), itemText: item.text(),
})); }));
expect(analytics.track).to.be.calledOnce;
}); });
it('throws an error if the card is not currently available', async () => { it('throws an error if the card is not currently available', async () => {
@@ -135,7 +140,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: { params: {
key: 'seafoam', key: 'seafoam',
}, },
}); }, analytics);
expect(user.stats.gp).to.equal(1); expect(user.stats.gp).to.equal(1);
expect(user.items.special.seafoam).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', { expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(), itemText: item.text(),
})); }));
expect(analytics.track).to.be.calledOnce;
}); });
it('throws an error if the spell is not currently available', async () => { it('throws an error if the spell is not currently available', async () => {
+10 -3
View File
@@ -13,15 +13,21 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
describe('common.ops.hourglassPurchase', () => { describe('common.ops.hourglassPurchase', () => {
let user; let user;
const analytics = { track () {} };
async function buyMount (_user, _req) { async function buyMount (_user, _req, _analytics) {
const buyOp = new BuyHourglassMountOperation(_user, _req); const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
return buyOp.purchase(); return buyOp.purchase();
} }
beforeEach(() => { beforeEach(() => {
user = generateUser(); user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
}); });
context('failure conditions', () => { context('failure conditions', () => {
@@ -125,11 +131,12 @@ describe('common.ops.hourglassPurchase', () => {
it('buys a pet', async () => { it('buys a pet', async () => {
user.purchased.plan.consecutive.trinkets = 2; 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(message).to.eql(i18n.t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1); expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 }); expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
expect(analytics.track).to.be.calledOnce;
}); });
it('buys a mount', async () => { it('buys a mount', async () => {
+8 -4
View File
@@ -17,17 +17,20 @@ describe('shared.ops.purchase', () => {
let user; let user;
let clock; let clock;
const goldPoints = 40; const goldPoints = 40;
const analytics = { track () {} };
before(() => { before(() => {
user = generateUser({ 'stats.class': 'rogue' }); user = generateUser({ 'stats.class': 'rogue' });
}); });
beforeEach(() => { beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath'); sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sandbox.useFakeTimers(new Date('2024-01-10')); clock = sandbox.useFakeTimers(new Date('2024-01-10'));
}); });
afterEach(() => { afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore(); pinnedGearUtils.removeItemByPath.restore();
clock.restore(); clock.restore();
}); });
@@ -184,10 +187,11 @@ describe('shared.ops.purchase', () => {
const type = 'eggs'; const type = 'eggs';
const key = 'Wolf'; 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(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true); expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce;
}); });
it('purchases hatchingPotions', async () => { it('purchases hatchingPotions', async () => {
@@ -328,7 +332,7 @@ describe('shared.ops.purchase', () => {
const key = 'Wolf'; const key = 'Wolf';
try { try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }); await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
} catch (err) { } catch (err) {
expect(err).to.be.an.instanceof(BadRequest); expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity')); expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -341,7 +345,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10; user.balance = 10;
try { try {
await purchase(user, { params: { type, key }, quantity: -2 }); await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
} catch (err) { } catch (err) {
expect(err).to.be.an.instanceof(BadRequest); expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity')); expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -354,7 +358,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10; user.balance = 10;
try { try {
await purchase(user, { params: { type, key }, quantity: 2.9 }); await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
} catch (err) { } catch (err) {
expect(err).to.be.an.instanceof(BadRequest); expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity')); expect(err.message).to.equal(i18n.t('invalidQuantity'));
+15
View File
@@ -54,4 +54,19 @@ describe('armoire', () => {
const febuaryItems = armoire.all; const febuaryItems = armoire.all;
expect(febuaryItems.length).to.equal(384); 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('/paypal') === 0
|| route.indexOf('/amazon') === 0 || route.indexOf('/amazon') === 0
|| route.indexOf('/stripe') === 0 || route.indexOf('/stripe') === 0
|| route.indexOf('/analytics') === 0
) { ) {
url += `${route}`; url += `${route}`;
} else { } else {
+8
View File
@@ -12,12 +12,20 @@ module.exports = {
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': 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-unresolved': 'off',
'import/no-extraneous-dependencies': 'off', 'import/no-extraneous-dependencies': 'off',
'import/extensions': 'off', 'import/extensions': 'off',
'prefer-regex-literals': 'warn', 'prefer-regex-literals': 'warn',
'vue/no-v-html': 'off', 'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn', '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', { 'vue/html-self-closing': ['error', {
html: { html: {
void: 'never', void: 'never',
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Habitica - FAQ</title>
<meta name="description" content="Frequently Asked Questions about Habitica, the gamified task manager.">
<link href="https://fonts.googleapis.com/css?family=Roboto+Condensed:400,400i,700,700i|Roboto:400,400i,700,700i" rel="stylesheet">
<link rel="shortcut icon" sizes="48x48" href="/static/icons/favicon.ico">
<link rel="shortcut icon" sizes="192x192" href="/static/icons/favicon_192x192.png">
<link rel="mask-icon" href="/static/icons/favicon.ico">
<meta property="og:image" content="/static/emails/images/meta-image.png" />
<script type="module" src="/src/main.js"></script>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
<script type='text/javascript' src='/api/v4/i18n/core' vite-ignore></script>
</body>
</html>
+589 -20
View File
@@ -41,6 +41,7 @@
"vite": "^6.3.6", "vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3", "vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10", "vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6", "vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
@@ -54,7 +55,9 @@
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"mocha": "^11.1.0", "mocha": "^11.1.0",
"playwright": "^1.50.1", "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": { "node_modules/@amplitude/analytics-connector": {
@@ -2108,9 +2111,8 @@
"version": "0.3.11", "version": "0.3.11",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25" "@jridgewell/trace-mapping": "^0.3.25"
@@ -3632,12 +3634,41 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"license": "MIT" "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": { "node_modules/@types/json5": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -3648,9 +3679,8 @@
"version": "24.10.1", "version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.16.0"
} }
@@ -3846,6 +3876,181 @@
"vue-template-compiler": "^2.x" "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": { "node_modules/acorn": {
"version": "7.4.1", "version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
@@ -3893,6 +4098,48 @@
"url": "https://github.com/sponsors/epoberezkin" "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": { "node_modules/amplitude-js": {
"version": "8.21.9", "version": "8.21.9",
"resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz", "resolved": "https://registry.npmjs.org/amplitude-js/-/amplitude-js-8.21.9.tgz",
@@ -4370,9 +4617,8 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT", "devOptional": true,
"optional": true, "license": "MIT"
"peer": true
}, },
"node_modules/cac": { "node_modules/cac": {
"version": "6.7.14", "version": "6.7.14",
@@ -4537,6 +4783,16 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/cli-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
@@ -4603,9 +4859,8 @@
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT", "devOptional": true,
"optional": true, "license": "MIT"
"peer": true
}, },
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
@@ -4941,6 +5196,20 @@
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT" "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": { "node_modules/enquirer": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
@@ -6115,6 +6384,16 @@
"node": ">=0.10.0" "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": { "node_modules/expect-type": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
@@ -6592,6 +6871,13 @@
"node": ">= 6" "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": { "node_modules/globals": {
"version": "13.24.0", "version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -6635,6 +6921,13 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/habitica-markdown": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz", "resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-4.1.0.tgz",
@@ -7433,6 +7726,37 @@
"@pkgjs/parseargs": "^0.11.0" "@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": { "node_modules/jquery": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
@@ -7517,6 +7841,13 @@
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"license": "MIT" "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": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -7580,6 +7911,20 @@
"uc.micro": "^2.0.0" "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": { "node_modules/locate-path": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -7783,6 +8128,13 @@
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
"license": "MIT" "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": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -8106,6 +8458,13 @@
"node": ">= 0.4.0" "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": { "node_modules/nice-try": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -9145,6 +9504,63 @@
"node": ">=v12.22.7" "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": { "node_modules/secure-keys": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz", "resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz",
@@ -9422,9 +9838,8 @@
"version": "0.5.21", "version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
@@ -9715,6 +10130,20 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT" "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": { "node_modules/tar-mini": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz", "resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
@@ -9725,9 +10154,8 @@
"version": "5.44.1", "version": "5.44.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.1.tgz",
"integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==", "integrity": "sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==",
"devOptional": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0", "acorn": "^8.15.0",
@@ -9741,13 +10169,47 @@
"node": ">=10" "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": { "node_modules/terser/node_modules/acorn": {
"version": "8.15.0", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -10123,9 +10585,8 @@
"version": "7.16.0", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT", "devOptional": true,
"optional": true, "license": "MIT"
"peer": true
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.4", "version": "1.1.4",
@@ -10504,6 +10965,15 @@
"node": ">=6.0.0" "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": { "node_modules/vue-functional-data-merge": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz", "resolved": "https://registry.npmjs.org/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz",
@@ -10571,6 +11041,20 @@
"node": ">=18" "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": { "node_modules/webidl-conversions": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -10581,6 +11065,91 @@
"node": ">=12" "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": { "node_modules/whatwg-encoding": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+4 -1
View File
@@ -46,6 +46,7 @@
"vite": "^6.3.6", "vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3", "vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10", "vue": "^2.7.10",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6", "vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5", "vue-router": "^3.6.5",
"vuedraggable": "^2.24.3", "vuedraggable": "^2.24.3",
@@ -59,6 +60,8 @@
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"mocha": "^11.1.0", "mocha": "^11.1.0",
"playwright": "^1.50.1", "playwright": "^1.50.1",
"vitest": "^3.0.5" "terser-webpack-plugin": "^5.3.10",
"vitest": "^3.0.5",
"webpack": "^5.94.0"
} }
} }
+2 -134
View File
@@ -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 { .Gems {
display:inline-block; display:inline-block;
margin-right:5px; margin-right:5px;
@@ -91,6 +26,7 @@
margin-left: -3px; margin-left: -3px;
margin-top: -18px; margin-top: -18px;
} }
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 { .slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
width: 90px; width: 90px;
height: 90px; height: 90px;
@@ -98,7 +34,6 @@
/* Critical */ /* Critical */
.weapon_special_critical { .weapon_special_critical {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
width: 90px; width: 90px;
height: 90px; height: 90px;
margin-left:-12px; margin-left:-12px;
@@ -109,6 +44,7 @@
.weapon_special_1 { .weapon_special_1 {
margin-left: -12px; margin-left: -12px;
} }
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 { .broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
width: 90px; width: 90px;
height: 90px; height: 90px;
@@ -117,36 +53,15 @@
.back_special_heroicAureole { .back_special_heroicAureole {
width: 114px; width: 114px;
height: 90px; 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 { .head_special_1 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
margin-top: 3px; 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 { .Pet-Wolf-Cerberus {
width: 105px; width: 105px;
height: 72px; 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 { .broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
@@ -154,36 +69,17 @@
height: 120px; 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 { .weapon_special_ks2019 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
width: 120px; width: 120px;
height: 120px; height: 120px;
} }
.Pet-Gryphon-Gryphatrice { .Pet-Gryphon-Gryphatrice {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
width: 81px; width: 81px;
height: 99px; height: 99px;
} }
.Pet-Gryphatrice-Jubilant { .Pet-Gryphatrice-Jubilant {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
width: 81px; width: 81px;
height: 96px; height: 96px;
} }
@@ -193,39 +89,11 @@
height: 135px; 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 { .background_airship, .background_clocktower, .background_steamworks {
width: 141px; width: 141px;
height: 147px; 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_Head_"],
[class*="Mount_Body_"] { [class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */ margin-top:18px; /* Sprite accommodates 105x123 box */
@@ -695,11 +695,6 @@
width: 141px; width: 141px;
height: 147px; 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_beehive {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beehive.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_beehive.png');
width: 141px; width: 141px;
@@ -2351,11 +2346,6 @@
width: 141px; width: 141px;
height: 147px; 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_tulip_garden {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tulip_garden.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_tulip_garden.png');
width: 141px; width: 141px;
@@ -2411,11 +2401,6 @@
width: 141px; width: 141px;
height: 147px; 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_viking_ship {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_viking_ship.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_viking_ship.png');
width: 141px; width: 141px;
@@ -29895,11 +29880,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .broad_armor_armoire_lamplightersGreatcoat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lamplightersGreatcoat.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_lamplightersGreatcoat.png');
width: 114px; width: 114px;
@@ -30555,11 +30535,6 @@
width: 90px; width: 90px;
height: 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 { .head_armoire_lamplightersTopHat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lamplightersTopHat.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_lamplightersTopHat.png');
width: 114px; width: 114px;
@@ -30945,11 +30920,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .shield_armoire_gardenersSpade {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenersSpade.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_gardenersSpade.png');
width: 114px; width: 114px;
@@ -31580,11 +31550,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .slim_armor_armoire_lamplightersGreatcoat {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lamplightersGreatcoat.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_lamplightersGreatcoat.png');
width: 114px; width: 114px;
@@ -31965,11 +31930,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .weapon_armoire_buoyantBubbles {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_buoyantBubbles.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_buoyantBubbles.png');
width: 114px; width: 114px;
@@ -32070,11 +32030,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .weapon_armoire_gardenersWateringCan {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenersWateringCan.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_gardenersWateringCan.png');
width: 114px; width: 114px;
@@ -32170,11 +32125,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .weapon_armoire_lamplighter {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_lamplighter.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_lamplighter.png');
width: 114px; width: 114px;
@@ -32260,11 +32210,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .weapon_armoire_pinkKite {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pinkKite.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_armoire_pinkKite.png');
width: 114px; width: 114px;
@@ -34255,11 +34200,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .head_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png');
width: 114px; width: 114px;
@@ -34280,31 +34220,11 @@
width: 114px; width: 114px;
height: 90px; 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 { .shield_mystery_202605 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png');
width: 114px; width: 114px;
height: 90px; 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 { .slim_armor_mystery_202512 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png');
width: 114px; width: 114px;
@@ -34330,16 +34250,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .back_mystery_201402 {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png');
width: 90px; width: 90px;
@@ -37805,26 +37715,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .broad_armor_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_summerHealer.png');
width: 90px; width: 90px;
@@ -38075,26 +37965,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .head_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_summerHealer.png');
width: 90px; width: 90px;
@@ -38285,21 +38155,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .shield_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_summerHealer.png');
width: 90px; width: 90px;
@@ -38540,26 +38395,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .slim_armor_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_summerHealer.png');
width: 90px; width: 90px;
@@ -38800,26 +38635,6 @@
width: 114px; width: 114px;
height: 90px; 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 { .weapon_special_summerHealer {
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png'); background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_summerHealer.png');
width: 90px; width: 90px;
-1
View File
@@ -42,7 +42,6 @@ ul {
font-weight: 400; font-weight: 400;
line-height: 1.75; line-height: 1.75;
color: $purple-200; color: $purple-200;
cursor: pointer;
} }
h4 { h4 {
-4
View File
@@ -16,10 +16,6 @@
border-bottom: 0; border-bottom: 0;
} }
.d-content {
display: contents;
}
* { * {
transition: none; transition: none;
} }
@@ -108,15 +108,15 @@ export default {
const allEmails = []; const allEmails = [];
if (user.auth.local.email) allEmails.push(user.auth.local.email); if (user.auth.local.email) allEmails.push(user.auth.local.email);
if (user.auth.google && user.auth.google.emails) { if (user.auth.google && user.auth.google.emails) {
const { emails } = user.auth.google; const emails = user.auth.google.emails;
allEmails.push(...this.findSocialEmails(emails)); allEmails.push(...this.findSocialEmails(emails));
} }
if (user.auth.apple && user.auth.apple.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)); allEmails.push(...this.findSocialEmails(emails));
} }
if (user.auth.facebook && user.auth.facebook.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)); allEmails.push(...this.findSocialEmails(emails));
} }
return allEmails; return allEmails;
@@ -609,7 +609,7 @@ import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks
import saveHero from '../mixins/saveHero'; import saveHero from '../mixins/saveHero';
import LoadingSpinner from '@/components/ui/loadingSpinner'; 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 = { const humanReadablePaymentDetails = {
customerId: { customerId: {
@@ -20,29 +20,6 @@
class="form mx-auto" class="form mx-auto"
@submit.prevent.stop="register()" @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 <input
id="usernameInput" id="usernameInput"
v-model="username" v-model="username"
@@ -81,9 +58,8 @@
></label> ></label>
</div> </div>
<button <button
class="btn btn-info d-flex justify-content-center class="btn btn-info d-block w-100 sign-up mx-auto mb-5"
align-items-center w-100 sign-up mx-auto mb-5" :disabled="!username || usernameInvalid || !privacyAccepted"
:disabled="!email || emailError || !username || usernameInvalid || !privacyAccepted"
type="submit" type="submit"
> >
{{ $t('getStarted') }} {{ $t('getStarted') }}
@@ -157,14 +133,12 @@
border: 2px solid transparent; border: 2px solid transparent;
box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24); box-shadow: 0 1px 3px 0 rgba($black, 0.16), 0 1px 3px 0 rgba($black, 0.24);
&:not(:disabled):not(.disabled) {
&:focus, &:active { &:focus, &:active {
background-color: $blue-50; background-color: $blue-50;
border: 2px solid $purple-400; border: 2px solid $purple-400;
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24); box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
} }
} }
}
.w-448px { .w-448px {
width: 448px; width: 448px;
@@ -174,19 +148,23 @@
<script> <script>
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import PrivacyBanner from '@/components/header/banners/privacy'; import PrivacyBanner from '@/components/header/banners/privacy';
import accountCreation from '@/mixins/accountCreation';
import sanitizeRedirect from '@/mixins/sanitizeRedirect'; import sanitizeRedirect from '@/mixins/sanitizeRedirect';
export default { export default {
components: { components: {
PrivacyBanner, PrivacyBanner,
}, },
mixins: [accountCreation, sanitizeRedirect], mixins: [sanitizeRedirect],
data () { data () {
return { return {
authData: {},
email: '',
password: '',
passwordConfirm: '',
privacyAccepted: false, privacyAccepted: false,
registrationMethod: null,
username: '',
usernameIssues: [], usernameIssues: [],
needsEmailField: false,
}; };
}, },
computed: { computed: {
@@ -205,31 +183,22 @@ export default {
}, },
}, },
mounted () { 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.authData = this.$store.state.registrationOptions.authData;
this.email = this.$store.state.registrationOptions.email; this.email = this.$store.state.registrationOptions.email;
this.username = this.$store.state.registrationOptions.username; this.username = this.$store.state.registrationOptions.username;
this.password = this.$store.state.registrationOptions.password; this.password = this.$store.state.registrationOptions.password;
this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm; this.passwordConfirm = this.$store.state.registrationOptions.passwordConfirm;
if (window.sessionStorage.getItem('apple-token')) {
this.registrationMethod = 'apple';
if (!this.email) { 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') {
return; 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, ''); const usernameToCheck = this.email.split('@')[0].replace(/[^a-zA-Z0-9\-_]/g, '');
this.$store.dispatch('auth:verifyUsername', { this.$store.dispatch('auth:verifyUsername', {
username: usernameToCheck, username: usernameToCheck,
@@ -238,7 +207,6 @@ export default {
this.username = usernameToCheck; this.username = usernameToCheck;
} }
}); });
}
document.getElementById('usernameInput').focus(); document.getElementById('usernameInput').focus();
}, },
methods: { methods: {
@@ -269,7 +237,6 @@ export default {
idToken: window.sessionStorage.getItem('apple-token'), idToken: window.sessionStorage.getItem('apple-token'),
name: window.sessionStorage.getItem('apple-name'), name: window.sessionStorage.getItem('apple-name'),
username: this.username, username: this.username,
email: this.email,
allowRegister: true, allowRegister: true,
}); });
} else { } else {
@@ -189,7 +189,7 @@ export default {
this.cancel(); this.cancel();
return []; return [];
} }
this.currentSearch = regexRes[1]; // eslint-disable-line prefer-destructuring this.currentSearch = regexRes[1];
if (this.currentSearch.length === 0) return []; if (this.currentSearch.length === 0) return [];
@@ -470,7 +470,7 @@ export default {
return this.userGuilds.filter(group => { return this.userGuilds.filter(group => {
const leaderId = group.leader?._id || group.leader; const leaderId = group.leader?._id || group.leader;
if (leaderId !== this.user._id) return false; if (leaderId !== this.user._id) return false;
const { purchased } = group; const purchased = group.purchased;
if (!purchased?.wasUpgraded) return false; if (!purchased?.wasUpgraded) return false;
if (this.activeGroupPlanIds.includes(group._id)) return false; if (this.activeGroupPlanIds.includes(group._id)) return false;
if (!purchased.dateTerminated) return false; if (!purchased.dateTerminated) return false;
@@ -492,7 +492,7 @@ export default {
}, },
isPartyPreviouslyUpgraded () { isPartyPreviouslyUpgraded () {
if (!this.userParty) return false; if (!this.userParty) return false;
const { purchased } = this.userParty; const purchased = this.userParty.purchased;
if (!purchased?.wasUpgraded) return false; if (!purchased?.wasUpgraded) return false;
if (!purchased.dateTerminated) return false; if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date(); return new Date(purchased.dateTerminated) < new Date();
@@ -533,7 +533,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if (this.upgradeableGuilds.length > 0) { if (this.upgradeableGuilds.length > 0) {
[this.selectedOption] = this.upgradeableGuilds; this.selectedOption = this.upgradeableGuilds[0];
} else if (this.upgradeableParty) { } else if (this.upgradeableParty) {
this.selectedOption = this.upgradeableParty; this.selectedOption = this.upgradeableParty;
} else { } else {
@@ -198,6 +198,7 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
import todoIcon from '@/assets/svg/todo.svg?raw'; import todoIcon from '@/assets/svg/todo.svg?raw';
import rewardIcon from '@/assets/svg/reward.svg?raw'; import rewardIcon from '@/assets/svg/reward.svg?raw';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
export default { export default {
@@ -437,6 +438,14 @@ export default {
return false; return false;
}, },
changeMirrorPreference (newVal) { 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 || []; const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id); groupsToMirror.push(this.group._id);
@@ -240,6 +240,7 @@
<script> <script>
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import notifications from '@/mixins/notifications'; import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX'; import closeX from '../ui/closeX';
@@ -275,6 +276,11 @@ export default {
this.$store.state.party.data = party; this.$store.state.party.data = party;
this.user.party._id = party._id; this.user.party._id = party._id;
Analytics.updateUser({
partyID: party._id,
partySize: 1,
});
this.$root.$emit('bv::hide::modal', 'create-party-modal'); this.$root.$emit('bv::hide::modal', 'create-party-modal');
await this.$router.push('/party'); await this.$router.push('/party');
}, },
@@ -314,6 +314,7 @@ import extend from 'lodash/extend';
import groupUtilities from '@/mixins/groupsUtilities'; import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper'; import styleHelper from '@/mixins/styleHelper';
import { mapGetters } from '@/libs/store'; import { mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import participantListModal from './participantListModal'; import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal'; import groupFormModal from './groupFormModal';
import groupGemsModal from '@/components/groups/groupGemsModal'; import groupGemsModal from '@/components/groups/groupGemsModal';
@@ -559,6 +560,7 @@ export default {
if (this.isParty) { if (this.isParty) {
data.type = 'party'; data.type = 'party';
Analytics.updateUser({ partySize: null, partyID: null });
this.$store.state.partyMembers = []; this.$store.state.partyMembers = [];
} }
@@ -334,6 +334,7 @@ import orderBy from 'lodash/orderBy';
import * as quests from '@/../../common/script/content/quests'; import * as quests from '@/../../common/script/content/quests';
import getItemInfo from '@/../../common/script/libs/getItemInfo'; import getItemInfo from '@/../../common/script/libs/getItemInfo';
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import navigationBack from '@/assets/svg/navigation_back.svg?raw'; import navigationBack from '@/assets/svg/navigation_back.svg?raw';
import questDialogContent from '../shops/quests/questDialogContent'; import questDialogContent from '../shops/quests/questDialogContent';
@@ -420,6 +421,11 @@ export default {
async questInit () { async questInit () {
this.loading = true; this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const groupId = this.group._id || this.user.party._id; const groupId = this.group._id || this.user.party._id;
const key = this.selectedQuest; const key = this.selectedQuest;
@@ -123,6 +123,7 @@
<script> <script>
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import * as Analytics from '@/libs/analytics';
import { mapGetters, mapActions } from '@/libs/store'; import { mapGetters, mapActions } from '@/libs/store';
import MemberDetails from '../memberDetails'; import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal'; import createPartyModal from '../groups/createPartyModal';
@@ -235,8 +236,22 @@ export default {
}, },
async createOrInviteParty () { async createOrInviteParty () {
if (this.user.party._id) { 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'); this.$router.push('/looking-for-party');
} else { } 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'); this.$root.$emit('bv::show::modal', 'create-party-modal');
} }
}, },
@@ -114,6 +114,7 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications'; import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide'; import guide from '@/mixins/guide';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager'; import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
import yesterdailyModal from './tasks/yesterdailyModal'; import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './news/modal'; import newStuff from './news/modal';
@@ -647,6 +648,15 @@ export default {
// Reset daily analytics actions // Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0); setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_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 // Sync
@@ -433,6 +433,9 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import notificationsMixin from '@/mixins/notifications'; import notificationsMixin from '@/mixins/notifications';
import paymentsMixin from '@/mixins/payments'; import paymentsMixin from '@/mixins/payments';
// analytics
import * as Analytics from '@/libs/analytics';
export default { export default {
components: { components: {
selectTranslatedArray, selectTranslatedArray,
@@ -533,6 +536,16 @@ export default {
this.close(); this.close();
}, },
submit () { 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.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal'); this.$root.$emit('bv::hide::modal', 'payments-success-modal');
}, },
@@ -37,9 +37,6 @@ export default {
window.location.href = '/'; window.location.href = '/';
} else { } else {
window.sessionStorage.setItem('apple-token', response.idToken); window.sessionStorage.setItem('apple-token', response.idToken);
if (response.email) {
window.sessionStorage.setItem('apple-email', response.email);
}
window.location.href = '/username'; window.location.href = '/username';
} }
}, },
@@ -83,7 +83,7 @@
</div> </div>
</div> </div>
<draggable <draggable
v-if="taskList.length > 0 && !rerendering" v-if="taskList.length > 0"
ref="tasksList" ref="tasksList"
class="sortable-tasks" class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()" :disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
@@ -432,7 +432,6 @@ export default {
selectedItemToBuy: {}, selectedItemToBuy: {},
dragging: false, dragging: false,
rerendering: false,
}; };
}, },
computed: { computed: {
@@ -549,8 +548,8 @@ export default {
if (this.taskListOverride) originTasks = this.taskListOverride; if (this.taskListOverride) originTasks = this.taskListOverride;
// Server // Server
const taskIdToReplace = filteredList[data.newIndex]._id; const taskIdToReplace = filteredList[data.newIndex];
const newIndexOnServer = originTasks.findIndex(task => task._id === taskIdToReplace); const newIndexOnServer = originTasks.findIndex(taskId => taskId === taskIdToReplace);
let newOrder; let newOrder;
if (taskToMove.group.id && !this.isUser) { if (taskToMove.group.id && !this.isUser) {
@@ -569,9 +568,6 @@ export default {
// Client // Client
const deleted = originTasks.splice(data.oldIndex, 1); const deleted = originTasks.splice(data.oldIndex, 1);
originTasks.splice(data.newIndex, 0, deleted[0]); 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' async moveTo (task, where) { // where is 'top' or 'bottom'
const taskIdToMove = task._id; const taskIdToMove = task._id;
+17 -5
View File
@@ -13,8 +13,6 @@
}, `type_${task.type}` }, `type_${task.type}`
]" ]"
@click="castEnd($event, task)" @click="castEnd($event, task)"
tabindex="0"
@keypress.enter="$emit('editTask', task)"
> >
<div <div
class="d-flex" class="d-flex"
@@ -100,7 +98,9 @@
<div <div
class="task-clickable-area pt-1 pl-75 pb-0" class="task-clickable-area pt-1 pl-75 pb-0"
:class="{ 'cursor-auto': !teamManagerAccess }" :class="{ 'cursor-auto': !teamManagerAccess }"
tabindex="0"
@click="edit($event, task)" @click="edit($event, task)"
@keypress.enter="edit($event, task)"
> >
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<h3 <h3
@@ -432,6 +432,10 @@
outline: none; outline: none;
transition: none; transition: none;
border: $purple-400 solid 1px; border: $purple-400 solid 1px;
:not(task-best-control-inner-habit) { // round icon
border-radius: 4px;
}
} }
.control-bottom-box { .control-bottom-box {
@@ -458,13 +462,16 @@
&:hover:not(.task-not-editable.task-not-scoreable), &:hover:not(.task-not-editable.task-not-scoreable),
&:focus-within: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); box-shadow: 0 1px 8px 0 rgba($black, 0.12), 0 4px 4px 0 rgba($black, 0.16);
z-index: 11;
} }
} }
.task:not(.groupTask) { .task:not(.groupTask) {
&:hover, &:focus { &:hover,
border: none; &:focus-within {
outline: 1px solid $purple-400; .left-control, .right-control, .task-content {
border-color: $purple-400;
}
} }
} }
@@ -515,6 +522,11 @@
&-user { &-user {
padding-right: 0px; padding-right: 0px;
} }
&:focus {
border-radius: 4px;
border: $purple-400 solid 1px;
}
} }
.task-title + .task-dropdown ::v-deep .dropdown-menu { .task-title + .task-dropdown ::v-deep .dropdown-menu {
@@ -412,25 +412,6 @@
</div> </div>
</div> </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 svg-icon color gray-50"
v-html="icons.alert"
></span>
<span
class="scheduling-warning-text"
v-html="schedulingWarning"
></span>
</div>
<div <div
v-if="!groupId" v-if="!groupId"
class="tags-select option mt-3" class="tags-select option mt-3"
@@ -1128,42 +1109,6 @@
height: 1rem; 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 { label {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -1432,87 +1377,6 @@ export default {
} }
return null; 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: { repeatsOn: {
get () { get () {
let repeatsOn = 'dayOfMonth'; let repeatsOn = 'dayOfMonth';
@@ -1586,7 +1450,7 @@ export default {
this.task.down = !this.task.down; this.task.down = !this.task.down;
}, },
weekdaysMin (dayNumber) { weekdaysMin (dayNumber) {
return this.$t(`weekdaysMin${dayNumber}`); return moment.weekdaysMin(dayNumber);
}, },
formattedDate (date) { formattedDate (date) {
return moment(date).format('MM/DD/YYYY'); return moment(date).format('MM/DD/YYYY');
@@ -222,22 +222,14 @@ export default {
return usernames; return usernames;
}, },
summarySentence () { 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)) { if (this.task.type === 'daily' && moment().isBefore(this.task.startDate)) {
return `This is ${this.formattedDifficulty(this.task.priority)} task that will repeat 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)} ${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') { if (this.task.type === 'daily') {
return `This is ${this.formattedDifficulty(this.task.priority)} task that repeats 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) { 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')}.`; 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>'); dayStringArray.push('</strong>');
} else if (weeksOfMonth.length > 0) { } else if (weeksOfMonth.length > 0) {
const weekNum = weeksOfMonth[0] + 1; switch (weeksOfMonth[0]) {
const weekNumStr = String(weekNum); case 0:
const lastDigit = weekNumStr.slice(-1); dayStringArray.push('first');
let ordinalSuffix = 'th'; break;
if (lastDigit === '1' && weekNumStr !== '11') ordinalSuffix = 'st'; case 1:
if (lastDigit === '2' && weekNumStr !== '12') ordinalSuffix = 'nd'; dayStringArray.push('second');
if (lastDigit === '3' && weekNumStr !== '13') ordinalSuffix = 'rd'; break;
dayStringArray.push(`${weekNum}${ordinalSuffix}`); 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)); activeDays = keys(pickBy(repeat, value => value === true));
dayStringArray.push(` ${this.expandDayString[activeDays[0]]} of the month</strong>`); 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>'; if (numericX === 2) return '<strong>every other week</strong>';
return `<strong>every ${numericX} weeks</strong>`; return `<strong>every ${numericX} weeks</strong>`;
case 'monthly': case 'monthly':
if (numericX === 1) return `<strong>${this.$t('everyMonth')}</strong>`; if (numericX === 1) return '<strong>every month</strong>';
return `<strong>${this.$t('everyXMonths', { count: numericX })}</strong>`; if (numericX === 2) return '<strong>every other month</strong>';
return `<strong>every ${numericX} months</strong>`;
case 'yearly': case 'yearly':
if (numericX === 1) return '<strong>every year</strong>'; if (numericX === 1) return '<strong>every year</strong>';
return `<strong>every ${everyX} years</strong>`; return `<strong>every ${everyX} years</strong>`;
@@ -68,12 +68,8 @@ export default {
}, },
methods: { methods: {
upDate (after) { upDate (after) {
// zero out the time so the server doesn't shift the day across a DST boundary on save this.value = after;
const normalized = after this.$emit('update:date', after);
? new Date(after.getFullYear(), after.getMonth(), after.getDate())
: null;
this.value = normalized;
this.$emit('update:date', normalized);
}, },
setToday () { setToday () {
this.upDate(moment().toDate()); this.upDate(moment().toDate());
+1 -1
View File
@@ -6,7 +6,7 @@ import amplitude from 'amplitude-js';
import Vue from 'vue'; import Vue from 'vue';
import getStore from '@/store'; import getStore from '@/store';
const { AMPLITUDE_KEY } = import.meta.env; const AMPLITUDE_KEY = import.meta.env.AMPLITUDE_KEY;
const REQUIRED_FIELDS = ['eventCategory', 'eventAction']; const REQUIRED_FIELDS = ['eventCategory', 'eventAction'];
let analyticsLoading = false; let analyticsLoading = false;
+18
View File
@@ -1,10 +1,28 @@
// Vue plugin to globally expose a '$t' method that calls common/i18n.t. // 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. // Can be anywhere inside vue as 'this.$t' or '$t' in templates.
import moment from 'moment';
import i18n from '@/../../common/script/i18n'; import i18n from '@/../../common/script/i18n';
function loadLocale (i18nData) { function loadLocale (i18nData) {
// Load i18n strings // Load i18n strings
i18n.strings = i18nData.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 { export default {
+2
View File
@@ -11,6 +11,7 @@ import {
NavbarPlugin, NavbarPlugin,
CollapsePlugin, CollapsePlugin,
} from 'bootstrap-vue'; } from 'bootstrap-vue';
import Fragment from 'vue-fragment';
import AppComponent from './app'; import AppComponent from './app';
import { setUpLogging } from '@/libs/logging'; import { setUpLogging } from '@/libs/logging';
import router from './router/index'; import router from './router/index';
@@ -43,6 +44,7 @@ Vue.use(FormRadioPlugin);
Vue.use(TooltipPlugin); Vue.use(TooltipPlugin);
Vue.use(NavbarPlugin); Vue.use(NavbarPlugin);
Vue.use(CollapsePlugin); Vue.use(CollapsePlugin);
Vue.use(Fragment.Plugin);
setUpLogging(); setUpLogging();
const store = getStore(); const store = getStore();
+12 -1
View File
@@ -6,8 +6,9 @@ import { mapState } from '@/libs/store';
import encodeParams from '@/libs/encodeParams'; import encodeParams from '@/libs/encodeParams';
import notificationsMixin from '@/mixins/notifications'; import notificationsMixin from '@/mixins/notifications';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager'; 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; let stripeInstance = null;
@@ -206,6 +207,16 @@ export default {
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`); alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
throw checkoutSessionResult.error; 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) { } catch (err) {
console.error('Error while redirecting to Stripe', err); // eslint-disable-line console.error('Error while redirecting to Stripe', err); // eslint-disable-line
alert(`Error while redirecting to Stripe: ${err.message}`); alert(`Error while redirecting to Stripe: ${err.message}`);
+10
View File
@@ -3,6 +3,7 @@ import Vue from 'vue';
import scoreTask from '@/../../common/script/ops/scoreTask'; import scoreTask from '@/../../common/script/ops/scoreTask';
import notifications from './notifications'; import notifications from './notifications';
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager'; import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export default { export default {
@@ -57,6 +58,15 @@ export default {
const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT); const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT);
if (!tasksScoredCount || tasksScoredCount < 2) { 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) { if (!tasksScoredCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1); setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
} else { } else {
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -90,7 +90,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content" v-if="allowedToChangeClass"> <fragment v-if="allowedToChangeClass">
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -71,7 +71,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -55,7 +55,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -77,7 +77,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -94,7 +94,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -78,7 +78,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -83,7 +83,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr> <tr>
<td class="settings-label"> <td class="settings-label">
{{ $t("showHeader") }} {{ $t("showHeader") }}
@@ -26,7 +26,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -67,7 +67,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-for="network in SOCIAL_AUTH_NETWORKS" v-for="network in SOCIAL_AUTH_NETWORKS"
:key="network.key" :key="network.key"
@@ -39,7 +39,7 @@
</a> </a>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<script> <script>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -66,7 +66,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -111,7 +111,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -56,7 +56,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -60,7 +60,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -54,7 +54,7 @@
</div> </div>
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -48,7 +48,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -1,5 +1,5 @@
<template> <template>
<div class="d-content"> <fragment>
<tr <tr
v-if="!mixinData.inlineSettingMixin.modalVisible" v-if="!mixinData.inlineSettingMixin.modalVisible"
> >
@@ -76,7 +76,7 @@
/> />
</td> </td>
</tr> </tr>
</div> </fragment>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
+2 -1
View File
@@ -4,7 +4,6 @@
:class="{ :class="{
'casting-spell': castingSpell, 'casting-spell': castingSpell,
}" }"
@dragover.prevent
> >
<!-- <banned-account-modal /> --> <!-- <banned-account-modal /> -->
<amazon-payments-modal v-if="!isStaticPage" /> <amazon-payments-modal v-if="!isStaticPage" />
@@ -131,6 +130,7 @@ import PrivacyBanner from '@/components/header/banners/privacy';
import AppFooter from '@/components/appFooter'; import AppFooter from '@/components/appFooter';
import notificationsDisplay from '@/components/notifications'; import notificationsDisplay from '@/components/notifications';
import { mapState } from '@/libs/store'; import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import BuyModal from '@/components/shops/buyModal.vue'; import BuyModal from '@/components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue'; import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications'; import notifications from '@/mixins/notifications';
@@ -276,6 +276,7 @@ export default {
} }
} }
Analytics.updateUser();
return this.loadAllTranslations(); return this.loadAllTranslations();
}).then(() => { }).then(() => {
this.$store.state.isUserLoaded = true; this.$store.state.isUserLoaded = true;
+43 -33
View File
@@ -1,5 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import * as Analytics from '@/libs/analytics';
import getStore from '@/store'; import getStore from '@/store';
import handleRedirect from './handleRedirect'; 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 // 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 // Hall
const HallPage = () => import('@/components/hall/index'); const HallPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/index');
const PatronsPage = () => import('@/components/hall/patrons'); const PatronsPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/patrons');
const HeroesPage = () => import('@/components/hall/heroes'); const HeroesPage = () => import(/* webpackChunkName: "hall" */'@/components/hall/heroes');
// Admin Pages // Admin Pages
const AdminContainerPage = () => import('@/components/admin/container'); const AdminContainerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/container');
const AdminPanelPage = () => import('@/components/admin/admin-panel'); const AdminPanelPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel');
const AdminPanelUserPage = () => import('@/components/admin/admin-panel/user-support'); const AdminPanelUserPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/user-support');
const AdminPanelSearchPage = () => import('@/components/admin/admin-panel/search'); const AdminPanelSearchPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/admin-panel/search');
const GroupAdminPage = () => import('@/components/admin/groups'); const GroupAdminPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups');
const GroupAdminGroupPage = () => import('@/components/admin/groups/group-support'); const GroupAdminGroupPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/groups/group-support');
const BlockerPage = () => import('@/components/admin/blocker'); const BlockerPage = () => import(/* webpackChunkName: "admin-panel" */'@/components/admin/blocker');
// Tasks // Tasks
const UserTasks = () => import('@/components/tasks/user'); const UserTasks = () => import(/* webpackChunkName: "userTasks" */'@/components/tasks/user');
// Inventory // Inventory
const InventoryContainer = () => import('@/components/inventory/index'); const InventoryContainer = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/index');
const ItemsPage = () => import('@/components/inventory/items/index'); const ItemsPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/items/index');
const EquipmentPage = () => import('@/components/inventory/equipment/index'); const EquipmentPage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/equipment/index');
const StablePage = () => import('@/components/inventory/stable/index'); const StablePage = () => import(/* webpackChunkName: "inventory" */'@/components/inventory/stable/index');
// Guilds & Parties // Guilds & Parties
const GroupPage = () => import('@/components/groups/group'); const GroupPage = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/group');
const GroupPlansAppPage = () => import('@/components/static/groupPlans'); const GroupPlansAppPage = () => import(/* webpackChunkName: "guilds" */ '@/components/static/groupPlans');
const LookingForParty = () => import('@/components/groups/lookingForParty'); const LookingForParty = () => import(/* webpackChunkName: "guilds" */ '@/components/groups/lookingForParty');
// Group Plans // Group Plans
const GroupPlanIndex = () => import('@/components/group-plans/index'); const GroupPlanIndex = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/index');
const GroupPlanTaskInformation = () => import('@/components/group-plans/taskInformation'); const GroupPlanTaskInformation = () => import(/* webpackChunkName: "group-plans" */ '@/components/group-plans/taskInformation');
const GroupPlanBilling = () => import('@/components/group-plans/billing'); 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 // Challenges
const ChallengeIndex = () => import('@/components/challenges/index'); const ChallengeIndex = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/index');
const MyChallenges = () => import('@/components/challenges/myChallenges'); const MyChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/myChallenges');
const FindChallenges = () => import('@/components/challenges/findChallenges'); const FindChallenges = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/findChallenges');
const ChallengeDetail = () => import('@/components/challenges/challengeDetail'); const ChallengeDetail = () => import(/* webpackChunkName: "challenges" */ '@/components/challenges/challengeDetail');
// Shops // Shops
const ShopsContainer = () => import('@/components/shops/index'); const ShopsContainer = () => import(/* webpackChunkName: "shops" */'@/components/shops/index');
const MarketPage = () => import('@/components/shops/market/index'); const MarketPage = () => import(/* webpackChunkName: "shops-market" */'@/components/shops/market/index');
const QuestsPage = () => import('@/components/shops/quests/index'); const QuestsPage = () => import(/* webpackChunkName: "shops-quest" */'@/components/shops/quests/index');
const CustomizationsPage = () => import('@/components/shops/customizations/index'); const CustomizationsPage = () => import(/* webpackChunkName: "shops-customizations" */'@/components/shops/customizations/index');
const SeasonalPage = () => import('@/components/shops/seasonal/index'); const SeasonalPage = () => import(/* webpackChunkName: "shops-seasonal" */'@/components/shops/seasonal/index');
const TimeTravelersPage = () => import('@/components/shops/timeTravelers/index'); const TimeTravelersPage = () => import(/* webpackChunkName: "shops-timetravelers" */'@/components/shops/timeTravelers/index');
Vue.use(VueRouter); Vue.use(VueRouter);
@@ -317,6 +318,15 @@ router.beforeEach(async (to, from, next) => {
router.app.$root.$emit('update-party'); 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 // Redirect old guild urls
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) { if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
const splits = to.hash.split('/'); const splits = to.hash.split('/');
+2 -2
View File
@@ -21,8 +21,8 @@ const NewsPage = () => import('@/components/static/newStuff');
const OverviewPage = () => import('@/components/static/overview'); const OverviewPage = () => import('@/components/static/overview');
const PressKitPage = () => import('@/components/static/pressKit'); const PressKitPage = () => import('@/components/static/pressKit');
const PrivacyPage = () => import('@/components/static/privacy'); const PrivacyPage = () => import('@/components/static/privacy');
const RegisterLoginReset = () => import('@/components/auth/registerLoginReset'); const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
const RegisterUsername = () => import('@/components/auth/registerUsername'); const RegisterUsername = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerUsername');
const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq'); const SubscriptionBenefitsFaq = () => import('@/components/static/subscriptionBenefitsFaq');
const TermsPage = () => import('@/components/static/terms'); const TermsPage = () => import('@/components/static/terms');
+1 -5
View File
@@ -101,7 +101,6 @@ export async function appleAuth (store, params) {
id_token: params.idToken, id_token: params.idToken,
name: params.name, name: params.name,
username: params.username, username: params.username,
email: params.email,
}, },
}); });
@@ -110,10 +109,7 @@ export async function appleAuth (store, params) {
} }
if (result.data.message && result.data.id_token) { if (result.data.message && result.data.id_token) {
return { return { idToken: result.data.id_token };
idToken: result.data.id_token,
email: result.data.email,
};
} }
const user = result.data.data; const user = result.data.data;
+8
View File
@@ -1,5 +1,6 @@
import axios from 'axios'; import axios from 'axios';
import Vue from 'vue'; import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
export async function getChat (store, payload) { export async function getChat (store, payload) {
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`); 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}`; url += `?previousMsg=${payload.previousMsg}`;
} }
if (group.type === 'party') {
Analytics.updateUser({
partyID: group.id,
partySize: group.memberCount,
});
}
const response = await axios.post(url, { const response = await axios.post(url, {
message: payload.message, message: payload.message,
}); });
@@ -1,6 +1,7 @@
import axios from 'axios'; import axios from 'axios';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import findIndex from 'lodash/findIndex'; import findIndex from 'lodash/findIndex';
import * as Analytics from '@/libs/analytics';
import { loadAsyncResource } from '@/libs/asyncResource'; import { loadAsyncResource } from '@/libs/asyncResource';
export async function getPublicGuilds (store, payload) { export async function getPublicGuilds (store, payload) {
@@ -73,6 +74,7 @@ export async function join (store, payload) {
if (invitationI !== -1) invitations.parties.splice(invitationI, 1); if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
user.party._id = groupId; user.party._id = groupId;
Analytics.updateUser({ partyID: groupId });
// load the party members so that they get shown in the header // load the party members so that they get shown in the header
store.dispatch('party:getMembers'); store.dispatch('party:getMembers');
} }
@@ -18,6 +18,7 @@ import * as shops from './shops';
import * as snackbars from './snackbars'; import * as snackbars from './snackbars';
import * as worldState from './worldState'; import * as worldState from './worldState';
import * as news from './news'; import * as news from './news';
import * as analytics from './analytics';
import * as faq from './faq'; import * as faq from './faq';
import * as blockers from './blockers'; import * as blockers from './blockers';
@@ -43,6 +44,7 @@ const actions = flattenAndNamespace({
snackbars, snackbars,
worldState, worldState,
news, news,
analytics,
faq, faq,
blockers, blockers,
}); });
@@ -1,6 +1,26 @@
import axios from 'axios'; 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 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}`); const response = await axios.post(`/api/v4/groups/${payload.groupId}/${payload.action}`);
// @TODO: Update user? // @TODO: Update user?
+14 -8
View File
@@ -3,6 +3,7 @@ import Vue from 'vue';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { loadAsyncResource } from '@/libs/asyncResource'; import { loadAsyncResource } from '@/libs/asyncResource';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager'; import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export function fetchUserTasks (store, options = {}) { export function fetchUserTasks (store, options = {}) {
@@ -111,6 +112,15 @@ export async function create (store, createdTask) {
} }
const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT); const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT);
if (!tasksCreatedCount || tasksCreatedCount < 2) { 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) { if (!tasksCreatedCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1); setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
} else { } else {
@@ -158,15 +168,11 @@ export async function collapseChecklist (store, task) {
} }
export async function destroy (store, task) { export async function destroy (store, task) {
const type = `${task.type}s`; const list = store.state.tasks.data[`${task.type}s`];
const listIndex = store.state.tasks.data[type].findIndex(t => t._id === task._id); const taskIndex = list.findIndex(t => t._id === task._id);
const orderIndex = store.state.user.data.tasksOrder[type].indexOf(task._id);
if (listIndex > -1) { if (taskIndex > -1) {
store.state.tasks.data[type].splice(listIndex, 1); list.splice(taskIndex, 1);
}
if (orderIndex > -1) {
store.state.user.data.tasksOrder[type].splice(orderIndex, 1);
} }
await axios.delete(`/api/v4/tasks/${task._id}`); await axios.delete(`/api/v4/tasks/${task._id}`);
+4 -4
View File
@@ -121,10 +121,6 @@ export default defineConfig({
include: [/moment-recur/, /node_modules/] include: [/moment-recur/, /node_modules/]
}, },
rollupOptions: { rollupOptions: {
input: {
main: path.resolve(__dirname, 'index.html'),
faq: path.resolve(__dirname, 'index-faq.html'),
},
output: { output: {
experimentalMinChunkSize: 20000 experimentalMinChunkSize: 20000
} }
@@ -163,6 +159,10 @@ export default defineConfig({
target: DEV_BASE_URL, target: DEV_BASE_URL,
changeOrigin: true, changeOrigin: true,
}, },
'^/analytics': {
target: DEV_BASE_URL,
changeOrigin: true,
},
} }
} }
}) })
+3 -16
View File
@@ -213,7 +213,7 @@
"backgroundStormyRooftopsNotes": "Промъквайте се върху буреносни покриви.", "backgroundStormyRooftopsNotes": "Промъквайте се върху буреносни покриви.",
"backgroundWindyAutumnText": "Ветровита есен", "backgroundWindyAutumnText": "Ветровита есен",
"backgroundWindyAutumnNotes": "Гонете листа през ветровита есен.", "backgroundWindyAutumnNotes": "Гонете листа през ветровита есен.",
"incentiveBackgrounds": "Стандартни фонове", "incentiveBackgrounds": "Комплект едноцветни фонове",
"backgroundVioletText": "Виолетово", "backgroundVioletText": "Виолетово",
"backgroundVioletNotes": "Енергичен виолетов фон.", "backgroundVioletNotes": "Енергичен виолетов фон.",
"backgroundBlueText": "Синьо", "backgroundBlueText": "Синьо",
@@ -494,7 +494,7 @@
"backgroundSnowglobeText": "Снежна топка", "backgroundSnowglobeText": "Снежна топка",
"backgroundDesertWithSnowNotes": "Бъди свидетел на рядката и мълчалива красота на Снежната пустиня.", "backgroundDesertWithSnowNotes": "Бъди свидетел на рядката и мълчалива красота на Снежната пустиня.",
"backgroundTeaPartyNotes": "Участвай в изискано Чаено парти.", "backgroundTeaPartyNotes": "Участвай в изискано Чаено парти.",
"backgroundButterflyGardenNotes": "Купонясвайте с опрашители в градина за пеперуди", "backgroundButterflyGardenNotes": "Забавлявайте се с опрашителите в Градина на пеперудите",
"backgroundAnimalCloudsText": "Животински облаци", "backgroundAnimalCloudsText": "Животински облаци",
"backgroundButterflyGardenText": "Градина на пеперудите", "backgroundButterflyGardenText": "Градина на пеперудите",
"backgroundWinterNocturneText": "Зимен ноктюрн", "backgroundWinterNocturneText": "Зимен ноктюрн",
@@ -503,18 +503,5 @@
"hideLockedBackgrounds": "Скрий заключените фонове", "hideLockedBackgrounds": "Скрий заключените фонове",
"backgroundSnowglobeNotes": "Разклати Снежната топка и заеми мястото си в микрокосмоса на снежния пейзаж.", "backgroundSnowglobeNotes": "Разклати Снежната топка и заеми мястото си в микрокосмоса на снежния пейзаж.",
"backgroundAmongGiantFlowersText": "Сред гигантски цветя", "backgroundAmongGiantFlowersText": "Сред гигантски цветя",
"backgroundAnimalCloudsNotes": "Използвай въображението си, за да намериш Животни в Облаците.", "backgroundAnimalCloudsNotes": "Използвай въображението си, за да намериш Животни в Облаците."
"backgroundSucculentGardenNotes": "",
"backgroundSucculentGardenText": "Градина със сукуленти",
"backgroundHotAirBalloonText": "горещ въздух балон",
"backgroundHeatherFieldText": "пирен поле",
"backgroundRainyBarnyardText": "Дъждовен фермерски двор",
"backgroundRelaxationRiverText": "Релаксация Река",
"backgroundFlyingOverGlacierNotes": "",
"backgroundUnderwaterRuinsText": "Подводен Руини",
"backgroundBeachCabanaText": "Плаж Кабана",
"backgroundSaltLakeText": "Сол Езеро",
"backgroundWintryCastleText": "Зимен Замък",
"backgroundVikingShipText": "Викинг Кораб",
"backgroundCampingOutText": "Къмпинг Навън"
} }
+2 -2
View File
@@ -4,7 +4,7 @@
"androidFaqStillNeedHelp": "Ако имате въпрос, който не намирате в този списък или в [ЧЗВ в уикито](http://habitica.fandom.com/wiki/FAQ), задайте го в кръчмата чрез Меню > Кръчма! Ще се радваме да помогнем.", "androidFaqStillNeedHelp": "Ако имате въпрос, който не намирате в този списък или в [ЧЗВ в уикито](http://habitica.fandom.com/wiki/FAQ), задайте го в кръчмата чрез Меню > Кръчма! Ще се радваме да помогнем.",
"webFaqStillNeedHelp": "Ако имате въпрос, който не намирате в този списък или в [ЧЗВ в уикито](http://habitica.fandom.com/wiki/FAQ), задайте го в [Помощната гилдия на Хабитика](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! Ще се радваме да помогнем.", "webFaqStillNeedHelp": "Ако имате въпрос, който не намирате в този списък или в [ЧЗВ в уикито](http://habitica.fandom.com/wiki/FAQ), задайте го в [Помощната гилдия на Хабитика](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! Ще се радваме да помогнем.",
"webFaqAnswer28": "Да! Бутона \"Пауза на щетите\" може да се намери в Настройки. Той ще ви предпази от загуба на точки живот (HP) за пропуснати ежедневни задачи. Това е полезно, ако сте на ваканция, нуждаете се от почивка или по какъвто и да било друг повод, за който имате нужда от почивка. Ако участвате в мисия, вашето собствено неприключило напредване ще бъде спряно, но все още ще получавате щети от пропуснатите ежедневни задачи на членовете на вашата група.\n\nЗа да поставите на пауза конкретни ежедневни задачи, можете да редактирате графика им, за да се изпълняват на всеки 0 дни, докато не сте готови да ги стартирате отново.", "webFaqAnswer28": "Да! Бутона \"Пауза на щетите\" може да се намери в Настройки. Той ще ви предпази от загуба на точки живот (HP) за пропуснати ежедневни задачи. Това е полезно, ако сте на ваканция, нуждаете се от почивка или по какъвто и да било друг повод, за който имате нужда от почивка. Ако участвате в мисия, вашето собствено неприключило напредване ще бъде спряно, но все още ще получавате щети от пропуснатите ежедневни задачи на членовете на вашата група.\n\nЗа да поставите на пауза конкретни ежедневни задачи, можете да редактирате графика им, за да се изпълняват на всеки 0 дни, докато не сте готови да ги стартирате отново.",
"webFaqAnswer32": "Всички играчи започват като клас \"Войн\", докато достигнат ниво 10. След като достигнете ниво 10, ще получите възможността да изберете нов клас или да продължите като Войн.\n\nВсеки клас разполага с различни Екипировка и Умения. Ако не искате да изберете клас, можете да изберете \"Отказ\". Ако изберете да се откажете, винаги можете да активирате Класовата система от Настройки по-късно.\n\nАко искате да промените класа си след ниво 10, можете да го направите, като използвате Орбът на прераждането. Орбът на прераждането е достъпен в Пазара за 6 диаманта на ниво 50 или безплатен на ниво 100.\n\nСъщо така, можете да промените своя клас по всяко време от Настройки за 3 диаманта. Това няма да нулира нивото ви като Орбът на прераждането, но ще ви позволи да преразпределите точките на уменията, които сте събрали, като сте вдигнали нивото си, за да са релевантни с новия ви клас.", "webFaqAnswer32": "В Habitica има четири класа: Войн, Магьосник, Крадец и Лечител. Всички играчи започват като клас \"Войн\", докато достигнат ниво 10. След като достигнете ниво 10, ще получите възможността да изберете нов клас или да продължите като Войн.\n\nВсеки клас разполага с различни Екипировка и Умения. Ако не искате да изберете клас, можете да изберете \"Отказ\". Ако изберете да се откажете, винаги можете да активирате Класовата система от Настройки по-късно.",
"commonQuestions": "Чести въпроси", "commonQuestions": "Чести въпроси",
"faqQuestion25": "Какви са различните видове задачи?", "faqQuestion25": "Какви са различните видове задачи?",
"webFaqAnswer25": "Habitica използва три различни типа задачи, за да отговори на вашите нужди: Навици, Ежедневни и Задачи.\n\nНавиците могат да бъдат положителни или отрицателни и представляват нещо, което искате да проследявате няколко пъти на ден или според незададен график. Положителните навици ще ви наградят със злато и опит (Exp), докато отрицателните навици ще ви наказват със загуба на точки живот (HP).\n\nЕжедневните задачи са повтарящи се задачи, които искате да изпълнявате по-структурирано. Например веднъж на ден, три пъти на седмица или четири пъти на месец. Пропускането на ежедневни задачи води до загуба на HP, но колкото по-трудни са, толкова по-добри са наградите!\n\nЗадачите са еднократни задачи, за които получавате награди след като ги изпълните. Задачите могат да имат срок, но няма загуба на HP, ако го пропуснете.\n\nИзберете типа задача, който най-добре отговаря на това, което искате да постигнете!", "webFaqAnswer25": "Habitica използва три различни типа задачи, за да отговори на вашите нужди: Навици, Ежедневни и Задачи.\n\nНавиците могат да бъдат положителни или отрицателни и представляват нещо, което искате да проследявате няколко пъти на ден или според незададен график. Положителните навици ще ви наградят със злато и опит (Exp), докато отрицателните навици ще ви наказват със загуба на точки живот (HP).\n\nЕжедневните задачи са повтарящи се задачи, които искате да изпълнявате по-структурирано. Например веднъж на ден, три пъти на седмица или четири пъти на месец. Пропускането на ежедневни задачи води до загуба на HP, но колкото по-трудни са, толкова по-добри са наградите!\n\nЗадачите са еднократни задачи, за които получавате награди след като ги изпълните. Задачите могат да имат срок, но няма загуба на HP, ако го пропуснете.\n\nИзберете типа задача, който най-добре отговаря на това, което искате да постигнете!",
@@ -16,7 +16,7 @@
"faqQuestion29": "Как да възстановя загубени точки живот (HP)?", "faqQuestion29": "Как да възстановя загубени точки живот (HP)?",
"webFaqAnswer29": "Можете да възвърнете 15 HP, като закупите отвара от колоната си за Награди, за 25 злато. Освен това винаги ще възвърнете пълното си HP, когато качите ниво!", "webFaqAnswer29": "Можете да възвърнете 15 HP, като закупите отвара от колоната си за Награди, за 25 злато. Освен това винаги ще възвърнете пълното си HP, когато качите ниво!",
"faqQuestion30": "Какво става, когато изчерпам HP?", "faqQuestion30": "Какво става, когато изчерпам HP?",
"webFaqAnswer30": "Ако вашето HP стигне до нула, ще загубите едно ниво, цялото си злато и един случаен предмет, който може да бъде закупен отново.", "webFaqAnswer30": "Ако вашите HP стигнат до нула, ще загубите едно ниво, цялото си злато и един случаен предмет, който може да бъде закупен отново.",
"faqQuestion31": "Защо загубих HP при неотрицателна задача ?", "faqQuestion31": "Защо загубих HP при неотрицателна задача ?",
"webFaqAnswer31": "Ако завършите задача и загубите HP, когато не би трябвало, сте срещнали забавяне, докато сървърът синхронизира промените, направени на други платформи. Например, ако използвате злато, мана или загубите HP в мобилното приложение и след това завършите задача в уебсайта, сървърът просто потвърждава, че всичко е синхронизирано.", "webFaqAnswer31": "Ако завършите задача и загубите HP, когато не би трябвало, сте срещнали забавяне, докато сървърът синхронизира промените, направени на други платформи. Например, ако използвате злато, мана или загубите HP в мобилното приложение и след това завършите задача в уебсайта, сървърът просто потвърждава, че всичко е синхронизирано.",
"faqQuestion32": "Кога мога да си избера клас?", "faqQuestion32": "Кога мога да си избера клас?",
+2 -21
View File
@@ -18,7 +18,7 @@
"resetAccPop": "Започнете отначало, премахвайки всички нива, злато, екипировка, история и задачи.", "resetAccPop": "Започнете отначало, премахвайки всички нива, злато, екипировка, история и задачи.",
"deleteAccount": "Изтриване на профила", "deleteAccount": "Изтриване на профила",
"deleteAccPop": "Изтрива и премахва Вашия профил в Хабитика.", "deleteAccPop": "Изтрива и премахва Вашия профил в Хабитика.",
"feedback": "Ако искате да ни изпратите отзивите си, моля, въведете ги по-долу. Ще се радваме да чуем обратната ви връзка! Ще бде анонимно, освен ако не изберете да въведете контактите си. Не говорите английски добре? Няма проблем! Пишете ни на езика, който предпочитате.", "feedback": "Ако искате да ни изпратите отзивите си, моля, въведете ги по-долу. Ще се радваме да научим какво Ви е харесало, или пък не, в Хабитика! Не говорите английски добре? Няма проблем! Пишете на който искате език.",
"dataExport": "Изнасяне на данни", "dataExport": "Изнасяне на данни",
"saveData": "Ето няколко възможности за запазване на данните Ви.", "saveData": "Ето няколко възможности за запазване на данните Ви.",
"habitHistory": "История на навиците", "habitHistory": "История на навиците",
@@ -157,24 +157,5 @@
"changeUsernameDisclaimer": "Потребителското ви име се ползва за покани, @споменавания в чата и съобщения, трябва да е от 1 до 20 символа, да съдържа само буквите от a до z, цифрите от 0 до 9, тирета или долни черти и не може да съдържа неприлични думи.", "changeUsernameDisclaimer": "Потребителското ви име се ползва за покани, @споменавания в чата и съобщения, трябва да е от 1 до 20 символа, да съдържа само буквите от a до z, цифрите от 0 до 9, тирета или долни черти и не може да съдържа неприлични думи.",
"verifyUsernameVeteranPet": "Един от тези любимци-ветерани ще Ви чака след като приключите с потвърждението!", "verifyUsernameVeteranPet": "Един от тези любимци-ветерани ще Ви чака след като приключите с потвърждението!",
"subscriptionReminders": "Абонаментни Напомняния", "subscriptionReminders": "Абонаментни Напомняния",
"newPMNotificationTitle": "Ново съобщение от <%= name %>", "newPMNotificationTitle": "Ново съобщение от <%= name %>"
"resetAccount": "Нулирай акаунт",
"generalSettings": "Общи настройки",
"taskSettings": "Настройки на Задачите",
"confirmCancelChanges": "Сигурни ли сте? Ще загубите незапазените промени.",
"account": "Акаунт",
"loginMethods": "Методи за Влизане",
"character": "Герой",
"siteLanguage": "Език на сайта",
"showLevelUpModal": "Когато вдигате ниво",
"showHatchPetModal": "Когато излюпвате Любимец",
"showRaisePetModal": "Когато отгледате Любимец до Оседлан Любимец",
"baileyAnnouncement": "Най-новите вести на Бейли",
"view": "Виж",
"feedbackPlaceholder": "Добавете обратна връзка",
"downloadCSV": "Изтеглете CSV",
"yourUserData": "Вашите Потребителски Данни",
"taskHistory": "История на Задачите",
"yourUserDataDisclaimer": "Тук можете да изтеглите копие на историята на задачите си или пълните си потребителски данни.",
"useridCopied": "Потребителският ID е копиран."
} }
+1 -4
View File
@@ -162,8 +162,5 @@
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!", "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ů!", "achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!", "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!", "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á"
} }
+5 -62
View File
@@ -117,7 +117,7 @@
"backgroundTavernNotes": "Navštiv krčmu města Habitica.", "backgroundTavernNotes": "Navštiv krčmu města Habitica.",
"backgrounds102015": "Sada 17: zveřejněna v říjnu 2015", "backgrounds102015": "Sada 17: zveřejněna v říjnu 2015",
"backgroundHarvestMoonText": "Měsíc při sklizni", "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", "backgroundSlimySwampText": "Slizká bažina",
"backgroundSlimySwampNotes": "Přebroď se slizkou bažinou.", "backgroundSlimySwampNotes": "Přebroď se slizkou bažinou.",
"backgroundSwarmingDarknessText": "Valící se temnota", "backgroundSwarmingDarknessText": "Valící se temnota",
@@ -213,7 +213,7 @@
"backgroundStormyRooftopsNotes": "Propliž se přes bouřlivé střechy.", "backgroundStormyRooftopsNotes": "Propliž se přes bouřlivé střechy.",
"backgroundWindyAutumnText": "Větrný podzim", "backgroundWindyAutumnText": "Větrný podzim",
"backgroundWindyAutumnNotes": "Hoň se za listy během větrného podzimu.", "backgroundWindyAutumnNotes": "Hoň se za listy během větrného podzimu.",
"incentiveBackgrounds": "Standardní pozadí", "incentiveBackgrounds": "Prosté pozadí",
"backgroundVioletText": "Fialová", "backgroundVioletText": "Fialová",
"backgroundVioletNotes": "Živá fialová tapeta.", "backgroundVioletNotes": "Živá fialová tapeta.",
"backgroundBlueText": "Modrá", "backgroundBlueText": "Modrá",
@@ -736,64 +736,7 @@
"backgroundMaskMakersWorkshopNotes": "Vyzkoušej novou tvář v maskářově dílně.", "backgroundMaskMakersWorkshopNotes": "Vyzkoušej novou tvář v maskářově dílně.",
"backgroundCemeteryGateText": "Hřbitovní brána", "backgroundCemeteryGateText": "Hřbitovní brána",
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány.", "backgroundCemeteryGateNotes": "Straš u hřbitovní brány.",
"backgroundAutumnBridgeText": "Most na podzim", "backgroundAutumnBridgeText": "Podzimní most",
"backgroundAutumnBridgeNotes": "Obdivuj krásu mostu na podzim.", "backgroundAutumnBridgeNotes": "Obdivuj krásu podzimního mostu.",
"backgroundInsideACrystalText": "Uvnitř krystalu", "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"
} }
+3 -14
View File
@@ -4,7 +4,7 @@
"brokenChaLink": "Nefunkční odkaz na výzvu", "brokenChaLink": "Nefunkční odkaz na výzvu",
"keepIt": "Ponechat", "keepIt": "Ponechat",
"removeIt": "Odstranit", "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?", "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?", "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", "challenges": "Výzvy",
@@ -85,7 +85,7 @@
"summaryRequired": "Je požadováno shrnutí", "summaryRequired": "Je požadováno shrnutí",
"summaryTooLong": "Shrnutí je příliš dlouhé", "summaryTooLong": "Shrnutí je příliš dlouhé",
"descriptionRequired": "Je požadován popis", "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í", "categoiresRequired": "Musí být vybrána jedna nebo více kategorií",
"viewProgressOf": "Zobrazit pokrok", "viewProgressOf": "Zobrazit pokrok",
"viewProgress": "Zobrazit pokrok", "viewProgress": "Zobrazit pokrok",
@@ -94,16 +94,5 @@
"selectParticipant": "Zvol účastníka", "selectParticipant": "Zvol účastníka",
"filters": "Filtry", "filters": "Filtry",
"wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.", "wonChallengeDesc": "Vyhrál/a jsi výzvu <%= challengeName %>! Tvá výhra je zaznamenána ve tvých úspěších.",
"yourReward": "Tvá odměna", "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"
} }
+2 -3
View File
@@ -54,7 +54,7 @@
"battleGear": "Bojová výzbroj", "battleGear": "Bojová výzbroj",
"gear": "Výbava", "gear": "Výbava",
"autoEquipBattleGear": "Automaticky použít nové vybavení", "autoEquipBattleGear": "Automaticky použít nové vybavení",
"costume": "kostým", "costume": "Kostým",
"useCostume": "Použít 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ě.", "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í.", "autoEquipPopoverText": "Zvol tuto možnost pro automatické nasazení koupeného vybavení.",
@@ -184,6 +184,5 @@
"chatCastSpellUser": "<%= username %> použil/a <%= spell %> na <%= target %>.", "chatCastSpellUser": "<%= username %> použil/a <%= spell %> na <%= target %>.",
"purchasePetItemConfirm": "Tento nákup by překročil počet položek, které potřebujete k vylíhnutí všech možných <%= itemText %> domácích zvířátek. Jsi si jistá?", "purchasePetItemConfirm": "Tento nákup by překročil počet položek, které potřebujete k vylíhnutí všech možných <%= itemText %> domácích zvířátek. Jsi si jistá?",
"notEnoughGold": "Nedostatek zlaťáků.", "notEnoughGold": "Nedostatek zlaťáků.",
"chatCastSpellPartyTimes": "<%= username %> použil/a <%= spell %> pro skupinu <%= times %> times.", "chatCastSpellPartyTimes": "<%= username %> použil/a <%= spell %> pro skupinu <%= times %> times."
"pointsAvailable": "Dostupné body"
} }
+2 -4
View File
@@ -1,5 +1,5 @@
{ {
"stable": "Mazlíčci a Mounty", "stable": "Stáj",
"pets": "Mazlíčci", "pets": "Mazlíčci",
"activePet": "Aktivní mazlíček", "activePet": "Aktivní mazlíček",
"noActivePet": "Bez aktivního mazlíčka", "noActivePet": "Bez aktivního mazlíčka",
@@ -109,7 +109,5 @@
"wackyPets": "Šílená zvířátka", "wackyPets": "Šílená zvířátka",
"invalidAmount": "Neplatný počet jídla,je vyžadováno pozitivní celé číslo", "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", "tooMuchFood": "Snažíš se dát svému zvířeti moc jídla, akce byla zrušena",
"notEnoughFood": "Nemáš dost jídla", "notEnoughFood": "Nemáš dost jídla"
"veteranCactus": "Kaktus Veterán",
"veteranDragon": "Drak Veterán"
} }
+1 -21
View File
@@ -160,25 +160,5 @@
"newPMNotificationTitle": "Nová zpráva od <%= name %>", "newPMNotificationTitle": "Nová zpráva od <%= name %>",
"displaynameIssueNewline": "Zobrazovaná jména nesmí obsahovat zpětné lomítko následované písmenem N.", "displaynameIssueNewline": "Zobrazovaná jména nesmí obsahovat zpětné lomítko následované písmenem N.",
"resetAccount": "Resetovat účet", "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!", "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."
} }
+1 -7
View File
@@ -935,11 +935,5 @@
"backgroundWaterfallWithRainbowText": "Wasserfall mit Regenbogen", "backgroundWaterfallWithRainbowText": "Wasserfall mit Regenbogen",
"backgroundWaterfallWithRainbowNotes": "Bewundere die atemberaubende Schönheit eines Wasserfalls mit Regenbogen.", "backgroundWaterfallWithRainbowNotes": "Bewundere die atemberaubende Schönheit eines Wasserfalls mit Regenbogen.",
"backgrounds042026": "SET 143: Veröffentlicht im April 2026", "backgrounds042026": "SET 143: Veröffentlicht im April 2026",
"backgrounds052026": "SET 144: Veröffentlicht im Mai 2026", "backgrounds052026": "SET 144: Veröffentlicht im Mai 2026"
"backgroundRidingACometText": "Ein Kometenritt",
"backgroundRidingACometNotes": "Reise durch das All bei einem Kometenritt!",
"backgroundElvenCitadelText": "Elven Citadel",
"backgroundElvenCitadelNotes": "Unternehmen Sie die malerische Reise zu einer Elfenzitadelle.",
"backgroundOnAStrangePlanetNotes": "Wage dich dorthin, wo noch kein Habitican gewesen ist: Auf einem fremden Planeten.",
"backgroundOnAStrangePlanetText": "un eine strange planete"
} }
+1 -2
View File
@@ -410,6 +410,5 @@
"questEggPlatypusText": "Schnabeltier", "questEggPlatypusText": "Schnabeltier",
"questEggPlatypusMountText": "Schnabeltier", "questEggPlatypusMountText": "Schnabeltier",
"questEggPlatypusAdjective": "ein Perfektionist", "questEggPlatypusAdjective": "ein Perfektionist",
"hatchingPotionOpal": "Opal", "hatchingPotionOpal": "Opal"
"hatchingPotionAlien": "Außerirdischer"
} }
+1 -3
View File
@@ -187,7 +187,5 @@
"minPasswordLengthLogin": "Dein Passwort ist mindestens 8 Zeichen lang.", "minPasswordLengthLogin": "Dein Passwort ist mindestens 8 Zeichen lang.",
"enterValidEmail": "Bitte gib eine gültige E-Mail-Adresse ein.", "enterValidEmail": "Bitte gib eine gültige E-Mail-Adresse ein.",
"whatToCallYou": "Wie sollen wir dich nennen?", "whatToCallYou": "Wie sollen wir dich nennen?",
"acceptPrivacyTOS": "Du bestätigst, dass du mindestens 18 Jahre alt bist und dass du unsere <a href='/static/terms' target='_blank'>Nutzungsbedingungen</a> und <a href='/static/privacy' target='_blank'>Datenschutz-Bestimmungen</a> gelesen hast und akzeptierst", "acceptPrivacyTOS": "Du bestätigst, dass du mindestens 18 Jahre alt bist und dass du unsere <a href='/static/terms' target='_blank'>Nutzungsbedingungen</a> und <a href='/static/privacy' target='_blank'>Datenschutz-Bestimmungen</a> gelesen hast und akzeptierst"
"emailAddress": "E-Mail_adresse",
"emailRequiredForSupport": "Wir benötigen eine E-Mail-Adresse für den Benutzersupport. Bitte geben Sie eine E-Mail-Adresse ein, um mit der Erstellung Ihres Kontos fortzufahren."
} }

Some files were not shown because too many files have changed in this diff Show More