diff --git a/.github/workflows/e2e-bdd.yml b/.github/workflows/e2e-bdd.yml
index b82b7f4..af2f86b 100644
--- a/.github/workflows/e2e-bdd.yml
+++ b/.github/workflows/e2e-bdd.yml
@@ -22,6 +22,9 @@ jobs:
- name: Install dependencies
run: npm ci
+ - name: Type check
+ run: npm run typecheck
+
- name: Install Playwright browser
run: npx playwright install --with-deps chromium
diff --git a/package-lock.json b/package-lock.json
index e892e8b..d586124 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -74,7 +74,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -528,6 +527,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/traverse": "^7.28.5"
@@ -544,6 +544,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
"integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -559,6 +560,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
"integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -574,6 +576,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
"integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
@@ -591,6 +594,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/traverse": "^7.28.6"
@@ -691,6 +695,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=6.9.0"
},
@@ -811,6 +816,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
@@ -997,6 +1003,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
"@babel/helper-plugin-utils": "^7.18.6"
@@ -1062,6 +1069,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
"integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1108,6 +1116,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
"integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-class-features-plugin": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1176,6 +1185,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1192,6 +1202,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
"integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1207,6 +1218,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz",
"integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1223,6 +1235,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
"integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1238,6 +1251,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
"integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6",
"@babel/plugin-transform-destructuring": "^7.28.5"
@@ -1254,6 +1268,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
@@ -1333,6 +1348,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
"integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.28.6"
},
@@ -1378,6 +1394,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
"integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1393,6 +1410,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
"integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1425,6 +1443,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz",
"integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helper-plugin-utils": "^7.28.6",
@@ -1443,6 +1462,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
"integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-module-transforms": "^7.27.1",
"@babel/helper-plugin-utils": "^7.27.1"
@@ -1475,6 +1495,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
"integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1539,6 +1560,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
"integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1",
"@babel/helper-replace-supers": "^7.27.1"
@@ -1634,6 +1656,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
"integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1759,6 +1782,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1775,6 +1799,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
"integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1865,6 +1890,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
"integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1880,6 +1906,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
"integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1914,6 +1941,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
"integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
@@ -1929,6 +1957,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
"integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -1961,6 +1990,7 @@
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
"integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
"@babel/helper-plugin-utils": "^7.28.6"
@@ -2062,6 +2092,7 @@
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz",
"integrity": "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-define-polyfill-provider": "^0.6.7",
"core-js-compat": "^3.48.0"
@@ -2075,6 +2106,7 @@
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
+ "peer": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -2101,6 +2133,7 @@
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/helper-plugin-utils": "^7.0.0",
"@babel/types": "^7.4.4",
@@ -2307,7 +2340,6 @@
"integrity": "sha512-7A/9CJpJDxv1SQ7hAZU0zPn2yRxx6XMR+LO4T94Enm3cYNWsEEj+RGX38NLX4INT+H6w5raX3Csb/qs4vUBsOA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@cucumber/ci-environment": "13.0.0",
"@cucumber/cucumber-expressions": "19.0.0",
@@ -2460,7 +2492,6 @@
"integrity": "sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@cucumber/messages": ">=31.0.0 <33"
}
@@ -2553,7 +2584,6 @@
"integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==",
"dev": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@cucumber/messages": ">=17.1.1"
}
@@ -2564,7 +2594,6 @@
"integrity": "sha512-1OSoW+GQvFUNAl6tdP2CTBexTXMNJF0094goVUcvugtQeXtJ0K8sCP0xbq7GGoiezs/eJAAOD03+zAPT64orHQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"class-transformer": "0.5.1",
"reflect-metadata": "0.2.2"
@@ -3452,7 +3481,6 @@
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"react-native": "*"
}
@@ -3746,7 +3774,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz",
"integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/component": "0.6.18",
"@firebase/logger": "0.4.4",
@@ -3813,7 +3840,6 @@
"resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz",
"integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@firebase/app": "0.13.2",
"@firebase/component": "0.6.18",
@@ -3829,8 +3855,7 @@
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz",
"integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==",
- "license": "Apache-2.0",
- "peer": true
+ "license": "Apache-2.0"
},
"node_modules/@firebase/auth": {
"version": "1.10.8",
@@ -4281,7 +4306,6 @@
"integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==",
"hasInstallScript": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"tslib": "^2.1.0"
},
@@ -5528,7 +5552,6 @@
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@react-navigation/core": "^6.4.17",
"escape-string-regexp": "^4.0.0",
@@ -5772,7 +5795,6 @@
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -6517,7 +6539,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -7740,6 +7761,7 @@
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"license": "BSD-2-Clause",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -7867,7 +7889,6 @@
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.49.tgz",
"integrity": "sha512-ge3gUnuyGEePWWKzPY7TQ7FsvtFTdmsdYDHeBVUjMr9KIoQig/gf8A03oH26p3UtTL6sUJcyOIg9vwIHGNPSUw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
"@expo/cli": "0.22.28",
@@ -12537,7 +12558,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -12621,7 +12641,6 @@
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.5.tgz",
"integrity": "sha512-op2p2kB+lqMF1D7AdX4+wvaR0OPFbvWYs+VBE7bwsb99Cn9xISrLRLAgFflZedQsa5HvnOGrULhtnmItbIKVVw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.6.3",
"@react-native/assets-registry": "0.76.5",
@@ -12699,7 +12718,6 @@
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.14.1.tgz",
"integrity": "sha512-+tUhT5WBl8nh5+P+chYhAjR470iCByf9z5EYdCEbPaAK3Yfzw+o8VRPnUgmPAKlSccOgQBxx3NOl/Wzckn9ujg==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"react": "*",
"react-native": "*"
@@ -12710,7 +12728,6 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-3.37.0.tgz",
"integrity": "sha512-vEi4qZqWYoGuVGuHTv1K2XA90rgSydksmR5+tb5uhL93whl6Bch6EEXzC+8eEfj4SimiCgXBPY7r/xTXJxvnUg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"react-freeze": "^1.0.0",
"warn-once": "^0.1.0"
diff --git a/package.json b/package.json
index 8c4b111..8638a1c 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"test:e2e:report": "cucumber-js --config cucumber.js --format progress-bar --format html:reports/cucumber-report.html",
"test:e2e:full": "cross-env EXPO_PUBLIC_E2E_MODE=true BASE_URL=http://localhost:3000 cucumber-js --config cucumber.js",
"test:e2e:ci": "cross-env EXPO_PUBLIC_E2E_MODE=true BASE_URL=http://localhost:3000 cucumber-js --config cucumber.js --format progress-bar --format html:reports/cucumber-report.html --format json:reports/cucumber-report.json",
+ "typecheck": "tsc --noEmit",
"test:unit": "node --import tsx --test tests/**/*.test.ts",
"admin:support": "node scripts/admin-support.mjs"
},
@@ -39,17 +40,16 @@
"react-native-web": "~0.19.13"
},
"devDependencies": {
- "@types/react": "~18.3.12",
- "typescript": "~5.3.3",
"@cucumber/cucumber": "^12.7.0",
+ "@types/react": "~18.3.12",
"cross-env": "^7.0.3",
"firebase-admin": "^12.6.0",
"playwright": "^1.52.0",
- "tsx": "^4.20.6"
+ "tsx": "^4.20.6",
+ "typescript": "~5.3.3"
},
"overrides": {
"tar": ">=7.5.10",
"@xmldom/xmldom": ">=0.8.0"
}
}
-
diff --git a/src/components/MilestoneAnimation.tsx b/src/components/MilestoneAnimation.tsx
index 53ee3f3..5066095 100644
--- a/src/components/MilestoneAnimation.tsx
+++ b/src/components/MilestoneAnimation.tsx
@@ -206,7 +206,7 @@ export default function MilestoneAnimation({
{Math.round(safeProgress * 100)}% visible
Stage {progressStage + 1} of 11
{theme === 'house' && (
- Land -> Foundation -> Walls -> Roof -> Landscaping
+ {'Land -> Foundation -> Walls -> Roof -> Landscaping'}
)}
)}
diff --git a/src/screens/auth/LoginScreen.tsx b/src/screens/auth/LoginScreen.tsx
index f1a917b..081ecc3 100644
--- a/src/screens/auth/LoginScreen.tsx
+++ b/src/screens/auth/LoginScreen.tsx
@@ -12,6 +12,7 @@ import {
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { login, resetPassword } from '../../services/authService';
+import { getErrorMessage } from '../../utils/errorUtils';
type Props = { navigation: NativeStackNavigationProp };
@@ -29,8 +30,8 @@ export default function LoginScreen({ navigation }: Props) {
setLoading(true);
try {
await login(email.trim(), password);
- } catch (e: any) {
- Alert.alert('Login failed', e.message);
+ } catch (error: unknown) {
+ Alert.alert('Login failed', getErrorMessage(error, 'Login failed. Please try again.'));
} finally {
setLoading(false);
}
@@ -45,8 +46,8 @@ export default function LoginScreen({ navigation }: Props) {
try {
await resetPassword(emailToReset);
Alert.alert('Reset link sent', `If an account exists for ${emailToReset}, a reset email has been sent.`);
- } catch (e: any) {
- Alert.alert('Reset failed', e.message);
+ } catch (error: unknown) {
+ Alert.alert('Reset failed', getErrorMessage(error, 'Reset failed. Please try again.'));
}
}
diff --git a/src/screens/auth/RegisterScreen.tsx b/src/screens/auth/RegisterScreen.tsx
index 1d0ccf9..7886c03 100644
--- a/src/screens/auth/RegisterScreen.tsx
+++ b/src/screens/auth/RegisterScreen.tsx
@@ -12,6 +12,7 @@ import {
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { register } from '../../services/authService';
+import { getErrorMessage } from '../../utils/errorUtils';
type Props = { navigation: NativeStackNavigationProp };
@@ -33,8 +34,8 @@ export default function RegisterScreen({ navigation }: Props) {
setLoading(true);
try {
await register(email.trim(), password);
- } catch (e: any) {
- Alert.alert('Registration failed', e.message);
+ } catch (error: unknown) {
+ Alert.alert('Registration failed', getErrorMessage(error, 'Registration failed. Please try again.'));
} finally {
setLoading(false);
}
diff --git a/src/screens/goals/CreateGoalScreen.tsx b/src/screens/goals/CreateGoalScreen.tsx
index bc5b51c..0f72d50 100644
--- a/src/screens/goals/CreateGoalScreen.tsx
+++ b/src/screens/goals/CreateGoalScreen.tsx
@@ -22,6 +22,7 @@ import SidebarNav from '../../components/SidebarNav';
import { formatCurrency, parseNumberInput } from '../../utils/format';
import { captureError, trackEvent } from '../../services/telemetryService';
import { showToast } from '../../services/toastService';
+import { getErrorMessage } from '../../utils/errorUtils';
type Nav = NativeStackNavigationProp;
@@ -113,9 +114,9 @@ export default function CreateGoalScreen() {
didCompleteRef.current = true;
showToast('Goal created successfully.', 'success');
navigation.replace('GoalDetail', { goalId });
- } catch (e: any) {
- captureError('create_goal_submit', e);
- showToast(e?.message ?? 'Failed to create goal.', 'error');
+ } catch (error: unknown) {
+ captureError('create_goal_submit', error);
+ showToast(getErrorMessage(error, 'Failed to create goal.'), 'error');
} finally {
setLoading(false);
}
diff --git a/src/screens/goals/DepositScreen.tsx b/src/screens/goals/DepositScreen.tsx
index 0becb61..d426ef0 100644
--- a/src/screens/goals/DepositScreen.tsx
+++ b/src/screens/goals/DepositScreen.tsx
@@ -26,6 +26,7 @@ import { isE2EMode } from '../../config/runtime';
import { formatCurrency, parseNumberInput } from '../../utils/format';
import { captureError, trackEvent } from '../../services/telemetryService';
import { showToast } from '../../services/toastService';
+import { getErrorMessage } from '../../utils/errorUtils';
type Nav = NativeStackNavigationProp;
type Route = RouteProp;
@@ -153,9 +154,9 @@ export default function DepositScreen() {
showToast('Deposit recorded.', 'success');
navigation.goBack();
}
- } catch (e: any) {
- captureError('deposit_submit', e);
- showToast(e?.message ?? 'Failed to record deposit.', 'error');
+ } catch (error: unknown) {
+ captureError('deposit_submit', error);
+ showToast(getErrorMessage(error, 'Failed to record deposit.'), 'error');
} finally {
setLoading(false);
}
diff --git a/src/screens/goals/EditGoalScreen.tsx b/src/screens/goals/EditGoalScreen.tsx
index c1920a6..2604cee 100644
--- a/src/screens/goals/EditGoalScreen.tsx
+++ b/src/screens/goals/EditGoalScreen.tsx
@@ -25,6 +25,7 @@ import SidebarNav from '../../components/SidebarNav';
import { formatCurrency } from '../../utils/format';
import { captureError, trackEvent } from '../../services/telemetryService';
import { showToast } from '../../services/toastService';
+import { getErrorMessage } from '../../utils/errorUtils';
type Props = NativeStackScreenProps;
@@ -124,9 +125,9 @@ export default function EditGoalScreen({ navigation, route }: Props) {
setWasSaved(true);
showToast('Goal changes saved.', 'success');
navigation.goBack();
- } catch (e: any) {
- captureError('edit_goal_save', e);
- showToast(e?.message ?? 'Failed to update goal.', 'error');
+ } catch (error: unknown) {
+ captureError('edit_goal_save', error);
+ showToast(getErrorMessage(error, 'Failed to update goal.'), 'error');
} finally {
setLoading(false);
}
@@ -150,9 +151,9 @@ export default function EditGoalScreen({ navigation, route }: Props) {
setWasSaved(true);
showToast('Goal deleted.', 'success');
navigation.navigate('AppTabs');
- } catch (e: any) {
- captureError('edit_goal_delete', e);
- showToast(e?.message ?? 'Failed to delete goal.', 'error');
+ } catch (error: unknown) {
+ captureError('edit_goal_delete', error);
+ showToast(getErrorMessage(error, 'Failed to delete goal.'), 'error');
} finally {
setLoading(false);
}
diff --git a/src/services/goalService.ts b/src/services/goalService.ts
index df7c19d..729bf59 100644
--- a/src/services/goalService.ts
+++ b/src/services/goalService.ts
@@ -39,8 +39,22 @@ export interface Goal {
currentBalance: number;
}
+/** Typed shape of a goal document as stored in Firestore. */
+interface GoalDocData {
+ userId: string;
+ name: string;
+ targetAmount: number;
+ monthlyContribution: number;
+ annualInterestRate: number;
+ timelineMonths?: number;
+ visualTheme?: ThemeType;
+ createdAt: Timestamp;
+ completedAt?: Timestamp;
+ currentBalance?: number;
+}
+
function mapGoal(snap: QueryDocumentSnapshot): Goal {
- const d = snap.data();
+ const d = snap.data() as GoalDocData;
return {
id: snap.id,
userId: d.userId,
@@ -82,7 +96,7 @@ export async function updateGoalBalance(goalId: string, newBalance: number): Pro
export async function markGoalCompleted(goalId: string): Promise {
if (isE2EMode) {
- e2eUpdateGoal(goalId, { completedAt: serverTimestamp() as unknown as Timestamp });
+ e2eUpdateGoal(goalId, { completedAt: Timestamp.now() });
return;
}
await updateDoc(doc(db, 'goals', goalId), { completedAt: serverTimestamp() });
diff --git a/src/utils/errorUtils.ts b/src/utils/errorUtils.ts
new file mode 100644
index 0000000..bd8c6c6
--- /dev/null
+++ b/src/utils/errorUtils.ts
@@ -0,0 +1,7 @@
+/**
+ * Safely extracts a human-readable message from an unknown caught value.
+ * Use this in catch blocks instead of `(e: any).message`.
+ */
+export function getErrorMessage(error: unknown, fallback = 'An unexpected error occurred.'): string {
+ return error instanceof Error ? error.message : fallback;
+}
diff --git a/tests/errorUtils.test.ts b/tests/errorUtils.test.ts
new file mode 100644
index 0000000..f74b212
--- /dev/null
+++ b/tests/errorUtils.test.ts
@@ -0,0 +1,30 @@
+import { describe, it } from 'node:test';
+import assert from 'node:assert/strict';
+import { getErrorMessage } from '../src/utils/errorUtils';
+
+describe('getErrorMessage', () => {
+ it('returns the error message when given an Error instance', () => {
+ const err = new Error('something went wrong');
+ assert.equal(getErrorMessage(err), 'something went wrong');
+ });
+
+ it('returns the fallback when given a non-Error value (string)', () => {
+ assert.equal(getErrorMessage('raw string'), 'An unexpected error occurred.');
+ });
+
+ it('returns the fallback when given a non-Error value (object)', () => {
+ assert.equal(getErrorMessage({ code: 42 }), 'An unexpected error occurred.');
+ });
+
+ it('returns the fallback when given null', () => {
+ assert.equal(getErrorMessage(null), 'An unexpected error occurred.');
+ });
+
+ it('returns the fallback when given undefined', () => {
+ assert.equal(getErrorMessage(undefined), 'An unexpected error occurred.');
+ });
+
+ it('uses the custom fallback when provided', () => {
+ assert.equal(getErrorMessage('not an Error', 'custom fallback'), 'custom fallback');
+ });
+});